Skip to main content

oxigdal_gpu/
indirect_dispatch.rs

1//! Indirect compute dispatch support for GPU-driven workloads.
2//!
3//! Indirect dispatch allows the workgroup counts for a compute shader to be
4//! stored in a GPU-side buffer (written by a preceding GPU pass), eliminating
5//! the need for a CPU readback round-trip before issuing the next dispatch.
6//!
7//! # Key types
8//!
9//! - [`DispatchIndirectArgs`] — layout-stable, 12-byte struct that maps
10//!   directly onto the `VkDispatchIndirectCommand` / D3D12 / Metal equivalent.
11//! - [`IndirectDispatchBuffer`] — a GPU buffer holding one or more dispatch
12//!   argument slots, initialised from the CPU and updateable at any time.
13//!
14//! # Helper functions
15//!
16//! - [`workgroup_count_1d`] / [`workgroup_count_2d`] / [`workgroup_count_3d`]
17//!   — compute the ceiling-division workgroup counts for common dispatch
18//!   dimensions without requiring a GPU context.
19//! - [`args_for_elements`] — short-hand for building a 1-D dispatch args struct.
20//! - [`dispatch_indirect_on_pass`] — convenience wrapper that sets the pipeline,
21//!   bind groups, and issues the indirect dispatch in one call.
22
23use std::sync::Arc;
24use wgpu::{Buffer, BufferAddress, BufferDescriptor, BufferUsages, ComputePipeline};
25
26use crate::context::GpuContext;
27use crate::error::{GpuError, GpuResult};
28
29// ─────────────────────────────────────────────────────────────────────────────
30// DispatchIndirectArgs
31// ─────────────────────────────────────────────────────────────────────────────
32
33/// Layout-stable representation of workgroup dispatch dimensions stored in GPU
34/// memory.
35///
36/// The struct is `repr(C)` so that its in-memory byte layout is well-defined
37/// and can be written directly to a GPU buffer via
38/// [`wgpu::Queue::write_buffer`].  It corresponds to the 12-byte
39/// `VkDispatchIndirectCommand` structure (and its equivalents in D3D12 and
40/// Metal).
41///
42/// # Layout
43///
44/// | Bytes | Field | Meaning                         |
45/// |-------|-------|---------------------------------|
46/// | 0–3   | `x`   | Workgroup count along X axis    |
47/// | 4–7   | `y`   | Workgroup count along Y axis    |
48/// | 8–11  | `z`   | Workgroup count along Z axis    |
49///
50/// All values are stored in little-endian byte order when serialised via
51/// [`DispatchIndirectArgs::as_bytes`].
52#[repr(C)]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct DispatchIndirectArgs {
55    /// Workgroup count along the X dispatch dimension.
56    pub x: u32,
57    /// Workgroup count along the Y dispatch dimension.
58    pub y: u32,
59    /// Workgroup count along the Z dispatch dimension.
60    pub z: u32,
61}
62
63impl DispatchIndirectArgs {
64    /// Create new dispatch arguments with explicit x / y / z workgroup counts.
65    #[inline]
66    pub fn new(x: u32, y: u32, z: u32) -> Self {
67        Self { x, y, z }
68    }
69
70    /// Create 1-D dispatch arguments for `total` elements with a workgroup
71    /// size of `workgroup_size`.
72    ///
73    /// The Y and Z dimensions are set to 1. If `workgroup_size` is 0 the X
74    /// count is also 0.
75    #[inline]
76    pub fn for_1d(total: u32, workgroup_size: u32) -> Self {
77        Self {
78            x: workgroup_count_1d(total, workgroup_size),
79            y: 1,
80            z: 1,
81        }
82    }
83
84    /// Serialise the struct into a 12-byte little-endian array suitable for
85    /// writing to a GPU buffer.
86    #[inline]
87    pub fn as_bytes(&self) -> [u8; 12] {
88        let mut buf = [0u8; 12];
89        buf[0..4].copy_from_slice(&self.x.to_le_bytes());
90        buf[4..8].copy_from_slice(&self.y.to_le_bytes());
91        buf[8..12].copy_from_slice(&self.z.to_le_bytes());
92        buf
93    }
94}
95
96// ─────────────────────────────────────────────────────────────────────────────
97// IndirectDispatchBuffer
98// ─────────────────────────────────────────────────────────────────────────────
99
100/// A GPU buffer that holds one or more [`DispatchIndirectArgs`] slots for use
101/// with `wgpu::ComputePass::dispatch_workgroups_indirect`.
102///
103/// Each slot occupies exactly 12 bytes (the size of one
104/// [`DispatchIndirectArgs`]).  The buffer is created with
105/// `INDIRECT | COPY_DST | STORAGE` usages so it can both receive CPU-side
106/// writes and be written by a preceding compute shader pass.
107///
108/// # Multi-slot layout
109///
110/// ```text
111/// Offset 0       12     24     36 …
112///         ├──────┼──────┼──────┤
113///         │ [0]  │ [1]  │ [2]  │  …
114///         └──────┴──────┴──────┘
115///           12 B   12 B   12 B
116/// ```
117///
118/// The active slot for a dispatch is selected via [`IndirectDispatchBuffer::offset`]
119/// (defaults to slot 0) and can be overridden by calling
120/// [`IndirectDispatchBuffer::update_at`] to write to a specific slot and then
121/// constructing a view with the desired byte offset — or by creating separate
122/// buffers for each independently-driven dispatch.
123pub struct IndirectDispatchBuffer {
124    buffer: Arc<Buffer>,
125    /// Byte offset into `buffer` at which slot 0 begins.
126    ///
127    /// Always 0 for buffers created via the public constructors; exposed as a
128    /// field to allow future sub-buffer views.
129    offset: BufferAddress,
130    /// Total number of 12-byte dispatch slots in the buffer.
131    capacity: u32,
132}
133
134impl IndirectDispatchBuffer {
135    /// Create a single-slot indirect dispatch buffer initialised with `args`.
136    ///
137    /// The buffer is 12 bytes and supports `INDIRECT | COPY_DST | STORAGE`
138    /// usages.
139    ///
140    /// # Errors
141    ///
142    /// Returns [`GpuError::InvalidKernelParams`] if the device rejects the
143    /// buffer descriptor (should not happen in practice).
144    pub fn new(ctx: &GpuContext, args: DispatchIndirectArgs) -> GpuResult<Self> {
145        let buffer = ctx.device().create_buffer(&BufferDescriptor {
146            label: Some("indirect_dispatch"),
147            size: 12,
148            usage: BufferUsages::INDIRECT | BufferUsages::COPY_DST | BufferUsages::STORAGE,
149            mapped_at_creation: false,
150        });
151        let buf = Arc::new(buffer);
152        ctx.queue().write_buffer(&buf, 0, &args.as_bytes());
153        Ok(Self {
154            buffer: buf,
155            offset: 0,
156            capacity: 1,
157        })
158    }
159
160    /// Create a multi-slot indirect dispatch buffer with `max_dispatches`
161    /// slots, all initialised to zero.
162    ///
163    /// The total buffer size is `12 * max_dispatches` bytes.  Each slot can be
164    /// written individually via [`IndirectDispatchBuffer::update_at`].
165    ///
166    /// # Errors
167    ///
168    /// Returns [`GpuError::InvalidKernelParams`] if `max_dispatches` is 0.
169    pub fn new_with_capacity(ctx: &GpuContext, max_dispatches: u32) -> GpuResult<Self> {
170        if max_dispatches == 0 {
171            return Err(GpuError::InvalidKernelParams {
172                reason: "max_dispatches must be > 0".to_string(),
173            });
174        }
175        let size = 12u64 * max_dispatches as u64;
176        let buffer = ctx.device().create_buffer(&BufferDescriptor {
177            label: Some("indirect_dispatch_multi"),
178            size,
179            usage: BufferUsages::INDIRECT | BufferUsages::COPY_DST | BufferUsages::STORAGE,
180            mapped_at_creation: false,
181        });
182        Ok(Self {
183            buffer: Arc::new(buffer),
184            offset: 0,
185            capacity: max_dispatches,
186        })
187    }
188
189    /// Overwrite slot 0 with new dispatch arguments.
190    ///
191    /// This is a CPU-side write using `queue.write_buffer`; the data is
192    /// uploaded before the next `queue.submit`.
193    pub fn update(&self, ctx: &GpuContext, args: DispatchIndirectArgs) {
194        ctx.queue()
195            .write_buffer(&self.buffer, self.offset, &args.as_bytes());
196    }
197
198    /// Overwrite a specific slot (0-indexed) with new dispatch arguments.
199    ///
200    /// # Errors
201    ///
202    /// Returns [`GpuError::InvalidKernelParams`] if `slot_idx >= capacity`.
203    pub fn update_at(
204        &self,
205        ctx: &GpuContext,
206        slot_idx: u32,
207        args: DispatchIndirectArgs,
208    ) -> GpuResult<()> {
209        if slot_idx >= self.capacity {
210            return Err(GpuError::InvalidKernelParams {
211                reason: format!(
212                    "slot_idx {} is out of range for capacity {}",
213                    slot_idx, self.capacity
214                ),
215            });
216        }
217        let slot_offset = self.offset + 12 * slot_idx as u64;
218        ctx.queue()
219            .write_buffer(&self.buffer, slot_offset, &args.as_bytes());
220        Ok(())
221    }
222
223    /// Return a reference to the underlying [`wgpu::Buffer`].
224    #[inline]
225    pub fn buffer(&self) -> &Buffer {
226        &self.buffer
227    }
228
229    /// Return the byte offset of slot 0 within the buffer.
230    ///
231    /// This is the value passed as the second argument to
232    /// `ComputePass::dispatch_workgroups_indirect`.
233    #[inline]
234    pub fn offset(&self) -> BufferAddress {
235        self.offset
236    }
237
238    /// Return the number of 12-byte dispatch slots in the buffer.
239    #[inline]
240    pub fn capacity(&self) -> u32 {
241        self.capacity
242    }
243}
244
245// ─────────────────────────────────────────────────────────────────────────────
246// dispatch_indirect_on_pass
247// ─────────────────────────────────────────────────────────────────────────────
248
249/// Set the pipeline, bind groups, and issue an indirect dispatch in one call.
250///
251/// This is a thin convenience wrapper around the raw `wgpu::ComputePass` API:
252///
253/// ```text
254/// pass.set_pipeline(pipeline);
255/// for (i, bg) in bind_groups.iter().enumerate() {
256///     pass.set_bind_group(i as u32, *bg, &[]);
257/// }
258/// pass.dispatch_workgroups_indirect(indirect.buffer(), indirect.offset());
259/// ```
260///
261/// # Arguments
262///
263/// * `pass`        — an in-progress `ComputePass`.
264/// * `pipeline`    — the compute pipeline to bind.
265/// * `bind_groups` — slice of bind groups; index 0 → slot 0, etc.
266/// * `indirect`    — the buffer supplying the workgroup counts.
267///
268/// # Panics
269///
270/// Panics with a wgpu validation error (not a Rust panic) if the pipeline,
271/// bind groups, or indirect buffer are invalid for the current device.
272pub fn dispatch_indirect_on_pass<'a>(
273    pass: &mut wgpu::ComputePass<'a>,
274    pipeline: &'a ComputePipeline,
275    bind_groups: &[&'a wgpu::BindGroup],
276    indirect: &'a IndirectDispatchBuffer,
277) {
278    pass.set_pipeline(pipeline);
279    for (i, bg) in bind_groups.iter().enumerate() {
280        pass.set_bind_group(i as u32, *bg, &[]);
281    }
282    pass.dispatch_workgroups_indirect(indirect.buffer(), indirect.offset());
283}
284
285// ─────────────────────────────────────────────────────────────────────────────
286// Pure arithmetic helpers — no GPU context required
287// ─────────────────────────────────────────────────────────────────────────────
288
289/// Compute the ceiling-division workgroup count for a 1-D dispatch.
290///
291/// Returns `ceil(total_elements / workgroup_size)`.  If `workgroup_size` is 0
292/// the result is 0 (to avoid a division-by-zero panic).
293///
294/// # Examples
295///
296/// ```
297/// use oxigdal_gpu::workgroup_count_1d;
298/// assert_eq!(workgroup_count_1d(64, 64), 1);
299/// assert_eq!(workgroup_count_1d(65, 64), 2);
300/// assert_eq!(workgroup_count_1d(0,  64), 0);
301/// ```
302#[inline]
303pub fn workgroup_count_1d(total_elements: u32, workgroup_size: u32) -> u32 {
304    if workgroup_size == 0 {
305        return 0;
306    }
307    total_elements.saturating_add(workgroup_size - 1) / workgroup_size
308}
309
310/// Compute the ceiling-division workgroup counts for a 2-D dispatch.
311///
312/// Returns `(ceil(width / wg_x), ceil(height / wg_y))`.
313///
314/// # Examples
315///
316/// ```
317/// use oxigdal_gpu::workgroup_count_2d;
318/// assert_eq!(workgroup_count_2d(128, 64, 16, 8), (8, 8));
319/// ```
320#[inline]
321pub fn workgroup_count_2d(width: u32, height: u32, wg_x: u32, wg_y: u32) -> (u32, u32) {
322    (
323        workgroup_count_1d(width, wg_x),
324        workgroup_count_1d(height, wg_y),
325    )
326}
327
328/// Compute the ceiling-division workgroup counts for a 3-D dispatch.
329///
330/// Returns `(ceil(d0 / wg0), ceil(d1 / wg1), ceil(d2 / wg2))`.
331///
332/// # Examples
333///
334/// ```
335/// use oxigdal_gpu::workgroup_count_3d;
336/// assert_eq!(workgroup_count_3d((10, 10, 10), (4, 4, 4)), (3, 3, 3));
337/// ```
338#[inline]
339pub fn workgroup_count_3d(dims: (u32, u32, u32), wg_dims: (u32, u32, u32)) -> (u32, u32, u32) {
340    (
341        workgroup_count_1d(dims.0, wg_dims.0),
342        workgroup_count_1d(dims.1, wg_dims.1),
343        workgroup_count_1d(dims.2, wg_dims.2),
344    )
345}
346
347/// Build [`DispatchIndirectArgs`] for a 1-D dispatch over `total` elements
348/// with the given `workgroup_size`.
349///
350/// Y and Z are set to 1.
351///
352/// # Examples
353///
354/// ```
355/// use oxigdal_gpu::{args_for_elements, DispatchIndirectArgs};
356/// let args = args_for_elements(128, 64);
357/// assert_eq!(args, DispatchIndirectArgs { x: 2, y: 1, z: 1 });
358/// ```
359#[inline]
360pub fn args_for_elements(total: u32, workgroup_size: u32) -> DispatchIndirectArgs {
361    DispatchIndirectArgs {
362        x: workgroup_count_1d(total, workgroup_size),
363        y: 1,
364        z: 1,
365    }
366}