#![cfg(test)]
use awsm_materials::MaterialShaderId;
use crate::dynamic_materials::{BucketEntry, ShadingBase};
use crate::render_passes::material_decal::shader::cache_key::ShaderCacheKeyMaterialDecal;
use crate::render_passes::material_decal::shader::template::ShaderTemplateMaterialDecal;
use crate::render_passes::material_opaque::shader::cache_key::{
DynamicShaderInfo, ShaderCacheKeyMaterialOpaque,
};
use crate::render_passes::material_opaque::shader::template::ShaderTemplateMaterialOpaque;
use crate::render_passes::material_transparent::shader::cache_key::ShaderCacheKeyMaterialTransparent;
use crate::render_passes::material_transparent::shader::template::ShaderTemplateMaterialTransparent;
use crate::render_passes::shared::material::cache_key::ShaderMaterialVertexAttributes;
fn naga_validate(src: &str, label: &str) {
let module = match naga::front::wgsl::parse_str(src) {
Ok(m) => m,
Err(e) => panic!(
"{label}: naga WGSL PARSE failed:\n{}",
e.emit_to_string(src)
),
};
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
if let Err(e) = validator.validate(&module) {
panic!(
"{label}: naga WGSL VALIDATION failed:\n{}",
e.emit_to_string(src)
);
}
}
fn first_party_key(
shader_id: MaterialShaderId,
base: ShadingBase,
owns_skybox: bool,
msaa: Option<u32>,
mipmaps: bool,
) -> ShaderCacheKeyMaterialOpaque {
first_party_key_prep(shader_id, base, owns_skybox, msaa, mipmaps)
}
fn first_party_key_prep(
shader_id: MaterialShaderId,
base: ShadingBase,
owns_skybox: bool,
msaa: Option<u32>,
mipmaps: bool,
) -> ShaderCacheKeyMaterialOpaque {
ShaderCacheKeyMaterialOpaque {
texture_pool_arrays_len: 1,
texture_pool_samplers_len: 1,
msaa_sample_count: msaa,
mipmaps,
max_shadow_casters: 4,
shader_id,
base,
owns_skybox,
pbr_features: awsm_materials::pbr::PbrFeatures::default().bits(),
dispatch_hash: 0,
dynamic_shader: None,
bucket_entries: crate::dynamic_materials::first_party_bucket_entries(),
}
}
fn custom_key(
includes: awsm_materials::ShaderIncludes,
msaa: Option<u32>,
mipmaps: bool,
) -> ShaderCacheKeyMaterialOpaque {
let dyn_id = MaterialShaderId::from_dynamic_raw(MaterialShaderId::DYNAMIC_START);
let mut bucket_entries = crate::dynamic_materials::first_party_bucket_entries();
bucket_entries.push(BucketEntry {
shader_id: dyn_id,
base: ShadingBase::Custom,
pbr_features: awsm_materials::pbr::PbrFeatures::default().bits(),
name: "noise".to_string(),
});
ShaderCacheKeyMaterialOpaque {
texture_pool_arrays_len: 1,
texture_pool_samplers_len: 1,
msaa_sample_count: msaa,
mipmaps,
max_shadow_casters: 4,
shader_id: dyn_id,
base: ShadingBase::Custom,
owns_skybox: false,
pbr_features: awsm_materials::pbr::PbrFeatures::default().bits(),
dispatch_hash: 1,
dynamic_shader: Some(DynamicShaderInfo {
shader_includes: includes,
struct_decl: "struct MaterialData { _pad: u32, };".to_string(),
loader_decl:
"fn material_data_load(byte_offset: u32) -> MaterialData { return MaterialData(0u); }"
.to_string(),
wgsl_fragment: "return OpaqueShadingOutput(input.world_normal * 0.5 + 0.5, 1.0);"
.to_string(),
}),
bucket_entries,
}
}
fn render(key: &ShaderCacheKeyMaterialOpaque, label: &str) -> String {
ShaderTemplateMaterialOpaque::try_from(key)
.unwrap_or_else(|e| panic!("{label}: template build failed: {e:?}"))
.into_source()
.unwrap_or_else(|e| panic!("{label}: render failed: {e:?}"))
}
const CONFIGS: [(Option<u32>, bool); 3] = [(None, true), (None, false), (Some(4), true)];
#[test]
fn first_party_opaque_shaders_validate() {
let bases = [
(MaterialShaderId::PBR, ShadingBase::Pbr, false, "pbr"),
(MaterialShaderId::UNLIT, ShadingBase::Unlit, false, "unlit"),
(MaterialShaderId::TOON, ShadingBase::Toon, false, "toon"),
(
MaterialShaderId::FLIPBOOK,
ShadingBase::Flipbook,
false,
"flipbook",
),
(MaterialShaderId::SKYBOX, ShadingBase::Pbr, true, "skybox"),
];
for (id, base, owns_skybox, name) in bases {
for (msaa, mips) in CONFIGS {
let label = format!("opaque/{name} msaa={msaa:?} mips={mips}");
let src = render(&first_party_key(id, base, owns_skybox, msaa, mips), &label);
naga_validate(&src, &label);
if msaa.is_some() {
assert!(
src.contains("fn cs_shade(") && !src.contains("fn cs_opaque("),
"{label}: MSAA opaque module must define `fn cs_shade` and NOT `fn cs_opaque`"
);
} else {
assert!(
src.contains("fn cs_opaque(") && !src.contains("fn cs_shade("),
"{label}: non-MSAA opaque module must define `fn cs_opaque` and NOT `fn cs_shade`"
);
}
}
}
}
#[test]
fn unified_shade_opaque_shaders_validate() {
let bases = [
(MaterialShaderId::PBR, ShadingBase::Pbr, false, "pbr"),
(MaterialShaderId::UNLIT, ShadingBase::Unlit, false, "unlit"),
(MaterialShaderId::TOON, ShadingBase::Toon, false, "toon"),
(
MaterialShaderId::FLIPBOOK,
ShadingBase::Flipbook,
false,
"flipbook",
),
(MaterialShaderId::SKYBOX, ShadingBase::Pbr, true, "skybox"),
];
for (id, base, owns_skybox, name) in bases {
for mips in [false, true] {
let key = first_party_key_prep(id, base, owns_skybox, Some(4), mips);
let label = format!("opaque-unified/{name} msaa=4 mips={mips}");
let src = render(&key, &label);
naga_validate(&src, &label);
assert!(
src.contains("fn cs_shade("),
"{label}: unified opaque module missing `fn cs_shade` entry point \
(dispatch requests it → pipeline-create would fail on GPU)"
);
assert!(
!src.contains("fn cs_opaque("),
"{label}: MSAA module must NOT carry `fn cs_opaque` (cross-AA code)"
);
assert!(
src.contains("var edge_id_tex: texture_storage_2d<r32uint, read>"),
"{label}: unified module missing the read-only `edge_id_tex` binding"
);
}
}
for mips in [false, true] {
let key = custom_key(awsm_materials::ShaderIncludes::all(), Some(4), mips);
let label = format!("opaque-unified/custom msaa=4 mips={mips}");
let src = render(&key, &label);
naga_validate(&src, &label);
assert!(
src.contains("fn cs_shade("),
"{label}: unified Custom module missing `fn cs_shade`"
);
}
}
#[test]
fn custom_material_ibl_include_validates() {
use awsm_materials::ShaderIncludes;
for (msaa, mips) in CONFIGS {
let mut key = custom_key(ShaderIncludes::IBL, msaa, mips);
key.dynamic_shader.as_mut().unwrap().wgsl_fragment =
"let ibl = sample_ibl(vec3<f32>(0.8, 0.8, 0.8), input.world_normal, \
input.surface_to_camera, 0.3, 0.0); return OpaqueShadingOutput(ibl, 1.0);"
.to_string();
let label = format!("opaque/custom ibl msaa={msaa:?} mips={mips}");
let src = render(&key, &label);
naga_validate(&src, &label);
assert!(
src.contains("fn sample_ibl("),
"{label}: `ibl` include did not emit sample_ibl"
);
}
}
#[test]
fn custom_material_normal_map_include_validates() {
use awsm_materials::ShaderIncludes;
for (msaa, mips) in CONFIGS {
let mut key = custom_key(ShaderIncludes::NORMAL_MAP, msaa, mips);
key.dynamic_shader.as_mut().unwrap().wgsl_fragment =
"let n = apply_normal_map(input, vec3<f32>(0.6, 0.5, 0.9)); \
let _tbn = material_tbn(input); return OpaqueShadingOutput(n * 0.5 + 0.5, 1.0);"
.to_string();
let label = format!("opaque/custom normal_map msaa={msaa:?} mips={mips}");
let src = render(&key, &label);
naga_validate(&src, &label);
assert!(
src.contains("fn apply_normal_map("),
"{label}: `normal_map` include did not emit apply_normal_map"
);
}
}
#[test]
fn opaque_prep_read_variant_validates() {
let key = first_party_key_prep(
MaterialShaderId::PBR,
ShadingBase::Pbr,
false,
None, false, );
let src = render(&key, "opaque/pbr prep_read");
naga_validate(&src, "opaque/pbr prep_read");
assert!(
src.contains("fn cs_opaque("),
"prep_read opaque module missing `fn cs_opaque`"
);
assert!(
src.contains("textureLoad(prep_uv,"),
"prep_read opaque module should `textureLoad(prep_uv, ...)` in texture_uv()"
);
assert!(
src.contains("textureLoad(prep_vcolor,"),
"prep_read opaque module should `textureLoad(prep_vcolor, ...)` in vertex_color()"
);
assert!(
!src.contains("fn _texture_uv_per_vertex("),
"prep_read opaque module should drop the `_texture_uv_per_vertex` recompute helper"
);
assert!(
!src.contains("fn _vertex_color_per_vertex("),
"prep_read opaque module should drop the `_vertex_color_per_vertex` recompute helper"
);
}
#[test]
fn opaque_shadow_from_buffer_variant_validates() {
let prep_key = first_party_key_prep(
MaterialShaderId::PBR,
ShadingBase::Pbr,
false,
None, true,
);
let src = render(&prep_key, "opaque/pbr shadow_from_buffer");
naga_validate(&src, "opaque/pbr shadow_from_buffer");
assert!(
src.contains("fn cs_opaque("),
"shadow_from_buffer opaque module missing `fn cs_opaque`"
);
assert!(
src.contains("textureLoad(prep_shadow_visibility"),
"shadow_from_buffer opaque module should `textureLoad(prep_shadow_visibility, ...)`"
);
assert!(
src.contains("var prep_shadow_visibility: texture_2d_array<f32>")
|| src.contains("prep_shadow_visibility: texture_2d_array<f32>"),
"shadow_from_buffer opaque module should declare `prep_shadow_visibility` (binding 26)"
);
assert!(
!src.contains("fn sample_shadow_directional"),
"shadow_from_buffer opaque module should DROP `fn sample_shadow_directional` (the inline sampler)"
);
let msaa_key = first_party_key_prep(
MaterialShaderId::PBR,
ShadingBase::Pbr,
false,
Some(4), true,
);
let msaa_src = render(&msaa_key, "opaque/pbr prep-on msaa4");
naga_validate(&msaa_src, "opaque/pbr prep-on msaa4");
assert!(
!msaa_src.contains("fn sample_shadow_directional"),
"MSAA+prep PBR opaque must DROP inline `fn sample_shadow_directional` (5b: cs_edge reads the compact edge-shadow buffer)"
);
assert!(
msaa_src.contains("textureLoad(prep_shadow_visibility"),
"MSAA+prep PBR opaque cs_opaque (PRIMARY) must READ the full-screen prep shadow buffer"
);
assert!(
msaa_src.contains("textureLoad(prep_edge_shadow"),
"MSAA+prep PBR opaque cs_edge (EDGE) must READ the compact per-edge-sample shadow buffer"
);
assert!(
msaa_src.contains("var prep_edge_shadow: texture_2d_array<f32>")
|| msaa_src.contains("prep_edge_shadow: texture_2d_array<f32>"),
"MSAA+prep PBR opaque must declare `prep_edge_shadow` (binding 27)"
);
assert!(
msaa_src.contains("g_prep_ctx.mode == PREP_MODE_EDGE"),
"MSAA+prep PBR opaque must branch the shared shadow read on the EDGE mode"
);
assert!(
msaa_src.contains("textureLoad(prep_uv,") && msaa_src.contains("textureLoad(prep_vcolor,"),
"MSAA+prep PBR opaque cs_opaque (PRIMARY) must read the prep UV/vcolor arrays"
);
assert!(
msaa_src.contains("fn _texture_uv_per_vertex(")
&& msaa_src.contains("fn _vertex_color_per_vertex("),
"MSAA+prep PBR opaque must KEEP the recompute helpers (cs_edge recomputes attrs; 5b-attrs deferred)"
);
let no_msaa_src = render(&prep_key, "opaque/pbr prep-on no-msaa");
assert!(
!no_msaa_src.contains("textureLoad(prep_edge_shadow"),
"no-MSAA PBR opaque must NOT read the compact edge buffer (no edges)"
);
eprintln!(
"[stage4] PBR opaque no-MSAA — prep-read(shadow_from_buffer): {} B",
src.len(),
);
eprintln!(
"[stage5b] PBR opaque MSAA4 — prep-on(edge-shadow buffer): {} B",
msaa_src.len(),
);
}
fn render_classify(msaa: Option<u32>, emit_edge_data: bool, label: &str) -> String {
use crate::render_passes::material_classify::shader::cache_key::ShaderCacheKeyMaterialClassify;
use crate::render_passes::material_classify::shader::template::ShaderTemplateMaterialClassify;
ShaderTemplateMaterialClassify::try_from(&ShaderCacheKeyMaterialClassify {
msaa_sample_count: msaa,
bucket_count: crate::dynamic_materials::first_party_bucket_entries().len() as u32,
emit_edge_data,
})
.unwrap_or_else(|e| panic!("{label}: template build failed: {e:?}"))
.into_source()
.unwrap_or_else(|e| panic!("{label}: render failed: {e:?}"))
}
#[test]
fn material_classify_shader_validates() {
for (msaa, emit) in [(None, false), (Some(4u32), true)] {
let label = format!("classify msaa={msaa:?} emit={emit}");
let src = render_classify(msaa, emit, &label);
naga_validate(&src, &label);
assert!(
src.contains("fn cs_main("),
"{label}: classify module missing `fn cs_main` entry point"
);
}
let on = render_classify(Some(4), true, "classify on");
assert!(
on.contains("var edge_id_tex: texture_storage_2d<r32uint, write>"),
"MSAA classify must declare `edge_id_tex` storage texture (binding 11)"
);
assert!(
on.contains("textureStore(edge_id_tex,"),
"MSAA classify must write `edge_id_tex`"
);
assert!(
on.contains("ubucket1"),
"MSAA classify must build the ANY-sample tile_mask (4-sample OR)"
);
assert!(
!on.contains("fn append_edge_sample("),
"U2b-3: append_edge_sample (edge-sample lists) must be REMOVED"
);
assert!(
on.contains("edge_slot_map_base"),
"cs_shade still needs the slot_map pack — edge_slot_map_base must remain"
);
}
#[test]
fn material_prep_shader_validates() {
use crate::render_passes::material_prep::shader::cache_key::ShaderCacheKeyMaterialPrep;
use crate::render_passes::material_prep::shader::template::ShaderTemplateMaterialPrep;
for msaa in [None, Some(4u32)] {
let label = format!("material_prep msaa={msaa:?}");
let src = ShaderTemplateMaterialPrep::try_from(&ShaderCacheKeyMaterialPrep {
msaa_sample_count: msaa,
max_shadow_casters: 4,
})
.unwrap_or_else(|e| panic!("{label}: template build failed: {e:?}"))
.into_source()
.unwrap_or_else(|e| panic!("{label}: render failed: {e:?}"));
naga_validate(&src, &label);
assert!(
src.contains("fn cs_prep("),
"{label}: prep module missing `fn cs_prep` entry point"
);
assert!(
src.contains("fn compute_shadow_visibility_packed("),
"{label}: prep module missing shared `compute_shadow_visibility_packed` helper"
);
if msaa.is_some() {
assert!(
src.contains("fn cs_prep_edge("),
"{label}: MSAA prep module missing `fn cs_prep_edge` entry point"
);
assert!(
src.contains("textureStore(edge_shadow_out"),
"{label}: cs_prep_edge must write the compact edge-shadow texture"
);
} else {
assert!(
!src.contains("fn cs_prep_edge("),
"{label}: no-MSAA prep module must NOT emit `cs_prep_edge` (no edges)"
);
}
}
}
#[test]
fn custom_opaque_shaders_validate() {
use awsm_materials::ShaderIncludes as S;
let tier_b = S::BRDF
.union(S::APPLY_LIGHTING)
.union(S::MATERIAL_COLOR_CALC);
for inc in [S::empty(), S::all(), tier_b] {
for (msaa, mips) in CONFIGS {
let label = format!(
"opaque/custom inc={:?} msaa={msaa:?} mips={mips}",
inc.bits()
);
let src = render(&custom_key(inc, msaa, mips), &label);
naga_validate(&src, &label);
}
}
}
#[test]
fn custom_froxel_lights_accessors_validate() {
use awsm_materials::ShaderIncludes as S;
let dyn_id = MaterialShaderId::from_dynamic_raw(MaterialShaderId::DYNAMIC_START);
let mut bucket_entries = crate::dynamic_materials::first_party_bucket_entries();
bucket_entries.push(BucketEntry {
shader_id: dyn_id,
base: ShadingBase::Custom,
pbr_features: awsm_materials::pbr::PbrFeatures::default().bits(),
name: "froxel_lit".to_string(),
});
let fragment = "var c = vec3<f32>(0.0);\n\
let n = material_pixel_light_count(input);\n\
for (var i = 0u; i < n; i = i + 1u) {\n\
let l = material_pixel_light(input, i);\n\
let s = light_sample(l, input.world_normal, input.world_position);\n\
c = c + s.radiance * s.n_dot_l;\n\
}\n\
return OpaqueShadingOutput(c, 1.0);";
for (msaa, mips) in CONFIGS {
let key = ShaderCacheKeyMaterialOpaque {
texture_pool_arrays_len: 1,
texture_pool_samplers_len: 1,
msaa_sample_count: msaa,
mipmaps: mips,
max_shadow_casters: 4,
shader_id: dyn_id,
base: ShadingBase::Custom,
owns_skybox: false,
pbr_features: awsm_materials::pbr::PbrFeatures::default().bits(),
dispatch_hash: 1,
dynamic_shader: Some(DynamicShaderInfo {
shader_includes: S::LIGHT_ACCESS,
struct_decl: "struct MaterialData { _pad: u32, };".to_string(),
loader_decl:
"fn material_data_load(byte_offset: u32) -> MaterialData { return MaterialData(0u); }"
.to_string(),
wgsl_fragment: fragment.to_string(),
}),
bucket_entries: bucket_entries.clone(),
};
let label = format!("opaque/custom-froxel-lit msaa={msaa:?} mips={mips}");
let src = render(&key, &label);
naga_validate(&src, &label);
assert!(
src.contains("fn froxel_base_for_pixel("),
"{label}: froxel_walk SSOT not pulled into the custom LIGHT_ACCESS kernel"
);
assert!(
src.contains("fn material_pixel_light("),
"{label}: custom froxel-light accessor missing"
);
}
}
fn transparent_first_party_key(
base: ShadingBase,
msaa: Option<u32>,
) -> ShaderCacheKeyMaterialTransparent {
ShaderCacheKeyMaterialTransparent {
instancing_transforms: false,
attributes: ShaderMaterialVertexAttributes {
normals: true,
tangents: true,
color_sets: None,
uv_sets: Some(1),
},
texture_pool_arrays_len: 1,
texture_pool_samplers_len: 1,
msaa_sample_count: msaa,
mipmaps: true,
base,
pbr_features: awsm_materials::pbr::PbrFeatures::default().bits(),
dispatch_hash: 0,
dynamic_shader_id: None,
dynamic_shader: None,
froxel_slice_count: crate::render_passes::light_culling::buffers::DEFAULT_SLICE_COUNT,
}
}
#[test]
fn first_party_transparent_shaders_validate() {
for (base, name) in [
(ShadingBase::Pbr, "pbr"),
(ShadingBase::Unlit, "unlit"),
(ShadingBase::Toon, "toon"),
(ShadingBase::Flipbook, "flipbook"),
] {
for msaa in [None, Some(4)] {
let label = format!("transparent/{name} msaa={msaa:?}");
let key = transparent_first_party_key(base, msaa);
let src = ShaderTemplateMaterialTransparent::try_from(&key)
.unwrap_or_else(|e| panic!("{label}: template build failed: {e:?}"))
.into_source()
.unwrap_or_else(|e| panic!("{label}: render failed: {e:?}"));
naga_validate(&src, &label);
}
}
}
#[test]
fn custom_transparent_shaders_validate() {
use awsm_materials::ShaderIncludes as S;
let dyn_id = MaterialShaderId::from_dynamic_raw(MaterialShaderId::DYNAMIC_START);
let tier_b = S::BRDF
.union(S::APPLY_LIGHTING)
.union(S::MATERIAL_COLOR_CALC);
for inc in [S::empty(), S::all(), tier_b] {
for msaa in [None, Some(4)] {
let mut key = transparent_first_party_key(ShadingBase::Custom, msaa);
key.dispatch_hash = 1;
key.dynamic_shader_id = Some(dyn_id);
key.dynamic_shader = Some(DynamicShaderInfo {
shader_includes: inc,
struct_decl: "struct MaterialData { _pad: u32, };".to_string(),
loader_decl:
"fn material_data_load(byte_offset: u32) -> MaterialData { return MaterialData(0u); }"
.to_string(),
wgsl_fragment:
"return TransparentShadingOutput(vec4<f32>(input.world_normal * 0.5 + 0.5, 0.5));"
.to_string(),
});
let label = format!("transparent/custom inc={:?} msaa={msaa:?}", inc.bits());
let src = ShaderTemplateMaterialTransparent::try_from(&key)
.unwrap_or_else(|e| panic!("{label}: template build failed: {e:?}"))
.into_source()
.unwrap_or_else(|e| panic!("{label}: render failed: {e:?}"));
naga_validate(&src, &label);
}
}
}
#[test]
fn decal_shader_validates_with_templated_layer_stride() {
for msaa in [None, Some(4)] {
for stride in [256u32, 2048u32] {
let key = ShaderCacheKeyMaterialDecal {
msaa_sample_count: msaa,
texture_pool_arrays_len: 1,
texture_pool_samplers_len: 1,
texture_pool_layers_per_array: stride,
};
let label = format!("decal msaa={msaa:?} stride={stride}");
let src = ShaderTemplateMaterialDecal::try_from(&key)
.unwrap_or_else(|e| panic!("{label}: template build failed: {e:?}"))
.into_source()
.unwrap_or_else(|e| panic!("{label}: render failed: {e:?}"));
assert!(
src.contains(&format!("% {stride}u")) && src.contains(&format!("/ {stride}u")),
"{label}: expected the templated stride in the unpacking math"
);
naga_validate(&src, &label);
}
}
}