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}