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}