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}