1#![allow(dead_code)]
3
4use std::collections::VecDeque;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum CommandType {
9 Draw,
11 Compute,
13 Copy,
15 Barrier,
17 Clear,
19 RenderPassMarker,
21}
22
23impl CommandType {
24 #[must_use]
26 pub fn is_draw(&self) -> bool {
27 matches!(self, Self::Draw)
28 }
29
30 #[must_use]
32 pub fn is_compute(&self) -> bool {
33 matches!(self, Self::Compute)
34 }
35
36 #[must_use]
38 pub fn is_copy(&self) -> bool {
39 matches!(self, Self::Copy)
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct CommandEntry {
46 pub command_type: CommandType,
48 pub payload: Vec<u8>,
50 pub label: String,
52}
53
54impl CommandEntry {
55 #[must_use]
57 pub fn new(command_type: CommandType, label: impl Into<String>) -> Self {
58 Self {
59 command_type,
60 payload: Vec::new(),
61 label: label.into(),
62 }
63 }
64
65 #[must_use]
67 pub fn with_payload(
68 command_type: CommandType,
69 label: impl Into<String>,
70 payload: Vec<u8>,
71 ) -> Self {
72 Self {
73 command_type,
74 payload,
75 label: label.into(),
76 }
77 }
78
79 #[allow(clippy::cast_precision_loss)]
84 #[must_use]
85 pub fn estimated_cost(&self) -> f32 {
86 let base: f32 = match self.command_type {
87 CommandType::Draw => 10.0,
88 CommandType::Compute => 8.0,
89 CommandType::Copy => 3.0,
90 CommandType::Barrier => 1.0,
91 CommandType::Clear => 2.0,
92 CommandType::RenderPassMarker => 0.1,
93 };
94 base + self.payload.len() as f32 * 0.001
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CommandBufferState {
102 Recording,
104 Executable,
106 Pending,
108 Reset,
110}
111
112pub struct CommandBuffer {
114 commands: VecDeque<CommandEntry>,
115 state: CommandBufferState,
116 label: String,
117}
118
119impl CommandBuffer {
120 #[must_use]
122 pub fn new(label: impl Into<String>) -> Self {
123 Self {
124 commands: VecDeque::new(),
125 state: CommandBufferState::Recording,
126 label: label.into(),
127 }
128 }
129
130 pub fn record(&mut self, entry: CommandEntry) {
136 assert_eq!(
137 self.state,
138 CommandBufferState::Recording,
139 "CommandBuffer '{}' must be in Recording state to accept new commands",
140 self.label
141 );
142 self.commands.push_back(entry);
143 }
144
145 pub fn finish(&mut self) -> bool {
149 if self.state == CommandBufferState::Recording {
150 self.state = CommandBufferState::Executable;
151 true
152 } else {
153 false
154 }
155 }
156
157 pub fn submit(&mut self) -> Option<Vec<CommandEntry>> {
164 if self.state != CommandBufferState::Executable {
165 return None;
166 }
167 self.state = CommandBufferState::Pending;
168 Some(self.commands.iter().cloned().collect())
169 }
170
171 pub fn reset(&mut self) {
173 self.commands.clear();
174 self.state = CommandBufferState::Reset;
175 }
176
177 pub fn begin(&mut self) -> bool {
181 if self.state == CommandBufferState::Reset {
182 self.state = CommandBufferState::Recording;
183 true
184 } else {
185 false
186 }
187 }
188
189 #[must_use]
191 pub fn command_count(&self) -> usize {
192 self.commands.len()
193 }
194
195 #[must_use]
197 pub fn state(&self) -> CommandBufferState {
198 self.state
199 }
200
201 #[must_use]
203 pub fn label(&self) -> &str {
204 &self.label
205 }
206
207 #[allow(clippy::cast_precision_loss)]
209 #[must_use]
210 pub fn total_estimated_cost(&self) -> f32 {
211 self.commands.iter().map(CommandEntry::estimated_cost).sum()
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum BufferSlot {
222 A,
224 B,
226}
227
228impl BufferSlot {
229 #[must_use]
231 pub fn flip(self) -> Self {
232 match self {
233 Self::A => Self::B,
234 Self::B => Self::A,
235 }
236 }
237
238 #[must_use]
240 pub fn index(self) -> usize {
241 match self {
242 Self::A => 0,
243 Self::B => 1,
244 }
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub enum SlotState {
251 Idle,
253 Recording,
255 ReadyToSubmit,
257 Inflight,
259 Retired,
261}
262
263pub struct DoubleBufferedSubmitter {
281 slots: [CommandBuffer; 2],
282 slot_states: [SlotState; 2],
283 active_slot: BufferSlot,
285 frame_count: u64,
287}
288
289impl DoubleBufferedSubmitter {
290 #[must_use]
292 pub fn new() -> Self {
293 let mut slot_a = CommandBuffer::new("DoubleBuffer-SlotA");
294 let mut slot_b = CommandBuffer::new("DoubleBuffer-SlotB");
295 slot_a.reset();
297 slot_b.reset();
298 Self {
299 slots: [slot_a, slot_b],
300 slot_states: [SlotState::Idle, SlotState::Idle],
301 active_slot: BufferSlot::A,
302 frame_count: 0,
303 }
304 }
305
306 #[must_use]
308 pub fn state(&self, slot: BufferSlot) -> SlotState {
309 self.slot_states[slot.index()]
310 }
311
312 #[must_use]
314 pub fn active_slot(&self) -> BufferSlot {
315 self.active_slot
316 }
317
318 #[must_use]
320 pub fn frame_count(&self) -> u64 {
321 self.frame_count
322 }
323
324 pub fn begin_record(&mut self, slot: BufferSlot) -> bool {
328 if self.slot_states[slot.index()] != SlotState::Idle {
329 return false;
330 }
331 self.slots[slot.index()].begin();
332 self.slot_states[slot.index()] = SlotState::Recording;
333 self.active_slot = slot;
334 true
335 }
336
337 pub fn record(&mut self, slot: BufferSlot, entry: CommandEntry) -> bool {
341 if self.slot_states[slot.index()] != SlotState::Recording {
342 return false;
343 }
344 self.slots[slot.index()].record(entry);
345 true
346 }
347
348 pub fn finish_record(&mut self, slot: BufferSlot) -> bool {
352 if self.slot_states[slot.index()] != SlotState::Recording {
353 return false;
354 }
355 let ok = self.slots[slot.index()].finish();
356 if ok {
357 self.slot_states[slot.index()] = SlotState::ReadyToSubmit;
358 }
359 ok
360 }
361
362 pub fn submit(&mut self, slot: BufferSlot) -> Option<Vec<CommandEntry>> {
367 if self.slot_states[slot.index()] != SlotState::ReadyToSubmit {
368 return None;
369 }
370 let cmds = self.slots[slot.index()].submit()?;
371 self.slot_states[slot.index()] = SlotState::Inflight;
372 self.frame_count += 1;
373 Some(cmds)
374 }
375
376 pub fn mark_retired(&mut self, slot: BufferSlot) -> bool {
380 if self.slot_states[slot.index()] != SlotState::Inflight {
381 return false;
382 }
383 self.slot_states[slot.index()] = SlotState::Retired;
384 true
385 }
386
387 pub fn reset_slot(&mut self, slot: BufferSlot) -> bool {
391 if self.slot_states[slot.index()] != SlotState::Retired {
392 return false;
393 }
394 self.slots[slot.index()].reset();
395 self.slot_states[slot.index()] = SlotState::Idle;
396 true
397 }
398
399 #[must_use]
401 pub fn command_count(&self, slot: BufferSlot) -> usize {
402 self.slots[slot.index()].command_count()
403 }
404}
405
406impl Default for DoubleBufferedSubmitter {
407 fn default() -> Self {
408 Self::new()
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 fn make_draw() -> CommandEntry {
417 CommandEntry::new(CommandType::Draw, "draw_quad")
418 }
419
420 fn make_compute() -> CommandEntry {
421 CommandEntry::new(CommandType::Compute, "dispatch_cs")
422 }
423
424 fn make_copy() -> CommandEntry {
425 CommandEntry::new(CommandType::Copy, "copy_buffer")
426 }
427
428 #[test]
431 fn test_is_draw_true() {
432 assert!(CommandType::Draw.is_draw());
433 }
434
435 #[test]
436 fn test_is_draw_false_for_compute() {
437 assert!(!CommandType::Compute.is_draw());
438 }
439
440 #[test]
441 fn test_is_compute_true() {
442 assert!(CommandType::Compute.is_compute());
443 }
444
445 #[test]
446 fn test_is_copy_true() {
447 assert!(CommandType::Copy.is_copy());
448 }
449
450 #[test]
451 fn test_is_copy_false_for_barrier() {
452 assert!(!CommandType::Barrier.is_copy());
453 }
454
455 #[test]
458 fn test_entry_estimated_cost_draw_greater_than_copy() {
459 let draw = make_draw();
460 let copy = make_copy();
461 assert!(draw.estimated_cost() > copy.estimated_cost());
462 }
463
464 #[test]
465 fn test_entry_estimated_cost_payload_increases_cost() {
466 let small = CommandEntry::with_payload(CommandType::Copy, "s", vec![0u8; 10]);
467 let large = CommandEntry::with_payload(CommandType::Copy, "l", vec![0u8; 1000]);
468 assert!(large.estimated_cost() > small.estimated_cost());
469 }
470
471 #[test]
472 fn test_entry_label_stored() {
473 let e = CommandEntry::new(CommandType::Draw, "my_draw");
474 assert_eq!(e.label, "my_draw");
475 }
476
477 #[test]
480 fn test_new_buffer_is_recording() {
481 let buf = CommandBuffer::new("test");
482 assert_eq!(buf.state(), CommandBufferState::Recording);
483 }
484
485 #[test]
486 fn test_record_increments_count() {
487 let mut buf = CommandBuffer::new("test");
488 buf.record(make_draw());
489 buf.record(make_compute());
490 assert_eq!(buf.command_count(), 2);
491 }
492
493 #[test]
494 fn test_finish_transitions_to_executable() {
495 let mut buf = CommandBuffer::new("test");
496 buf.record(make_draw());
497 assert!(buf.finish());
498 assert_eq!(buf.state(), CommandBufferState::Executable);
499 }
500
501 #[test]
502 fn test_submit_returns_commands() {
503 let mut buf = CommandBuffer::new("test");
504 buf.record(make_draw());
505 buf.record(make_copy());
506 buf.finish();
507 let cmds = buf.submit().expect("submit should succeed");
508 assert_eq!(cmds.len(), 2);
509 }
510
511 #[test]
512 fn test_submit_transitions_to_pending() {
513 let mut buf = CommandBuffer::new("test");
514 buf.record(make_compute());
515 buf.finish();
516 buf.submit();
517 assert_eq!(buf.state(), CommandBufferState::Pending);
518 }
519
520 #[test]
521 fn test_reset_clears_commands_and_sets_reset_state() {
522 let mut buf = CommandBuffer::new("test");
523 buf.record(make_draw());
524 buf.finish();
525 buf.submit();
526 buf.reset();
527 assert_eq!(buf.command_count(), 0);
528 assert_eq!(buf.state(), CommandBufferState::Reset);
529 }
530
531 #[test]
532 fn test_begin_after_reset_allows_recording() {
533 let mut buf = CommandBuffer::new("test");
534 buf.reset();
535 assert!(buf.begin());
536 buf.record(make_draw());
537 assert_eq!(buf.command_count(), 1);
538 }
539
540 #[test]
541 fn test_total_estimated_cost_sums_entries() {
542 let mut buf = CommandBuffer::new("test");
543 buf.record(make_draw());
544 buf.record(make_copy());
545 let expected = make_draw().estimated_cost() + make_copy().estimated_cost();
546 assert!((buf.total_estimated_cost() - expected).abs() < 1e-4);
547 }
548
549 #[test]
550 fn test_label_stored() {
551 let buf = CommandBuffer::new("my_buf");
552 assert_eq!(buf.label(), "my_buf");
553 }
554
555 #[test]
556 fn test_submit_fails_when_not_executable() {
557 let mut buf = CommandBuffer::new("test");
558 buf.record(make_draw());
559 assert!(buf.submit().is_none());
561 }
562
563 #[test]
564 fn test_finish_fails_when_already_executable() {
565 let mut buf = CommandBuffer::new("test");
566 buf.record(make_draw());
567 buf.finish();
568 assert!(!buf.finish());
570 }
571
572 #[test]
575 fn test_buffer_slot_flip() {
576 assert_eq!(BufferSlot::A.flip(), BufferSlot::B);
577 assert_eq!(BufferSlot::B.flip(), BufferSlot::A);
578 }
579
580 #[test]
581 fn test_buffer_slot_index() {
582 assert_eq!(BufferSlot::A.index(), 0);
583 assert_eq!(BufferSlot::B.index(), 1);
584 }
585
586 #[test]
589 fn test_double_buffer_initial_state() {
590 let db = DoubleBufferedSubmitter::new();
591 assert_eq!(db.state(BufferSlot::A), SlotState::Idle);
592 assert_eq!(db.state(BufferSlot::B), SlotState::Idle);
593 assert_eq!(db.frame_count(), 0);
594 }
595
596 #[test]
597 fn test_double_buffer_begin_record() {
598 let mut db = DoubleBufferedSubmitter::new();
599 assert!(db.begin_record(BufferSlot::A));
600 assert_eq!(db.state(BufferSlot::A), SlotState::Recording);
601 }
602
603 #[test]
604 fn test_double_buffer_begin_record_fails_when_not_idle() {
605 let mut db = DoubleBufferedSubmitter::new();
606 db.begin_record(BufferSlot::A);
607 assert!(!db.begin_record(BufferSlot::A));
609 }
610
611 #[test]
612 fn test_double_buffer_full_cycle_slot_a() {
613 let mut db = DoubleBufferedSubmitter::new();
614 assert!(db.begin_record(BufferSlot::A));
615 db.record(BufferSlot::A, make_draw());
616 assert!(db.finish_record(BufferSlot::A));
617 assert_eq!(db.state(BufferSlot::A), SlotState::ReadyToSubmit);
618 let cmds = db.submit(BufferSlot::A).expect("submit should succeed");
619 assert_eq!(cmds.len(), 1);
620 assert_eq!(db.state(BufferSlot::A), SlotState::Inflight);
621 assert_eq!(db.frame_count(), 1);
622 assert!(db.mark_retired(BufferSlot::A));
623 assert_eq!(db.state(BufferSlot::A), SlotState::Retired);
624 assert!(db.reset_slot(BufferSlot::A));
625 assert_eq!(db.state(BufferSlot::A), SlotState::Idle);
626 }
627
628 #[test]
629 fn test_double_buffer_interleaved_slots() {
630 let mut db = DoubleBufferedSubmitter::new();
631 db.begin_record(BufferSlot::A);
633 db.record(BufferSlot::A, make_compute());
634 db.finish_record(BufferSlot::A);
635 db.begin_record(BufferSlot::B);
637 db.record(BufferSlot::B, make_copy());
638 db.finish_record(BufferSlot::B);
639 assert!(db.submit(BufferSlot::A).is_some());
641 assert!(db.submit(BufferSlot::B).is_some());
642 assert_eq!(db.frame_count(), 2);
643 }
644
645 #[test]
646 fn test_double_buffer_submit_fails_when_not_ready() {
647 let mut db = DoubleBufferedSubmitter::new();
648 db.begin_record(BufferSlot::A);
649 assert!(db.submit(BufferSlot::A).is_none());
651 }
652
653 #[test]
654 fn test_double_buffer_mark_retired_fails_when_not_inflight() {
655 let mut db = DoubleBufferedSubmitter::new();
656 assert!(!db.mark_retired(BufferSlot::A));
657 }
658
659 #[test]
660 fn test_double_buffer_reset_slot_fails_when_not_retired() {
661 let mut db = DoubleBufferedSubmitter::new();
662 assert!(!db.reset_slot(BufferSlot::A));
663 }
664
665 #[test]
666 fn test_double_buffer_command_count() {
667 let mut db = DoubleBufferedSubmitter::new();
668 db.begin_record(BufferSlot::B);
669 db.record(BufferSlot::B, make_draw());
670 db.record(BufferSlot::B, make_draw());
671 assert_eq!(db.command_count(BufferSlot::B), 2);
672 }
673}