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(context: &GraphicsContext, label: Option<&str>, capacity: usize) -> Self {
198        // Check that required feature is available
199        context.require_feature(GpuFeatures::INDIRECT_FIRST_INSTANCE);
200
201        let buffer = context.device().create_buffer(&wgpu::BufferDescriptor {
202            label,
203            size: T::SIZE * capacity as u64,
204            usage: wgpu::BufferUsages::INDIRECT
205                | wgpu::BufferUsages::COPY_DST
206                | wgpu::BufferUsages::STORAGE,
207            mapped_at_creation: false,
208        });
209
210        Self {
211            buffer,
212            capacity,
213            _marker: PhantomData,
214        }
215    }
216
217    /// Create a new indirect buffer initialized with commands.
218    ///
219    /// # Arguments
220    ///
221    /// * `context` - The graphics context
222    /// * `label` - Optional debug label
223    /// * `commands` - Initial commands to write to the buffer
224    ///
225    /// # Panics
226    ///
227    /// Panics if `INDIRECT_FIRST_INSTANCE` feature is not enabled on the context.
228    pub fn new_init(context: &GraphicsContext, label: Option<&str>, commands: &[T]) -> Self {
229        context.require_feature(GpuFeatures::INDIRECT_FIRST_INSTANCE);
230
231        let buffer = context.device().create_buffer(&wgpu::BufferDescriptor {
232            label,
233            size: T::SIZE * commands.len() as u64,
234            usage: wgpu::BufferUsages::INDIRECT
235                | wgpu::BufferUsages::COPY_DST
236                | wgpu::BufferUsages::STORAGE,
237            mapped_at_creation: false,
238        });
239
240        context
241            .queue()
242            .write_buffer(&buffer, 0, bytemuck::cast_slice(commands));
243
244        Self {
245            buffer,
246            capacity: commands.len(),
247            _marker: PhantomData,
248        }
249    }
250
251    /// Get the underlying wgpu buffer.
252    pub fn buffer(&self) -> &wgpu::Buffer {
253        &self.buffer
254    }
255
256    /// Get the capacity (maximum number of commands).
257    pub fn capacity(&self) -> usize {
258        self.capacity
259    }
260
261    /// Get the size of the buffer in bytes.
262    pub fn size_bytes(&self) -> u64 {
263        T::SIZE * self.capacity as u64
264    }
265
266    /// Get the byte offset of a command at the given index.
267    pub fn offset_of(&self, index: usize) -> u64 {
268        T::SIZE * index as u64
269    }
270
271    /// Write commands to the buffer starting at the given index.
272    ///
273    /// # Arguments
274    ///
275    /// * `queue` - The command queue to use for the write
276    /// * `start_index` - Index of the first command to write
277    /// * `commands` - Commands to write
278    ///
279    /// # Panics
280    ///
281    /// Panics if the write would exceed the buffer capacity.
282    pub fn write_at(&self, queue: &wgpu::Queue, start_index: usize, commands: &[T]) {
283        assert!(
284            start_index + commands.len() <= self.capacity,
285            "Indirect buffer write would exceed capacity: {} + {} > {}",
286            start_index,
287            commands.len(),
288            self.capacity
289        );
290
291        let offset = T::SIZE * start_index as u64;
292        queue.write_buffer(&self.buffer, offset, bytemuck::cast_slice(commands));
293    }
294
295    /// Write commands to the buffer starting at index 0.
296    ///
297    /// # Arguments
298    ///
299    /// * `queue` - The command queue to use for the write
300    /// * `commands` - Commands to write
301    ///
302    /// # Panics
303    ///
304    /// Panics if the write would exceed the buffer capacity.
305    pub fn write(&self, queue: &wgpu::Queue, commands: &[T]) {
306        self.write_at(queue, 0, commands);
307    }
308
309    /// Clear the buffer by writing zeros.
310    pub fn clear(&self, queue: &wgpu::Queue) {
311        let zeros = vec![T::default(); self.capacity];
312        queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&zeros));
313    }
314}
315
316/// Extension trait for render passes to use indirect buffers.
317pub trait RenderPassIndirectExt<'a> {
318    /// Draw non-indexed geometry using an indirect buffer.
319    ///
320    /// # Arguments
321    ///
322    /// * `indirect_buffer` - Buffer containing draw commands
323    /// * `index` - Index of the command to execute
324    fn draw_indirect_at(&mut self, indirect_buffer: &'a IndirectBuffer<DrawIndirect>, index: usize);
325
326    /// Draw 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_indexed_indirect_at(
333        &mut self,
334        indirect_buffer: &'a IndirectBuffer<DrawIndexedIndirect>,
335        index: usize,
336    );
337}
338
339impl<'a> RenderPassIndirectExt<'a> for wgpu::RenderPass<'a> {
340    fn draw_indirect_at(
341        &mut self,
342        indirect_buffer: &'a IndirectBuffer<DrawIndirect>,
343        index: usize,
344    ) {
345        let offset = indirect_buffer.offset_of(index);
346        self.draw_indirect(indirect_buffer.buffer(), offset);
347    }
348
349    fn draw_indexed_indirect_at(
350        &mut self,
351        indirect_buffer: &'a IndirectBuffer<DrawIndexedIndirect>,
352        index: usize,
353    ) {
354        let offset = indirect_buffer.offset_of(index);
355        self.draw_indexed_indirect(indirect_buffer.buffer(), offset);
356    }
357}
358
359/// Extension trait for multi-draw indirect operations.
360///
361/// Requires `DownlevelFlags::INDIRECT_EXECUTION` (available on all desktop GPUs).
362pub trait RenderPassMultiDrawIndirectExt<'a> {
363    /// Draw non-indexed geometry multiple times using an indirect buffer.
364    ///
365    /// # Arguments
366    ///
367    /// * `indirect_buffer` - Buffer containing draw commands
368    /// * `start_index` - Index of the first command to execute
369    /// * `count` - Number of commands to execute
370    ///
371    /// # Panics
372    ///
373    /// Requires `DownlevelFlags::INDIRECT_EXECUTION`.
374    fn multi_draw_indirect(
375        &mut self,
376        indirect_buffer: &'a IndirectBuffer<DrawIndirect>,
377        start_index: usize,
378        count: u32,
379    );
380
381    /// Draw indexed geometry multiple times using an indirect buffer.
382    ///
383    /// # Arguments
384    ///
385    /// * `indirect_buffer` - Buffer containing draw commands
386    /// * `start_index` - Index of the first command to execute
387    /// * `count` - Number of commands to execute
388    ///
389    /// # Panics
390    ///
391    /// Requires `DownlevelFlags::INDIRECT_EXECUTION`.
392    fn multi_draw_indexed_indirect(
393        &mut self,
394        indirect_buffer: &'a IndirectBuffer<DrawIndexedIndirect>,
395        start_index: usize,
396        count: u32,
397    );
398}
399
400impl<'a> RenderPassMultiDrawIndirectExt<'a> for wgpu::RenderPass<'a> {
401    fn multi_draw_indirect(
402        &mut self,
403        indirect_buffer: &'a IndirectBuffer<DrawIndirect>,
404        start_index: usize,
405        count: u32,
406    ) {
407        let offset = indirect_buffer.offset_of(start_index);
408        self.multi_draw_indirect(indirect_buffer.buffer(), offset, count);
409    }
410
411    fn multi_draw_indexed_indirect(
412        &mut self,
413        indirect_buffer: &'a IndirectBuffer<DrawIndexedIndirect>,
414        start_index: usize,
415        count: u32,
416    ) {
417        let offset = indirect_buffer.offset_of(start_index);
418        self.multi_draw_indexed_indirect(indirect_buffer.buffer(), offset, count);
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn test_draw_indirect_size() {
428        // Verify the struct matches wgpu's expected layout
429        assert_eq!(DrawIndirect::size(), 16); // 4 u32s = 16 bytes
430        assert_eq!(DrawIndirect::SIZE, 16);
431    }
432
433    #[test]
434    fn test_draw_indexed_indirect_size() {
435        // Verify the struct matches wgpu's expected layout
436        assert_eq!(DrawIndexedIndirect::size(), 20); // 4 u32s + 1 i32 = 20 bytes
437        assert_eq!(DrawIndexedIndirect::SIZE, 20);
438    }
439
440    #[test]
441    fn test_draw_indirect_single() {
442        let cmd = DrawIndirect::single(36);
443        assert_eq!(cmd.vertex_count, 36);
444        assert_eq!(cmd.instance_count, 1);
445        assert_eq!(cmd.first_vertex, 0);
446        assert_eq!(cmd.first_instance, 0);
447    }
448
449    #[test]
450    fn test_draw_indexed_indirect_instanced() {
451        let cmd = DrawIndexedIndirect::instanced(36, 100);
452        assert_eq!(cmd.index_count, 36);
453        assert_eq!(cmd.instance_count, 100);
454        assert_eq!(cmd.first_index, 0);
455        assert_eq!(cmd.base_vertex, 0);
456        assert_eq!(cmd.first_instance, 0);
457    }
458}