49aa9e48fd
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.
260 lines
9.8 KiB
Rust
260 lines
9.8 KiB
Rust
//! # 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"]);
|
||
}
|