Skip to main content

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