astrelis_render/indirect.rs
1//! Indirect draw buffer support for GPU-driven rendering.
2//!
3//! This module provides type-safe wrappers for indirect draw commands and buffers.
4//! Indirect drawing allows the GPU to control draw parameters, enabling techniques
5//! like GPU culling and dynamic batching.
6//!
7//! # Feature Requirements
8//!
9//! - `INDIRECT_FIRST_INSTANCE`: Required for using `first_instance` in indirect commands.
10//! - `MULTI_DRAW_INDIRECT`: Required for multiple draw calls from a single buffer.
11
12use std::marker::PhantomData;
13
14use bytemuck::{Pod, Zeroable};
15
16use crate::context::GraphicsContext;
17use crate::features::GpuFeatures;
18
19/// Indirect draw command for non-indexed geometry.
20///
21/// This matches the layout expected by `wgpu::RenderPass::draw_indirect`.
22///
23/// # Fields
24///
25/// * `vertex_count` - Number of vertices to draw
26/// * `instance_count` - Number of instances to draw
27/// * `first_vertex` - Index of the first vertex to draw
28/// * `first_instance` - Instance ID of the first instance (requires INDIRECT_FIRST_INSTANCE)
29#[repr(C)]
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
31pub struct DrawIndirect {
32 pub vertex_count: u32,
33 pub instance_count: u32,
34 pub first_vertex: u32,
35 pub first_instance: u32,
36}
37
38// SAFETY: DrawIndirect is a repr(C) struct of u32s with no padding
39unsafe impl Pod for DrawIndirect {}
40unsafe impl Zeroable for DrawIndirect {}
41
42impl DrawIndirect {
43 /// Create a new indirect draw command.
44 pub const fn new(
45 vertex_count: u32,
46 instance_count: u32,
47 first_vertex: u32,
48 first_instance: u32,
49 ) -> Self {
50 Self {
51 vertex_count,
52 instance_count,
53 first_vertex,
54 first_instance,
55 }
56 }
57
58 /// Create a simple draw command for a single instance.
59 pub const fn single(vertex_count: u32) -> Self {
60 Self::new(vertex_count, 1, 0, 0)
61 }
62
63 /// Create a draw command for multiple instances.
64 pub const fn instanced(vertex_count: u32, instance_count: u32) -> Self {
65 Self::new(vertex_count, instance_count, 0, 0)
66 }
67
68 /// Size of the command in bytes.
69 pub const fn size() -> u64 {
70 std::mem::size_of::<Self>() as u64
71 }
72}
73
74/// Indirect draw command for indexed geometry.
75///
76/// This matches the layout expected by `wgpu::RenderPass::draw_indexed_indirect`.
77///
78/// # Fields
79///
80/// * `index_count` - Number of indices to draw
81/// * `instance_count` - Number of instances to draw
82/// * `first_index` - Index of the first index to draw
83/// * `base_vertex` - Value added to each index before indexing into the vertex buffer
84/// * `first_instance` - Instance ID of the first instance (requires INDIRECT_FIRST_INSTANCE)
85#[repr(C)]
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
87pub struct DrawIndexedIndirect {
88 pub index_count: u32,
89 pub instance_count: u32,
90 pub first_index: u32,
91 pub base_vertex: i32,
92 pub first_instance: u32,
93}
94
95// SAFETY: DrawIndexedIndirect is a repr(C) struct with no padding
96unsafe impl Pod for DrawIndexedIndirect {}
97unsafe impl Zeroable for DrawIndexedIndirect {}
98
99impl DrawIndexedIndirect {
100 /// Create a new indexed indirect draw command.
101 pub const fn new(
102 index_count: u32,
103 instance_count: u32,
104 first_index: u32,
105 base_vertex: i32,
106 first_instance: u32,
107 ) -> Self {
108 Self {
109 index_count,
110 instance_count,
111 first_index,
112 base_vertex,
113 first_instance,
114 }
115 }
116
117 /// Create a simple indexed draw command for a single instance.
118 pub const fn single(index_count: u32) -> Self {
119 Self::new(index_count, 1, 0, 0, 0)
120 }
121
122 /// Create an indexed draw command for multiple instances.
123 pub const fn instanced(index_count: u32, instance_count: u32) -> Self {
124 Self::new(index_count, instance_count, 0, 0, 0)
125 }
126
127 /// Size of the command in bytes.
128 pub const fn size() -> u64 {
129 std::mem::size_of::<Self>() as u64
130 }
131}
132
133/// Marker trait for indirect draw command types.
134pub trait IndirectCommand: Pod + Zeroable + Default {
135 /// Size of a single command in bytes.
136 const SIZE: u64;
137}
138
139impl IndirectCommand for DrawIndirect {
140 const SIZE: u64 = std::mem::size_of::<Self>() as u64;
141}
142
143impl IndirectCommand for DrawIndexedIndirect {
144 const SIZE: u64 = std::mem::size_of::<Self>() as u64;
145}
146
147/// A type-safe GPU buffer for indirect draw commands.
148///
149/// This wrapper ensures type safety and provides convenient methods for
150/// writing and using indirect draw commands.
151///
152/// # Type Parameters
153///
154/// * `T` - The type of indirect command (either `DrawIndirect` or `DrawIndexedIndirect`)
155///
156/// # Example
157///
158/// ```ignore
159/// use astrelis_render::{IndirectBuffer, DrawIndexedIndirect, Renderer};
160///
161/// // Create an indirect buffer for 100 indexed draw commands
162/// let indirect_buffer = IndirectBuffer::<DrawIndexedIndirect>::new(
163/// context,
164/// Some("My Indirect Buffer"),
165/// 100,
166/// );
167///
168/// // Write commands
169/// let commands = vec![
170/// DrawIndexedIndirect::single(36), // Draw 36 indices
171/// DrawIndexedIndirect::instanced(36, 10), // Draw 36 indices, 10 instances
172/// ];
173/// indirect_buffer.write(&context.queue, &commands);
174///
175/// // In render pass
176/// render_pass.draw_indexed_indirect(indirect_buffer.buffer(), 0);
177/// ```
178pub struct IndirectBuffer<T: IndirectCommand> {
179 buffer: wgpu::Buffer,
180 capacity: usize,
181 _marker: PhantomData<T>,
182}
183
184impl<T: IndirectCommand> IndirectBuffer<T> {
185 /// Create a new indirect buffer with the specified capacity.
186 ///
187 /// # Arguments
188 ///
189 /// * `context` - The graphics context
190 /// * `label` - Optional debug label
191 /// * `capacity` - Maximum number of commands the buffer can hold
192 ///
193 /// # Panics
194 ///
195 /// Panics if `INDIRECT_FIRST_INSTANCE` feature is not enabled on the context.
196 pub fn new(
197 context: &GraphicsContext,
198 label: Option<&str>,
199 capacity: usize,
200 ) -> Self {
201 // Check that required feature is available
202 context.require_feature(GpuFeatures::INDIRECT_FIRST_INSTANCE);
203
204 let buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
205 label,
206 size: T::SIZE * capacity as u64,
207 usage: wgpu::BufferUsages::INDIRECT
208 | wgpu::BufferUsages::COPY_DST
209 | wgpu::BufferUsages::STORAGE,
210 mapped_at_creation: false,
211 });
212
213 Self {
214 buffer,
215 capacity,
216 _marker: PhantomData,
217 }
218 }
219
220 /// Create a new indirect buffer initialized with commands.
221 ///
222 /// # Arguments
223 ///
224 /// * `context` - The graphics context
225 /// * `label` - Optional debug label
226 /// * `commands` - Initial commands to write to the buffer
227 ///
228 /// # Panics
229 ///
230 /// Panics if `INDIRECT_FIRST_INSTANCE` feature is not enabled on the context.
231 pub fn new_init(
232 context: &GraphicsContext,
233 label: Option<&str>,
234 commands: &[T],
235 ) -> Self {
236 context.require_feature(GpuFeatures::INDIRECT_FIRST_INSTANCE);
237
238 let buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
239 label,
240 size: T::SIZE * commands.len() as u64,
241 usage: wgpu::BufferUsages::INDIRECT
242 | wgpu::BufferUsages::COPY_DST
243 | wgpu::BufferUsages::STORAGE,
244 mapped_at_creation: false,
245 });
246
247 context
248 .queue
249 .write_buffer(&buffer, 0, bytemuck::cast_slice(commands));
250
251 Self {
252 buffer,
253 capacity: commands.len(),
254 _marker: PhantomData,
255 }
256 }
257
258 /// Get the underlying wgpu buffer.
259 pub fn buffer(&self) -> &wgpu::Buffer {
260 &self.buffer
261 }
262
263 /// Get the capacity (maximum number of commands).
264 pub fn capacity(&self) -> usize {
265 self.capacity
266 }
267
268 /// Get the size of the buffer in bytes.
269 pub fn size_bytes(&self) -> u64 {
270 T::SIZE * self.capacity as u64
271 }
272
273 /// Get the byte offset of a command at the given index.
274 pub fn offset_of(&self, index: usize) -> u64 {
275 T::SIZE * index as u64
276 }
277
278 /// Write commands to the buffer starting at the given index.
279 ///
280 /// # Arguments
281 ///
282 /// * `queue` - The command queue to use for the write
283 /// * `start_index` - Index of the first command to write
284 /// * `commands` - Commands to write
285 ///
286 /// # Panics
287 ///
288 /// Panics if the write would exceed the buffer capacity.
289 pub fn write_at(&self, queue: &wgpu::Queue, start_index: usize, commands: &[T]) {
290 assert!(
291 start_index + commands.len() <= self.capacity,
292 "Indirect buffer write would exceed capacity: {} + {} > {}",
293 start_index,
294 commands.len(),
295 self.capacity
296 );
297
298 let offset = T::SIZE * start_index as u64;
299 queue.write_buffer(&self.buffer, offset, bytemuck::cast_slice(commands));
300 }
301
302 /// Write commands to the buffer starting at index 0.
303 ///
304 /// # Arguments
305 ///
306 /// * `queue` - The command queue to use for the write
307 /// * `commands` - Commands to write
308 ///
309 /// # Panics
310 ///
311 /// Panics if the write would exceed the buffer capacity.
312 pub fn write(&self, queue: &wgpu::Queue, commands: &[T]) {
313 self.write_at(queue, 0, commands);
314 }
315
316 /// Clear the buffer by writing zeros.
317 pub fn clear(&self, queue: &wgpu::Queue) {
318 let zeros = vec![T::default(); self.capacity];
319 queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&zeros));
320 }
321}
322
323/// Extension trait for render passes to use indirect buffers.
324pub trait RenderPassIndirectExt<'a> {
325 /// Draw non-indexed geometry using an indirect buffer.
326 ///
327 /// # Arguments
328 ///
329 /// * `indirect_buffer` - Buffer containing draw commands
330 /// * `index` - Index of the command to execute
331 fn draw_indirect_at(&mut self, indirect_buffer: &'a IndirectBuffer<DrawIndirect>, index: usize);
332
333 /// Draw indexed geometry using an indirect buffer.
334 ///
335 /// # Arguments
336 ///
337 /// * `indirect_buffer` - Buffer containing draw commands
338 /// * `index` - Index of the command to execute
339 fn draw_indexed_indirect_at(
340 &mut self,
341 indirect_buffer: &'a IndirectBuffer<DrawIndexedIndirect>,
342 index: usize,
343 );
344}
345
346impl<'a> RenderPassIndirectExt<'a> for wgpu::RenderPass<'a> {
347 fn draw_indirect_at(
348 &mut self,
349 indirect_buffer: &'a IndirectBuffer<DrawIndirect>,
350 index: usize,
351 ) {
352 let offset = indirect_buffer.offset_of(index);
353 self.draw_indirect(indirect_buffer.buffer(), offset);
354 }
355
356 fn draw_indexed_indirect_at(
357 &mut self,
358 indirect_buffer: &'a IndirectBuffer<DrawIndexedIndirect>,
359 index: usize,
360 ) {
361 let offset = indirect_buffer.offset_of(index);
362 self.draw_indexed_indirect(indirect_buffer.buffer(), offset);
363 }
364}
365
366/// Extension trait for multi-draw indirect operations.
367///
368/// Requires the `MULTI_DRAW_INDIRECT` feature.
369pub trait RenderPassMultiDrawIndirectExt<'a> {
370 /// Draw non-indexed geometry multiple times using an indirect buffer.
371 ///
372 /// # Arguments
373 ///
374 /// * `indirect_buffer` - Buffer containing draw commands
375 /// * `start_index` - Index of the first command to execute
376 /// * `count` - Number of commands to execute
377 ///
378 /// # Panics
379 ///
380 /// Panics if `MULTI_DRAW_INDIRECT` feature is not enabled.
381 fn multi_draw_indirect(
382 &mut self,
383 indirect_buffer: &'a IndirectBuffer<DrawIndirect>,
384 start_index: usize,
385 count: u32,
386 );
387
388 /// Draw indexed geometry multiple times using an indirect buffer.
389 ///
390 /// # Arguments
391 ///
392 /// * `indirect_buffer` - Buffer containing draw commands
393 /// * `start_index` - Index of the first command to execute
394 /// * `count` - Number of commands to execute
395 ///
396 /// # Panics
397 ///
398 /// Panics if `MULTI_DRAW_INDIRECT` feature is not enabled.
399 fn multi_draw_indexed_indirect(
400 &mut self,
401 indirect_buffer: &'a IndirectBuffer<DrawIndexedIndirect>,
402 start_index: usize,
403 count: u32,
404 );
405}
406
407impl<'a> RenderPassMultiDrawIndirectExt<'a> for wgpu::RenderPass<'a> {
408 fn multi_draw_indirect(
409 &mut self,
410 indirect_buffer: &'a IndirectBuffer<DrawIndirect>,
411 start_index: usize,
412 count: u32,
413 ) {
414 let offset = indirect_buffer.offset_of(start_index);
415 self.multi_draw_indirect(indirect_buffer.buffer(), offset, count);
416 }
417
418 fn multi_draw_indexed_indirect(
419 &mut self,
420 indirect_buffer: &'a IndirectBuffer<DrawIndexedIndirect>,
421 start_index: usize,
422 count: u32,
423 ) {
424 let offset = indirect_buffer.offset_of(start_index);
425 self.multi_draw_indexed_indirect(indirect_buffer.buffer(), offset, count);
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_draw_indirect_size() {
435 // Verify the struct matches wgpu's expected layout
436 assert_eq!(DrawIndirect::size(), 16); // 4 u32s = 16 bytes
437 assert_eq!(DrawIndirect::SIZE, 16);
438 }
439
440 #[test]
441 fn test_draw_indexed_indirect_size() {
442 // Verify the struct matches wgpu's expected layout
443 assert_eq!(DrawIndexedIndirect::size(), 20); // 4 u32s + 1 i32 = 20 bytes
444 assert_eq!(DrawIndexedIndirect::SIZE, 20);
445 }
446
447 #[test]
448 fn test_draw_indirect_single() {
449 let cmd = DrawIndirect::single(36);
450 assert_eq!(cmd.vertex_count, 36);
451 assert_eq!(cmd.instance_count, 1);
452 assert_eq!(cmd.first_vertex, 0);
453 assert_eq!(cmd.first_instance, 0);
454 }
455
456 #[test]
457 fn test_draw_indexed_indirect_instanced() {
458 let cmd = DrawIndexedIndirect::instanced(36, 100);
459 assert_eq!(cmd.index_count, 36);
460 assert_eq!(cmd.instance_count, 100);
461 assert_eq!(cmd.first_index, 0);
462 assert_eq!(cmd.base_vertex, 0);
463 assert_eq!(cmd.first_instance, 0);
464 }
465}