Skip to main content

oxigdal_gpu/
push_constants.rs

1//! WGSL push constants (immediates) helper for `oxigdal-gpu`.
2//!
3//! In wgpu 29 the Vulkan "push constants" concept is exposed as **immediate
4//! data** (`var<immediate>` in WGSL, [`wgpu::Features::IMMEDIATES`] on the
5//! device, and [`wgpu::PipelineLayoutDescriptor::immediate_size`]).  This
6//! module provides ergonomic Rust wrappers around that API and remains
7//! forward-compatible with the naming used in earlier wgpu versions.
8//!
9//! # Quick start
10//!
11//! ```rust,no_run
12//! use oxigdal_gpu::push_constants::{
13//!     PushConstantsLayout, PushConstantsBuffer, make_push_constants_shader_source,
14//!     build_push_constants_pipeline,
15//! };
16//! use oxigdal_gpu::GpuContext;
17//!
18//! # async fn example() -> oxigdal_gpu::GpuResult<()> {
19//! let ctx = GpuContext::new().await?;
20//!
21//! let layout = PushConstantsLayout::compute_only(16);
22//! let mut buf = PushConstantsBuffer::new(layout.clone());
23//! buf.write_u32(0, 42)?;
24//! buf.write_f32(4, 3.14)?;
25//!
26//! let struct_wgsl = "struct PushConstantsBlock { value: u32, scale: f32, _pad0: u32, _pad1: u32, }";
27//! let body_wgsl = "let v = pc.value;";
28//! let src = make_push_constants_shader_source(struct_wgsl, body_wgsl);
29//! # Ok(())
30//! # }
31//! ```
32//!
33//! # Compatibility note
34//!
35//! wgpu 29 renamed Vulkan push constants to "immediates".  The public API of
36//! this module deliberately preserves the `PushConstants*` naming to keep the
37//! call-site interface stable regardless of the underlying wgpu version.
38
39use bytemuck::NoUninit;
40use std::mem::size_of;
41use std::sync::Arc;
42use tracing::debug;
43
44use crate::context::GpuContext;
45use crate::error::{GpuError, GpuResult};
46
47// ─────────────────────────────────────────────────────────────────────────────
48// Constants
49// ─────────────────────────────────────────────────────────────────────────────
50
51/// Minimum guaranteed push-constants (immediates) size per the Vulkan spec
52/// (128 bytes).  Adapters may expose a larger limit via
53/// [`max_push_constants_size`].
54pub const MAX_PUSH_CONSTANTS_SIZE_BYTES: u32 = 128;
55
56/// Required alignment for the start and end of each push-constant range.
57///
58/// Both the `start` and `end` fields of [`PushConstantRange`] must be
59/// multiples of this value.  This matches the Vulkan push-constant and wgpu
60/// immediate-data alignment requirement.
61pub const PUSH_CONSTANTS_ALIGNMENT: u32 = 4;
62
63// ─────────────────────────────────────────────────────────────────────────────
64// PushConstantRange
65// ─────────────────────────────────────────────────────────────────────────────
66
67/// Describes one logical push-constant range within a pipeline layout.
68///
69/// In wgpu 29 the underlying API does not support per-stage or per-range
70/// configurations — the immediate-data block covers the whole shader and is
71/// addressed by byte offset.  This struct deliberately mirrors the pre-wgpu-29
72/// interface so that callers can model their data layout without change.
73///
74/// # Layout constraints
75///
76/// - `start < end`
77/// - `end − start ≤ `[`MAX_PUSH_CONSTANTS_SIZE_BYTES`]
78/// - Both `start` and `end` must be 4-byte aligned.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct PushConstantRange {
81    /// Shader stages this range is visible to.  Stored for documentation
82    /// purposes; in wgpu 29 stage visibility is controlled at the bind-group
83    /// layout level, not the pipeline layout level.
84    pub stages: wgpu::ShaderStages,
85    /// Byte offset of the first byte in this range (inclusive).
86    pub start: u32,
87    /// Byte offset one past the last byte in this range (exclusive).
88    pub end: u32,
89}
90
91impl PushConstantRange {
92    /// Create a compute-only range that starts at offset 0 with the given
93    /// `size` in bytes.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use oxigdal_gpu::push_constants::PushConstantRange;
99    ///
100    /// let r = PushConstantRange::compute(32);
101    /// assert_eq!(r.start, 0);
102    /// assert_eq!(r.end, 32);
103    /// assert!(r.validate().is_ok());
104    /// ```
105    pub fn compute(size: u32) -> Self {
106        Self {
107            stages: wgpu::ShaderStages::COMPUTE,
108            start: 0,
109            end: size,
110        }
111    }
112
113    /// Validate that this range satisfies all layout constraints.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`GpuError::InvalidKernelParams`] when:
118    /// - `end <= start` (zero or negative size).
119    /// - The range size exceeds [`MAX_PUSH_CONSTANTS_SIZE_BYTES`].
120    /// - `start` or `end` is not 4-byte aligned.
121    pub fn validate(&self) -> GpuResult<()> {
122        if self.end <= self.start {
123            return Err(GpuError::invalid_kernel_params(format!(
124                "push-constant range start ({}) must be less than end ({})",
125                self.start, self.end
126            )));
127        }
128
129        let size = self.end - self.start;
130        if size > MAX_PUSH_CONSTANTS_SIZE_BYTES {
131            return Err(GpuError::invalid_kernel_params(format!(
132                "push-constant range size {} bytes exceeds the maximum of {} bytes",
133                size, MAX_PUSH_CONSTANTS_SIZE_BYTES
134            )));
135        }
136
137        if self.start % PUSH_CONSTANTS_ALIGNMENT != 0 {
138            return Err(GpuError::invalid_kernel_params(format!(
139                "push-constant range start {} is not {}-byte aligned",
140                self.start, PUSH_CONSTANTS_ALIGNMENT
141            )));
142        }
143
144        if self.end % PUSH_CONSTANTS_ALIGNMENT != 0 {
145            return Err(GpuError::invalid_kernel_params(format!(
146                "push-constant range end {} is not {}-byte aligned",
147                self.end, PUSH_CONSTANTS_ALIGNMENT
148            )));
149        }
150
151        Ok(())
152    }
153
154    /// Return the size of this range in bytes.
155    #[inline]
156    pub fn size(&self) -> u32 {
157        self.end.saturating_sub(self.start)
158    }
159}
160
161// ─────────────────────────────────────────────────────────────────────────────
162// PushConstantsLayout
163// ─────────────────────────────────────────────────────────────────────────────
164
165/// Describes all push-constant data ranges used by a pipeline.
166///
167/// In wgpu 29 only the total byte size matters at pipeline-creation time (set
168/// via [`wgpu::PipelineLayoutDescriptor::immediate_size`]).  The per-range
169/// breakdown is kept here for documentation and validation purposes.
170///
171/// # Invariants
172///
173/// All ranges must individually satisfy [`PushConstantRange::validate`], and
174/// `total_size` must equal `end` of the last range (or the extent of all
175/// non-overlapping ranges combined).
176#[derive(Debug, Clone)]
177pub struct PushConstantsLayout {
178    /// Individual data ranges (may overlap in theory, though callers should
179    /// keep them distinct for clarity).
180    pub ranges: Vec<PushConstantRange>,
181    /// Total number of bytes reserved in the immediate-data block.  Must
182    /// be 4-byte aligned and ≤ [`MAX_PUSH_CONSTANTS_SIZE_BYTES`].
183    pub total_size: u32,
184}
185
186impl PushConstantsLayout {
187    /// Create a layout with a single compute-only range covering `[0, size)`.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use oxigdal_gpu::push_constants::PushConstantsLayout;
193    ///
194    /// let l = PushConstantsLayout::compute_only(64);
195    /// assert_eq!(l.total_size, 64);
196    /// assert_eq!(l.ranges.len(), 1);
197    /// assert!(l.validate().is_ok());
198    /// ```
199    pub fn compute_only(size: u32) -> Self {
200        Self {
201            ranges: vec![PushConstantRange::compute(size)],
202            total_size: size,
203        }
204    }
205
206    /// Validate all contained ranges.
207    ///
208    /// # Errors
209    ///
210    /// Propagates the first validation error from any constituent
211    /// [`PushConstantRange::validate`] call.  Also returns an error when
212    /// `total_size` exceeds [`MAX_PUSH_CONSTANTS_SIZE_BYTES`] or is not
213    /// 4-byte aligned.
214    pub fn validate(&self) -> GpuResult<()> {
215        if self.total_size % PUSH_CONSTANTS_ALIGNMENT != 0 {
216            return Err(GpuError::invalid_kernel_params(format!(
217                "push-constants total_size {} is not {}-byte aligned",
218                self.total_size, PUSH_CONSTANTS_ALIGNMENT
219            )));
220        }
221
222        if self.total_size > MAX_PUSH_CONSTANTS_SIZE_BYTES {
223            return Err(GpuError::invalid_kernel_params(format!(
224                "push-constants total_size {} bytes exceeds maximum of {} bytes",
225                self.total_size, MAX_PUSH_CONSTANTS_SIZE_BYTES
226            )));
227        }
228
229        for range in &self.ranges {
230            range.validate()?;
231        }
232
233        Ok(())
234    }
235
236    /// Return the `immediate_size` value to pass to
237    /// [`wgpu::PipelineLayoutDescriptor`].
238    ///
239    /// This is simply `self.total_size`.
240    #[inline]
241    pub fn immediate_size_for_wgpu(&self) -> u32 {
242        self.total_size
243    }
244
245    /// Convert all ranges to a vector of descriptive strings (for logging).
246    pub fn to_wgpu_ranges(&self) -> Vec<PushConstantRangeDesc> {
247        self.ranges
248            .iter()
249            .map(|r| PushConstantRangeDesc {
250                stages: r.stages,
251                start: r.start,
252                end: r.end,
253            })
254            .collect()
255    }
256}
257
258/// Descriptive wrapper returned by [`PushConstantsLayout::to_wgpu_ranges`].
259///
260/// In wgpu 29 there is no `wgpu::PushConstantRange` type; this struct mirrors
261/// the pre-wgpu-29 shape for documentation purposes and to satisfy tests that
262/// inspect range fields.
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct PushConstantRangeDesc {
265    /// Shader stages associated with this range (informational).
266    pub stages: wgpu::ShaderStages,
267    /// Start byte offset (inclusive).
268    pub start: u32,
269    /// End byte offset (exclusive).
270    pub end: u32,
271}
272
273impl PushConstantRangeDesc {
274    /// Return the byte range as a `std::ops::Range<u32>`.
275    pub fn range(&self) -> std::ops::Range<u32> {
276        self.start..self.end
277    }
278}
279
280// ─────────────────────────────────────────────────────────────────────────────
281// PushConstantsBuffer
282// ─────────────────────────────────────────────────────────────────────────────
283
284/// A typed byte buffer holding push-constant (immediate) data.
285///
286/// Callers write scalar and vector values into the buffer via the typed helper
287/// methods (`write_u32`, `write_f32`, etc.) and then upload the raw bytes to
288/// the GPU via [`wgpu::ComputePass::set_immediates`].
289///
290/// # Layout
291///
292/// The buffer is zero-initialised at construction.  Writes use
293/// **little-endian** byte order (matching wgpu's expectation on all supported
294/// platforms).
295#[derive(Debug, Clone)]
296pub struct PushConstantsBuffer {
297    /// Raw byte storage.
298    pub data: Vec<u8>,
299    /// Layout that governs this buffer's structure.
300    pub layout: PushConstantsLayout,
301}
302
303impl PushConstantsBuffer {
304    /// Allocate a new zero-initialised buffer sized to `layout.total_size`.
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// use oxigdal_gpu::push_constants::{PushConstantsBuffer, PushConstantsLayout};
310    ///
311    /// let layout = PushConstantsLayout::compute_only(16);
312    /// let buf = PushConstantsBuffer::new(layout);
313    /// assert_eq!(buf.size(), 16);
314    /// assert!(buf.as_bytes().iter().all(|&b| b == 0));
315    /// ```
316    pub fn new(layout: PushConstantsLayout) -> Self {
317        let size = layout.total_size as usize;
318        Self {
319            data: vec![0u8; size],
320            layout,
321        }
322    }
323
324    /// Write any [`NoUninit`] + [`Copy`] type at `offset` bytes into the
325    /// buffer.
326    ///
327    /// The write is bounds-checked: an error is returned if
328    /// `offset + size_of::<T>()` would exceed the buffer size.
329    ///
330    /// # Errors
331    ///
332    /// Returns [`GpuError::InvalidKernelParams`] on bounds overflow.
333    pub fn write<T: NoUninit + Copy>(&mut self, offset: u32, value: T) -> GpuResult<()> {
334        let type_size = size_of::<T>();
335        let start = offset as usize;
336        let end = start.checked_add(type_size).ok_or_else(|| {
337            GpuError::invalid_kernel_params(format!(
338                "push-constants write: offset {} + type size {} overflows usize",
339                offset, type_size
340            ))
341        })?;
342
343        if end > self.data.len() {
344            return Err(GpuError::invalid_kernel_params(format!(
345                "push-constants write: offset {} + {} bytes = {} exceeds buffer size {}",
346                offset,
347                type_size,
348                end,
349                self.data.len()
350            )));
351        }
352
353        let bytes = bytemuck::bytes_of(&value);
354        self.data[start..end].copy_from_slice(bytes);
355        Ok(())
356    }
357
358    /// Write a `u32` at `offset` bytes (little-endian).
359    ///
360    /// # Errors
361    ///
362    /// Returns an error when the write would exceed the buffer bounds.
363    #[inline]
364    pub fn write_u32(&mut self, offset: u32, value: u32) -> GpuResult<()> {
365        self.write(offset, value)
366    }
367
368    /// Write an `i32` at `offset` bytes (little-endian).
369    ///
370    /// # Errors
371    ///
372    /// Returns an error when the write would exceed the buffer bounds.
373    #[inline]
374    pub fn write_i32(&mut self, offset: u32, value: i32) -> GpuResult<()> {
375        self.write(offset, value)
376    }
377
378    /// Write an `f32` at `offset` bytes (little-endian, IEEE 754).
379    ///
380    /// # Errors
381    ///
382    /// Returns an error when the write would exceed the buffer bounds.
383    #[inline]
384    pub fn write_f32(&mut self, offset: u32, value: f32) -> GpuResult<()> {
385        self.write(offset, value)
386    }
387
388    /// Write a `[f32; 4]` vec4 starting at `offset` bytes.
389    ///
390    /// This writes 16 bytes: `[x, y, z, w]` in little-endian order.
391    ///
392    /// # Errors
393    ///
394    /// Returns an error when the write would exceed the buffer bounds.
395    #[inline]
396    pub fn write_vec4_f32(&mut self, offset: u32, value: [f32; 4]) -> GpuResult<()> {
397        self.write(offset, value)
398    }
399
400    /// Write a `[u32; 4]` uvec4 starting at `offset` bytes (16 bytes total).
401    ///
402    /// # Errors
403    ///
404    /// Returns an error when the write would exceed the buffer bounds.
405    #[inline]
406    pub fn write_uvec4(&mut self, offset: u32, value: [u32; 4]) -> GpuResult<()> {
407        self.write(offset, value)
408    }
409
410    /// Write a `[f32; 2]` vec2 starting at `offset` bytes (8 bytes total).
411    ///
412    /// # Errors
413    ///
414    /// Returns an error when the write would exceed the buffer bounds.
415    #[inline]
416    pub fn write_vec2_f32(&mut self, offset: u32, value: [f32; 2]) -> GpuResult<()> {
417        self.write(offset, value)
418    }
419
420    /// Return a view of the raw byte content.
421    #[inline]
422    pub fn as_bytes(&self) -> &[u8] {
423        &self.data
424    }
425
426    /// Return the size of the buffer in bytes.
427    #[inline]
428    pub fn size(&self) -> u32 {
429        self.data.len() as u32
430    }
431
432    /// Reset all bytes to zero.
433    pub fn clear(&mut self) {
434        self.data.iter_mut().for_each(|b| *b = 0);
435    }
436}
437
438// ─────────────────────────────────────────────────────────────────────────────
439// Feature query helpers
440// ─────────────────────────────────────────────────────────────────────────────
441
442/// Return `true` if the GPU context supports immediate data (push constants).
443///
444/// This checks for [`wgpu::Features::IMMEDIATES`].  On platforms where this
445/// feature is unavailable, callers should fall back to uniform buffers.
446///
447/// # Examples
448///
449/// ```rust,no_run
450/// use oxigdal_gpu::{GpuContext, push_constants::supports_push_constants};
451///
452/// # async fn ex() -> oxigdal_gpu::GpuResult<()> {
453/// let ctx = GpuContext::new().await?;
454/// println!("push constants supported: {}", supports_push_constants(&ctx));
455/// # Ok(())
456/// # }
457/// ```
458pub fn supports_push_constants(ctx: &GpuContext) -> bool {
459    ctx.device().features().contains(wgpu::Features::IMMEDIATES)
460}
461
462/// Return the maximum immediate-data (push-constants) size in bytes supported
463/// by the current adapter's device.
464///
465/// Returns `0` when [`wgpu::Features::IMMEDIATES`] is not enabled on the
466/// device (the limit is reported as 0 in that case by wgpu).
467///
468/// # Examples
469///
470/// ```rust,no_run
471/// use oxigdal_gpu::{GpuContext, push_constants::max_push_constants_size};
472///
473/// # async fn ex() -> oxigdal_gpu::GpuResult<()> {
474/// let ctx = GpuContext::new().await?;
475/// println!("max push constants: {} bytes", max_push_constants_size(&ctx));
476/// # Ok(())
477/// # }
478/// ```
479pub fn max_push_constants_size(ctx: &GpuContext) -> u32 {
480    ctx.device().limits().max_immediate_size
481}
482
483// ─────────────────────────────────────────────────────────────────────────────
484// Shader source generation
485// ─────────────────────────────────────────────────────────────────────────────
486
487/// Generate a WGSL compute shader source that uses a `var<immediate>` block.
488///
489/// The generated shader:
490/// - Embeds `struct_def_wgsl` verbatim (must define `PushConstantsBlock`).
491/// - Declares `var<immediate> pc: PushConstantsBlock;` (wgpu 29 naming).
492/// - Wraps `body_wgsl` in a `@compute @workgroup_size(16, 16, 1)` entry point.
493///
494/// # Arguments
495///
496/// - `struct_def_wgsl` — a complete WGSL struct definition, **must** be named
497///   `PushConstantsBlock` so that the `var<immediate>` declaration refers to
498///   the correct type.
499/// - `body_wgsl` — statements for the compute entry-point body; may reference
500///   `pc` to access push-constant data.
501///
502/// # Example
503///
504/// ```
505/// use oxigdal_gpu::push_constants::make_push_constants_shader_source;
506///
507/// let src = make_push_constants_shader_source(
508///     "struct PushConstantsBlock { width: u32, height: u32 }",
509///     "let w = pc.width;",
510/// );
511/// assert!(src.contains("var<immediate>"));
512/// assert!(src.contains("@compute"));
513/// ```
514pub fn make_push_constants_shader_source(struct_def_wgsl: &str, body_wgsl: &str) -> String {
515    format!(
516        "{struct_def}\n\nvar<immediate> pc: PushConstantsBlock;\n\n\
517@compute @workgroup_size(16, 16, 1)\n\
518fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{\n\
519{body}\n\
520}}\n",
521        struct_def = struct_def_wgsl,
522        body = body_wgsl
523    )
524}
525
526// ─────────────────────────────────────────────────────────────────────────────
527// Pipeline construction
528// ─────────────────────────────────────────────────────────────────────────────
529
530/// Compile a compute pipeline that uses immediate data (push constants) with
531/// the given layout.
532///
533/// # Algorithm
534///
535/// 1. Validate `layout`.
536/// 2. Compile `wgsl` into a [`wgpu::ShaderModule`].
537/// 3. Create a [`wgpu::PipelineLayout`] with
538///    `immediate_size = layout.total_size` and the
539///    [`wgpu::Features::IMMEDIATES`] requirement already satisfied (callers
540///    must have requested the feature during context construction).
541/// 4. Create and return the [`wgpu::ComputePipeline`] wrapped in [`Arc`].
542///
543/// # Errors
544///
545/// - [`GpuError::InvalidKernelParams`] — layout validation failed.
546/// - [`GpuError::UnsupportedOperation`] — the device does not support the
547///   `IMMEDIATES` feature.
548/// - [`GpuError::ShaderCompilation`] — the WGSL source failed to compile.
549///
550/// # Examples
551///
552/// ```rust,no_run
553/// use oxigdal_gpu::{GpuContext, push_constants::{
554///     PushConstantsLayout, make_push_constants_shader_source,
555///     build_push_constants_pipeline,
556/// }};
557///
558/// # async fn ex() -> oxigdal_gpu::GpuResult<()> {
559/// let ctx = oxigdal_gpu::GpuContextConfig::new()
560///     .with_push_constants()
561///     .build().await?;
562/// let layout = PushConstantsLayout::compute_only(16);
563/// let src = make_push_constants_shader_source(
564///     "struct PushConstantsBlock { value: u32, _pad0: u32, _pad1: u32, _pad2: u32 }",
565///     "let v = pc.value;",
566/// );
567/// let pipeline = build_push_constants_pipeline(&ctx, &src, "main", &layout)?;
568/// # Ok(())
569/// # }
570/// ```
571pub fn build_push_constants_pipeline(
572    ctx: &GpuContext,
573    wgsl: &str,
574    entry: &str,
575    layout: &PushConstantsLayout,
576) -> GpuResult<Arc<wgpu::ComputePipeline>> {
577    layout.validate()?;
578
579    // Verify the IMMEDIATES feature is available on this device.
580    if !ctx.device().features().contains(wgpu::Features::IMMEDIATES) {
581        return Err(GpuError::unsupported_operation(
582            "wgpu::Features::IMMEDIATES is required for push-constants pipelines; \
583             create the GpuContext with GpuContextConfig::with_push_constants()",
584        ));
585    }
586
587    let device = ctx.device();
588
589    // Compile the shader module.
590    let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
591        label: Some("push_constants_shader"),
592        source: wgpu::ShaderSource::Wgsl(wgsl.into()),
593    });
594
595    // Build the pipeline layout, requesting immediate_size bytes.
596    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
597        label: Some("push_constants_layout"),
598        bind_group_layouts: &[],
599        immediate_size: layout.immediate_size_for_wgpu(),
600    });
601
602    // Compile the compute pipeline.
603    let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
604        label: Some("push_constants_pipeline"),
605        layout: Some(&pipeline_layout),
606        module: &shader_module,
607        entry_point: Some(entry),
608        compilation_options: wgpu::PipelineCompilationOptions::default(),
609        cache: None,
610    });
611
612    debug!(
613        "Built push-constants compute pipeline (entry={}, immediate_size={})",
614        entry,
615        layout.immediate_size_for_wgpu()
616    );
617
618    Ok(Arc::new(pipeline))
619}
620
621/// Upload immediate (push-constant) data and dispatch a compute pass.
622///
623/// This is a convenience function that:
624/// 1. Begins a compute pass on `encoder`.
625/// 2. Sets the pipeline.
626/// 3. Calls `set_immediates` with the buffer contents.
627/// 4. Dispatches `(workgroups_x, workgroups_y, workgroups_z)` workgroups.
628///
629/// The caller is responsible for submitting `encoder` to the queue.
630///
631/// # Errors
632///
633/// None — wgpu compute-pass encoding is infallible at this level.  Errors
634/// surface when the command buffer is submitted to the queue.
635pub fn dispatch_with_push_constants(
636    encoder: &mut wgpu::CommandEncoder,
637    pipeline: &wgpu::ComputePipeline,
638    buf: &PushConstantsBuffer,
639    workgroups_x: u32,
640    workgroups_y: u32,
641    workgroups_z: u32,
642) {
643    let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
644        label: Some("push_constants_dispatch"),
645        timestamp_writes: None,
646    });
647    pass.set_pipeline(pipeline);
648    pass.set_immediates(0, buf.as_bytes());
649    pass.dispatch_workgroups(workgroups_x, workgroups_y, workgroups_z);
650}
651
652// ─────────────────────────────────────────────────────────────────────────────
653// Tests
654// ─────────────────────────────────────────────────────────────────────────────
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    #[test]
661    fn test_push_constant_range_compute_size() {
662        let r = PushConstantRange::compute(32);
663        assert_eq!(r.start, 0);
664        assert_eq!(r.end, 32);
665        assert_eq!(r.size(), 32);
666        assert!(r.validate().is_ok());
667    }
668
669    #[test]
670    fn test_push_constant_range_validate_rejects_zero_size() {
671        let r = PushConstantRange {
672            stages: wgpu::ShaderStages::COMPUTE,
673            start: 0,
674            end: 0,
675        };
676        assert!(r.validate().is_err());
677    }
678
679    #[test]
680    fn test_push_constant_range_validate_rejects_over_limit() {
681        let r = PushConstantRange {
682            stages: wgpu::ShaderStages::COMPUTE,
683            start: 0,
684            end: MAX_PUSH_CONSTANTS_SIZE_BYTES + 4,
685        };
686        assert!(r.validate().is_err());
687    }
688
689    #[test]
690    fn test_push_constant_range_validate_rejects_unaligned_end() {
691        let r = PushConstantRange {
692            stages: wgpu::ShaderStages::COMPUTE,
693            start: 0,
694            end: 6, // not 4-byte aligned
695        };
696        assert!(r.validate().is_err());
697    }
698
699    #[test]
700    fn test_push_constants_layout_compute_only() {
701        let l = PushConstantsLayout::compute_only(64);
702        assert_eq!(l.total_size, 64);
703        assert_eq!(l.ranges.len(), 1);
704        assert!(l.validate().is_ok());
705    }
706
707    #[test]
708    fn test_push_constants_layout_to_wgpu_ranges() {
709        let l = PushConstantsLayout::compute_only(16);
710        let ranges = l.to_wgpu_ranges();
711        assert_eq!(ranges.len(), 1);
712        assert_eq!(ranges[0].range(), 0..16);
713    }
714
715    #[test]
716    fn test_push_constants_buffer_write_u32() {
717        let layout = PushConstantsLayout::compute_only(16);
718        let mut buf = PushConstantsBuffer::new(layout);
719        buf.write_u32(0, 42).expect("write_u32 failed");
720        let bytes = buf.as_bytes();
721        let val = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
722        assert_eq!(val, 42);
723    }
724
725    #[test]
726    fn test_push_constants_buffer_write_f32() {
727        let layout = PushConstantsLayout::compute_only(16);
728        let mut buf = PushConstantsBuffer::new(layout);
729        buf.write_f32(4, std::f32::consts::PI)
730            .expect("write_f32 failed");
731        let bytes = buf.as_bytes();
732        let val = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
733        assert!(
734            (val - std::f32::consts::PI).abs() < 1e-6,
735            "expected π, got {val}"
736        );
737    }
738
739    #[test]
740    fn test_push_constants_buffer_write_overflow_errors() {
741        let layout = PushConstantsLayout::compute_only(4);
742        let mut buf = PushConstantsBuffer::new(layout);
743        // Offset 4 + 4 bytes = 8 > 4: must fail.
744        let result = buf.write_u32(4, 1);
745        assert!(result.is_err(), "expected overflow error, got Ok");
746    }
747}