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}