#![cfg(test)]
use awsm_materials::MaterialShaderId;
use crate::dynamic_materials::{BucketEntry, ShadingBase};
fn first_party_bases() -> [(MaterialShaderId, ShadingBase, &'static str); 4] {
[
(MaterialShaderId::PBR, ShadingBase::Pbr, "pbr"),
(MaterialShaderId::UNLIT, ShadingBase::Unlit, "unlit"),
(MaterialShaderId::TOON, ShadingBase::Toon, "toon"),
(
MaterialShaderId::FLIPBOOK,
ShadingBase::Flipbook,
"flipbook",
),
]
}
fn single_bucket(shader_id: MaterialShaderId, base: ShadingBase, name: &str) -> Vec<BucketEntry> {
vec![BucketEntry {
shader_id,
base,
pbr_features: 0,
name: name.to_string(),
}]
}
fn assert_get_material_calls_defined(src: &str, label: &str) {
const NEEDLE: &str = "_get_material(";
for line in src.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with("fn ") || trimmed.starts_with("//") {
continue;
}
let mut rest = line;
while let Some(pos) = rest.find(NEEDLE) {
let head = &rest[..pos];
let ident_start = head
.rfind(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.map(|i| i + 1)
.unwrap_or(0);
let ident = &head[ident_start..];
if !ident.is_empty() {
let def = format!("fn {ident}_get_material");
assert!(
src.contains(&def),
"{label}: WGSL CALLS `{ident}_get_material(` but never defines `{def}` \
— the filtered materials_wgsl is missing this base's fragment entry point \
(would fail pipeline compile at first render / editor boot)",
);
}
rest = &rest[pos + NEEDLE.len()..];
}
}
}
#[test]
fn scanner_catches_missing_definition_and_accepts_present_one() {
assert_get_material_calls_defined(
"fn flipbook_get_material(o: u32) -> X { }\nlet m = flipbook_get_material(off);",
"self-test/good",
);
let missing = std::panic::catch_unwind(|| {
assert_get_material_calls_defined(
"let m = flipbook_get_material(off);", "self-test/bad",
)
});
assert!(
missing.is_err(),
"the completeness scanner FAILED to flag a called-but-undefined loader \
— the guard is inert",
);
}
#[test]
fn opaque_compute_defines_every_called_loader_per_base() {
use crate::render_passes::material_opaque::shader::cache_key::ShaderCacheKeyMaterialOpaque;
use crate::render_passes::material_opaque::shader::template::ShaderTemplateMaterialOpaque;
for (shader_id, base, name) in first_party_bases() {
let key = ShaderCacheKeyMaterialOpaque {
texture_pool_arrays_len: 1,
texture_pool_samplers_len: 1,
msaa_sample_count: Some(4),
mipmaps: true,
max_shadow_casters: 4,
shader_id,
base,
owns_skybox: false,
pbr_features: 0,
dispatch_hash: 0,
dynamic_shader: None,
bucket_entries: single_bucket(shader_id, base, name),
};
let src = ShaderTemplateMaterialOpaque::try_from(&key)
.unwrap_or_else(|e| panic!("opaque/{name}: template build failed: {e:?}"))
.into_source()
.unwrap_or_else(|e| panic!("opaque/{name}: render failed: {e:?}"));
assert_get_material_calls_defined(&src, &format!("opaque-compute/{name}"));
}
}
#[test]
fn transparent_defines_every_called_loader_per_base() {
use crate::render_passes::light_culling::buffers::DEFAULT_SLICE_COUNT;
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;
for (shader_id, base, name) in first_party_bases() {
let _ = shader_id; let key = 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: None,
mipmaps: true,
base,
pbr_features: 0,
dispatch_hash: 0,
dynamic_shader_id: None,
dynamic_shader: None,
froxel_slice_count: DEFAULT_SLICE_COUNT,
};
let src = ShaderTemplateMaterialTransparent::try_from(&key)
.unwrap_or_else(|e| panic!("transparent/{name}: template build failed: {e:?}"))
.into_source()
.unwrap_or_else(|e| panic!("transparent/{name}: render failed: {e:?}"));
assert_get_material_calls_defined(&src, &format!("transparent/{name}"));
}
}