mod byte_offsets;
mod mirror;
mod mirrors;
mod programs;
use concinnity_core::platform::Platform;
use mirror::Case;
use programs::Program;
fn check(program: &Program, cases: &[Case]) {
concinnity_shader::require_dxc!();
let mut drift = Vec::new();
for target in Platform::ALL {
if !cases.iter().any(|case| case.targets.contains(&target)) {
continue;
}
let layouts = match programs::layouts(program, target) {
Ok(layouts) => layouts,
Err(e) => {
drift.push(e);
continue;
}
};
for case in cases.iter().filter(|c| c.targets.contains(&target)) {
let name = case.mirror.shader_name;
let Some(shader) = layouts.get(name) else {
drift.push(format!(
"{} ({}): the reflection of {} declares no `{name}`; the mirror names a \
struct this variant does not compile",
case.mirror.rust_name,
target.key(),
program.row.entry,
));
continue;
};
drift.extend(
mirror::drift(&case.mirror, shader)
.into_iter()
.map(|line| format!("[{}] {line}", target.key())),
);
}
}
assert!(
drift.is_empty(),
"{} layouts drifted from the shader:\n {}",
program.row.entry,
drift.join("\n "),
);
}
#[test]
fn no_shader_redeclares_the_object_record() {
const RECORD: &str = "struct GpuObjectData";
let mut declarations = 0usize;
for (name, source) in concinnity_core::render::shaders::SOURCES {
if source.contains(RECORD) {
assert_eq!(
*name, "object_common.hlsl",
"{name} declares its own GpuObjectData; splice the shared fragment instead"
);
declarations += 1;
}
}
assert_eq!(
declarations, 1,
"object_common.hlsl no longer declares the record"
);
let metal = concat!(env!("CARGO_MANIFEST_DIR"), "/src/metal/shaders");
for entry in std::fs::read_dir(metal).unwrap_or_else(|e| panic!("read {metal}: {e}")) {
let path = entry.expect("dir entry").path();
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
assert!(
!source.contains(RECORD),
"{}: a hand-written Metal shader declares GpuObjectData",
path.display()
);
}
}
#[test]
fn main_bindless_layouts_match_the_shader() {
check(
&programs::MAIN_BINDLESS_FRAG,
&mirrors::forward::main_bindless(),
);
}
#[test]
fn the_metal_main_pass_declares_no_discrete_texture_or_sampler() {
concinnity_shader::require_dxc!();
let msl = programs::msl(&programs::MAIN_BINDLESS_FRAG).unwrap_or_else(|e| panic!("{e}"));
let signature = msl
.lines()
.find(|line| line.contains(" fragment_main_bindless("))
.expect("the entry point's signature");
for discrete in ["[[texture(", "[[sampler("] {
assert!(
!signature.contains(discrete),
"main_bindless.hlsl binds a {discrete}..)]] slot: {signature}\nput it in the \
texture or sampler argument buffer, which the ICB draws can reach"
);
}
for set in ["[[buffer(7)]]", "[[buffer(10)]]"] {
assert!(
signature.contains(set),
"the Metal entry takes no argument buffer at {set}: {signature}"
);
}
}
#[test]
fn a_world_fragment_declares_the_whole_argument_buffers() {
concinnity_shader::require_dxc!();
let msl = |program| programs::msl(program).unwrap_or_else(|e| panic!("{e}"));
let engine_msl = msl(&programs::MAIN_BINDLESS_FRAG);
let world_msl = msl(&programs::MAIN_BINDLESS_FRAG_LATE_MEMBER_SHADE);
for member in [
"tex_pool",
"shadow_map",
"irradiance_cube",
"prefilter_cube",
"ssao_tex",
"probe_cubes",
"spot_shadow_map",
"ltc_matrix",
"ltc_magnitude",
"tex_sampler",
"shadow_sampler",
"cube_sampler",
] {
let expected = concinnity_shader::msl_argument_id(&engine_msl, member);
assert!(
expected.is_some(),
"the engine fragment's argument buffers do not declare `{member}`"
);
assert_eq!(
concinnity_shader::msl_argument_id(&world_msl, member),
expected,
"the world fragment's argument buffers declare `{member}` elsewhere than the engine's"
);
}
}
#[test]
fn a_world_hook_reads_material_params_where_metal_binds_them() {
concinnity_shader::require_dxc!();
let slot = "material_params_sb [[buffer(16)]]";
for program in [
&programs::MAIN_BINDLESS_FRAG_PARAMS_SHADE,
&programs::MAIN_BINDLESS_VERT_PARAMS_TRANSFORM,
] {
let msl = programs::msl(program).unwrap_or_else(|e| panic!("{e}"));
assert!(
msl.contains(slot),
"{}: the parameter table is not at buffer(16)",
program.row.entry
);
}
}
#[test]
fn material_params_layouts_match_the_shader() {
check(
&programs::MAIN_BINDLESS_FRAG_PARAMS_SHADE,
&mirrors::forward::material_params(),
);
}
#[test]
fn cull_layouts_match_the_shader() {
check(&programs::CULL_KERNEL, &mirrors::geometry::cull());
}
#[test]
fn light_cull_layouts_match_the_shader() {
check(
&programs::LIGHT_CULL_KERNEL,
&mirrors::forward::light_cull(),
);
}
#[test]
fn rt_skin_layouts_match_the_shader() {
check(&programs::RT_SKIN_KERNEL, &mirrors::geometry::rt_skin());
}
#[test]
fn gbuffer_prepass_vertex_layouts_match_the_shader() {
check(
&programs::MAIN_PREPASS_VERT,
&mirrors::geometry::gbuffer_vertex(),
);
}
#[test]
fn the_metal_prepass_reads_its_buffers_where_the_host_binds_them() {
use concinnity_core::render::shader_programs::metal::prepass_buffers;
concinnity_shader::require_dxc!();
let stages = [
(
&programs::MAIN_PREPASS_VERT,
&[
("gb_view", prepass_buffers::VIEW),
("prev_models", prepass_buffers::PREV_MODELS),
("draw_args", prepass_buffers::DRAW_ARGS),
][..],
),
(
&programs::MAIN_PREPASS_FRAG,
&[("gb_view", prepass_buffers::VIEW)][..],
),
];
for (program, buffers) in stages {
let msl = programs::msl(program).unwrap_or_else(|e| panic!("{e}"));
for &(name, slot) in buffers {
assert_eq!(
msl_buffer_slot(&msl, name),
Some(slot),
"{} {name}",
program.row.entry
);
}
}
}
fn msl_buffer_slot(msl: &str, name: &str) -> Option<usize> {
let tag = format!(" {name} [[buffer(");
let at = msl.find(&tag)? + tag.len();
msl[at..].split(')').next()?.parse().ok()
}
#[test]
fn msl_buffer_slots_are_read_off_the_parameter_list() {
let msl = "vertex main0_out main0(constant GbView& gb_view [[buffer(3)]], \
const device float4x4* prev_models [[buffer(17)]])";
assert_eq!(msl_buffer_slot(msl, "gb_view"), Some(3));
assert_eq!(msl_buffer_slot(msl, "prev_models"), Some(17));
assert_eq!(msl_buffer_slot(msl, "draw_args"), None);
}
#[test]
fn shadow_layouts_match_the_shader() {
check(&programs::SHADOW_VERT, &mirrors::geometry::shadow());
}
#[test]
fn decal_layouts_match_the_shader() {
check(&programs::DECAL_VERT, &mirrors::geometry::decal());
}
#[test]
fn line_layouts_match_the_shader() {
check(&programs::LINE_VERT, &mirrors::geometry::line());
}
#[test]
fn particle_layouts_match_the_shader() {
check(&programs::PARTICLE_VERT, &mirrors::geometry::particle());
}
#[test]
fn text_layouts_match_the_shader() {
check(&programs::TEXT_VERT, &mirrors::geometry::text());
}
#[test]
fn glass_layouts_match_the_shader() {
check(&programs::GLASS_VERT, &mirrors::transparent::glass());
}
#[test]
fn glass_mesh_layouts_match_the_shader() {
check(
&programs::GLASS_MESH_VERT,
&mirrors::transparent::glass_mesh(),
);
}
#[test]
fn water_layouts_match_the_shader() {
check(&programs::WATER_VERT, &mirrors::transparent::water());
}
#[test]
fn rt_reflections_layouts_match_the_shader() {
check(
&programs::RT_REFLECTIONS_FRAG,
&mirrors::transparent::rt_reflections(),
);
}
#[test]
fn fog_layouts_match_the_shader() {
check(&programs::FOG_FROXEL, &mirrors::transparent::fog());
}
#[test]
fn raymarch_layouts_match_the_shader() {
check(&programs::RAYMARCH_FRAG, &mirrors::raymarch::surface());
}
#[test]
fn raymarch_shadow_layouts_match_the_shader() {
check(
&programs::RAYMARCH_SHADOW_VERT,
&mirrors::raymarch::shadow(),
);
}
#[test]
fn taa_layouts_match_the_shader() {
check(&programs::TAA_FRAG, &mirrors::post::taa());
}
#[test]
fn bloom_layouts_match_the_shader() {
check(&programs::BLOOM_PREFILTER, &mirrors::post::bloom());
}
#[test]
fn composite_layouts_match_the_shader() {
check(&programs::COMPOSITE_FRAG, &mirrors::post::composite());
}
#[test]
fn ssao_layouts_match_the_shader() {
check(&programs::SSAO_KERNEL, &mirrors::post::ssao());
}
#[test]
fn ssr_layouts_match_the_shader() {
check(&programs::SSR_RESOLVE, &mirrors::post::ssr());
}
#[test]
fn ssgi_layouts_match_the_shader() {
check(&programs::SSGI_TRACE, &mirrors::post::ssgi());
}
#[test]
fn auto_exposure_layouts_match_the_shader() {
check(
&programs::AUTO_EXPOSURE_BUILD,
&mirrors::post::auto_exposure(),
);
}
#[test]
fn hiz_layouts_match_the_shader() {
check(&programs::HIZ_INIT_SINGLE, &mirrors::post::hiz());
}