Skip to main content

oximedia_gpu/
command_buffer.rs

1//! GPU command buffer recording and submission.
2#![allow(dead_code)]
3
4use std::collections::VecDeque;
5
6/// Type of a GPU command.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum CommandType {
9    /// Draw call
10    Draw,
11    /// Compute dispatch
12    Compute,
13    /// Copy / transfer
14    Copy,
15    /// Resource barrier / transition
16    Barrier,
17    /// Clear a render target
18    Clear,
19    /// Begin / end render pass markers
20    RenderPassMarker,
21}
22
23impl CommandType {
24    /// Returns `true` if the command represents a draw call.
25    #[must_use]
26    pub fn is_draw(&self) -> bool {
27        matches!(self, Self::Draw)
28    }
29
30    /// Returns `true` if the command is a compute dispatch.
31    #[must_use]
32    pub fn is_compute(&self) -> bool {
33        matches!(self, Self::Compute)
34    }
35
36    /// Returns `true` if the command transfers data between buffers/textures.
37    #[must_use]
38    pub fn is_copy(&self) -> bool {
39        matches!(self, Self::Copy)
40    }
41}
42
43/// A single recorded GPU command with metadata.
44#[derive(Debug, Clone)]
45pub struct CommandEntry {
46    /// Type of the command.
47    pub command_type: CommandType,
48    /// Opaque payload (e.g., serialised draw parameters).
49    pub payload: Vec<u8>,
50    /// Human-readable label for debugging.
51    pub label: String,
52}
53
54impl CommandEntry {
55    /// Create a new command entry.
56    #[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    /// Create a new entry with a raw payload.
66    #[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    /// Estimate the GPU cost (in arbitrary units) of executing this command.
80    ///
81    /// Draw calls are assumed to be more expensive than compute dispatches,
82    /// which in turn are more expensive than copies.
83    #[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        // Payload size adds a small overhead proportional to data moved.
95        base + self.payload.len() as f32 * 0.001
96    }
97}
98
99/// State of a [`CommandBuffer`].
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CommandBufferState {
102    /// Ready to record commands.
103    Recording,
104    /// Recording finished; ready to submit.
105    Executable,
106    /// Submitted to GPU queue; cannot be re-used until reset.
107    Pending,
108    /// Buffer has been reset and can start recording again.
109    Reset,
110}
111
112/// A GPU command buffer that records and submits work to the GPU.
113pub struct CommandBuffer {
114    commands: VecDeque<CommandEntry>,
115    state: CommandBufferState,
116    label: String,
117}
118
119impl CommandBuffer {
120    /// Create a new, empty command buffer in the `Recording` state.
121    #[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    /// Record a new command into the buffer.
131    ///
132    /// # Panics
133    ///
134    /// Panics if the buffer is not in the `Recording` state.
135    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    /// Finish recording and transition the buffer to `Executable`.
146    ///
147    /// Returns `false` if the buffer was not in `Recording` state.
148    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    /// Simulate submission to the GPU queue.
158    ///
159    /// Returns the list of submitted commands (for testing / inspection) and
160    /// transitions the buffer to `Pending`.
161    ///
162    /// Returns `None` if the buffer is not `Executable`.
163    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    /// Reset the buffer, clearing all recorded commands.
172    pub fn reset(&mut self) {
173        self.commands.clear();
174        self.state = CommandBufferState::Reset;
175    }
176
177    /// Begin a fresh recording pass after a reset.
178    ///
179    /// Returns `false` if the buffer was not in `Reset` state.
180    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    /// Number of recorded commands.
190    #[must_use]
191    pub fn command_count(&self) -> usize {
192        self.commands.len()
193    }
194
195    /// Current state of the buffer.
196    #[must_use]
197    pub fn state(&self) -> CommandBufferState {
198        self.state
199    }
200
201    /// Label of this buffer.
202    #[must_use]
203    pub fn label(&self) -> &str {
204        &self.label
205    }
206
207    /// Total estimated GPU cost of all recorded commands.
208    #[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#[cfg(test)]
216mod tests {
217    use super::*;
218
219    fn make_draw() -> CommandEntry {
220        CommandEntry::new(CommandType::Draw, "draw_quad")
221    }
222
223    fn make_compute() -> CommandEntry {
224        CommandEntry::new(CommandType::Compute, "dispatch_cs")
225    }
226
227    fn make_copy() -> CommandEntry {
228        CommandEntry::new(CommandType::Copy, "copy_buffer")
229    }
230
231    // --- CommandType tests ---
232
233    #[test]
234    fn test_is_draw_true() {
235        assert!(CommandType::Draw.is_draw());
236    }
237
238    #[test]
239    fn test_is_draw_false_for_compute() {
240        assert!(!CommandType::Compute.is_draw());
241    }
242
243    #[test]
244    fn test_is_compute_true() {
245        assert!(CommandType::Compute.is_compute());
246    }
247
248    #[test]
249    fn test_is_copy_true() {
250        assert!(CommandType::Copy.is_copy());
251    }
252
253    #[test]
254    fn test_is_copy_false_for_barrier() {
255        assert!(!CommandType::Barrier.is_copy());
256    }
257
258    // --- CommandEntry tests ---
259
260    #[test]
261    fn test_entry_estimated_cost_draw_greater_than_copy() {
262        let draw = make_draw();
263        let copy = make_copy();
264        assert!(draw.estimated_cost() > copy.estimated_cost());
265    }
266
267    #[test]
268    fn test_entry_estimated_cost_payload_increases_cost() {
269        let small = CommandEntry::with_payload(CommandType::Copy, "s", vec![0u8; 10]);
270        let large = CommandEntry::with_payload(CommandType::Copy, "l", vec![0u8; 1000]);
271        assert!(large.estimated_cost() > small.estimated_cost());
272    }
273
274    #[test]
275    fn test_entry_label_stored() {
276        let e = CommandEntry::new(CommandType::Draw, "my_draw");
277        assert_eq!(e.label, "my_draw");
278    }
279
280    // --- CommandBuffer tests ---
281
282    #[test]
283    fn test_new_buffer_is_recording() {
284        let buf = CommandBuffer::new("test");
285        assert_eq!(buf.state(), CommandBufferState::Recording);
286    }
287
288    #[test]
289    fn test_record_increments_count() {
290        let mut buf = CommandBuffer::new("test");
291        buf.record(make_draw());
292        buf.record(make_compute());
293        assert_eq!(buf.command_count(), 2);
294    }
295
296    #[test]
297    fn test_finish_transitions_to_executable() {
298        let mut buf = CommandBuffer::new("test");
299        buf.record(make_draw());
300        assert!(buf.finish());
301        assert_eq!(buf.state(), CommandBufferState::Executable);
302    }
303
304    #[test]
305    fn test_submit_returns_commands() {
306        let mut buf = CommandBuffer::new("test");
307        buf.record(make_draw());
308        buf.record(make_copy());
309        buf.finish();
310        let cmds = buf.submit().expect("submit should succeed");
311        assert_eq!(cmds.len(), 2);
312    }
313
314    #[test]
315    fn test_submit_transitions_to_pending() {
316        let mut buf = CommandBuffer::new("test");
317        buf.record(make_compute());
318        buf.finish();
319        buf.submit();
320        assert_eq!(buf.state(), CommandBufferState::Pending);
321    }
322
323    #[test]
324    fn test_reset_clears_commands_and_sets_reset_state() {
325        let mut buf = CommandBuffer::new("test");
326        buf.record(make_draw());
327        buf.finish();
328        buf.submit();
329        buf.reset();
330        assert_eq!(buf.command_count(), 0);
331        assert_eq!(buf.state(), CommandBufferState::Reset);
332    }
333
334    #[test]
335    fn test_begin_after_reset_allows_recording() {
336        let mut buf = CommandBuffer::new("test");
337        buf.reset();
338        assert!(buf.begin());
339        buf.record(make_draw());
340        assert_eq!(buf.command_count(), 1);
341    }
342
343    #[test]
344    fn test_total_estimated_cost_sums_entries() {
345        let mut buf = CommandBuffer::new("test");
346        buf.record(make_draw());
347        buf.record(make_copy());
348        let expected = make_draw().estimated_cost() + make_copy().estimated_cost();
349        assert!((buf.total_estimated_cost() - expected).abs() < 1e-4);
350    }
351
352    #[test]
353    fn test_label_stored() {
354        let buf = CommandBuffer::new("my_buf");
355        assert_eq!(buf.label(), "my_buf");
356    }
357
358    #[test]
359    fn test_submit_fails_when_not_executable() {
360        let mut buf = CommandBuffer::new("test");
361        buf.record(make_draw());
362        // Not finished yet — still Recording
363        assert!(buf.submit().is_none());
364    }
365
366    #[test]
367    fn test_finish_fails_when_already_executable() {
368        let mut buf = CommandBuffer::new("test");
369        buf.record(make_draw());
370        buf.finish();
371        // Calling finish again should return false
372        assert!(!buf.finish());
373    }
374}