Files
wsg/lib/tests/wgsl_validate.rs
T
Jérôme Bousquié 49aa9e48fd feat(particles): Étape 28 A — ParticlePool infrastructure (no driver)
Creates the GPU resource layer for particles per ARCHI §3.2/§6, without
simulation: the pool owns all buffers/pipeline/bind group but draws nothing
(indirect args zeroed → no-op) until a driver is attached (Étape B).

New:
- resources/particle.rs: Particle (80 B, #[repr(C)], no padding — D19),
  SIZE/ZERO consts + offset/layout unit tests
- shaders/particle_billboard.wgsl: camera-facing billboard, empty vertex
  layout (quad via vertex_index), instance slot via storage binding
  compact_index (D17), uv_rect atlas support (D15/D18)
- core/particles.rs: ParticlePoolConfig, BlendingMode, ParticleDriver trait
  (D3), ParticlePool (4 buffers + pipeline + bind group), default disc
  texture (D11), unit tests

Wired:
- Scene: SceneGpu keeps queue/sample_count; particle_pools registry +
  create_particle_pool() (default disc when no texture given)
- utils::conf PARTICLE_BILLBOARD_SHADER, module re-exports, prelude
- tests/wgsl_validate.rs: particle billboard naga validation (2 entry points)

Docs: DRAFT call-site/tree synced with the final code; AGENTS.md test count
(138) + wgpu 30 API drift gotcha (contents/DeviceExt/ALPHA_BLENDING,
DepthStencilState no Default, NonZero min_binding_size, const Zeroable).

cargo test -p wsg-lib: 138 pass (121 lib + 10 wgsl + 7), 0 warnings.
2026-09-26 12:34:21 +02:00

260 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # Validation WGSL (naga)
//!
//! The `standard_shader.wgsl` shader is not yet loaded by a `RenderPipeline` (see Steps 3–5):
//! this offline validation via `wgpu::naga` is therefore the **only** guarantee of its validity until
//! it is wired in. It protects against future regressions (shader re-edits, layout changes)
//! without requiring a GPU context.
//!
//! No new dependency is introduced: `wgpu` re-exports `naga`, already a `wsg-lib` dependency.
use wgpu::naga;
/// Parses and fully validates the embedded `standard_shader.wgsl` shader via naga.
/// A failure here means the shader would be rejected by `Device::create_shader_module` at Step 3.
#[test]
fn standard_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/standard_shader.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("standard_shader.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("standard_shader.wgsl: validation failed: {e:?}"));
// Contract: at least vs_main + fs_main (+ fs_pbr since Étape 27).
assert!(module.entry_points.len() >= 3, "vs_main + fs_main + fs_pbr expected");
}
/// Parses and fully validates the embedded `shadow_shader.wgsl` shader (Step 14, D4) via naga.
/// The "shadow" pipeline is wired directly by `build_shadow_pipeline` (bypassing the
/// PipelineCache), so this offline validation is the guarantee of its validity. The contract
/// expects a single entry point (`vs_main` — pipeline with no fragment stage).
#[test]
fn shadow_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/shadow_shader.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("shadow_shader.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("shadow_shader.wgsl: validation failed: {e:?}"));
let entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
assert_eq!(entry_names, vec!["vs_main"], "only vs_main expected");
}
/// Parses and fully validates the embedded `gpu_driven.wgsl` compute shader (Phase 3, Step 15)
/// via naga. The renderer compiles it directly into two `ComputePipeline`s (one per entry point),
/// so this offline validation is the guarantee of its validity. The contract expects exactly the
/// two compute entry points: `compute_matrices` and `cull`.
#[test]
fn gpu_driven_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/gpu_driven.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("gpu_driven.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("gpu_driven.wgsl: validation failed: {e:?}"));
let mut entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
entry_names.sort();
assert_eq!(
entry_names,
vec!["compute_matrices", "cull"],
"the two compute entry points are expected"
);
}
/// Parses and fully validates the embedded `particle_billboard.wgsl` shader (Étape 28 A) via naga.
/// The particle pool compiles it into one `RenderPipeline` (empty vertex layout — quad via
/// `@builtin(vertex_index)`, instance slot via storage binding, D17/D19), so this offline
/// validation is the guarantee of its validity. The contract expects two entry points:
/// `vs_main` + `fs_main` (no compute in this step).
#[test]
fn particle_billboard_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/particle_billboard.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("particle_billboard.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("particle_billboard.wgsl: validation failed: {e:?}"));
let entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
assert_eq!(
entry_names,
vec!["vs_main", "fs_main"],
"the two entry points (vs + fs) are expected"
);
}
/// Parses and fully validates the embedded `tonemap.wgsl` shader (Étape 20) via naga.
/// The renderer compiles it into one `RenderPipeline` (vertex `vs_main` + one of the two
/// fragment entry points `fs_aces` / `fs_reinhard`), so this offline validation is the
/// guarantee of its validity. The contract expects three entry points.
#[test]
fn tonemap_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/tonemap.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("tonemap.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("tonemap.wgsl: validation failed: {e:?}"));
let mut entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
entry_names.sort();
assert_eq!(
entry_names,
vec!["fs_aces", "fs_reinhard", "vs_main"],
"the three entry points are expected"
);
}
/// Parses and fully validates the `bloom_threshold.wgsl` shader (Étape 23) via naga.
#[test]
fn bloom_threshold_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/bloom_threshold.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("bloom_threshold.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("bloom_threshold.wgsl: validation failed: {e:?}"));
let mut entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
entry_names.sort();
assert_eq!(entry_names, vec!["fs_main", "vs_main"]);
}
/// Parses and fully validates the `bloom_blur.wgsl` shader (Étape 23) via naga.
#[test]
fn bloom_blur_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/bloom_blur.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("bloom_blur.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("bloom_blur.wgsl: validation failed: {e:?}"));
let mut entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
entry_names.sort();
assert_eq!(entry_names, vec!["fs_main", "vs_main"]);
}
/// Parses and fully validates the `bloom_composite.wgsl` shader (Étape 23) via naga.
#[test]
fn bloom_composite_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/bloom_composite.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("bloom_composite.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("bloom_composite.wgsl: validation failed: {e:?}"));
let mut entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
entry_names.sort();
assert_eq!(entry_names, vec!["fs_main", "vs_main"]);
}
/// Parses and fully validates the `dof_coc.wgsl` shader (Étape 26) via naga.
#[test]
fn dof_coc_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/dof_coc.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("dof_coc.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("dof_coc.wgsl: validation failed: {e:?}"));
let mut entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
entry_names.sort();
assert_eq!(entry_names, vec!["fs_main", "vs_main"]);
}
/// Parses and fully validates the `dof_blur.wgsl` shader (Étape 26) via naga.
#[test]
fn dof_blur_shader_is_valid_wgsl() {
let src = include_str!("../src/shaders/dof_blur.wgsl");
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("dof_blur.wgsl: parsing error: {e:?}"));
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.unwrap_or_else(|e| panic!("dof_blur.wgsl: validation failed: {e:?}"));
let mut entry_names: Vec<&str> = module
.entry_points
.iter()
.map(|ep| ep.name.as_str())
.collect();
entry_names.sort();
assert_eq!(entry_names, vec!["fs_main", "vs_main"]);
}