oximedia_gpu/
command_buffer.rs1#![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#[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 #[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 #[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 #[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 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 assert!(!buf.finish());
373 }
374}