concinnity-device 0.19.53

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
//! Layout drift guard for the `#[repr(C)]` structs the CPU uploads into the
//! single-source shaders. The expected offsets and sizes are not written down
//! here: they come from the compiler over the same source the renderer
//! compiles, per target, so an edit on either side of the boundary fails the
//! check. The SPIR-V module dxc emits states every offset in its own
//! decorations, so that module is what the check reads.
//!
//! That is the difference from a hand-written assert. A hand assert pins the
//! Rust struct against a number and a comment describing what the shader is
//! believed to do, and nothing checks the comment; a shader-side edit -- the
//! direction that actually broke things during the single-source migration --
//! slides straight past it.
//!
//! The reflection is taken per target because the three do not agree. MSL sizes
//! a `float3` at 16 bytes -- in a structured buffer as much as in a constant
//! buffer -- where SPIR-V and DXIL pack a scalar after it at 12, and SPIR-V
//! aligns a following `float2` to 16 where neither of the others does. The
//! engine's shader sources avoid both shapes on purpose (`float4` lanes
//! instead of `float3` + scalar), so today the three agree on every mirrored
//! struct -- which is itself worth asserting rather than assuming. The block
//! sizes already differ: DirectX reports a 276-byte `ShadowUniforms` block where
//! Metal and SPIR-V round it to 288.
//!
//! Not every layout assert can move here. The Metal ICB encode kernel is
//! hand-written, so its parameter block has no single source to reflect and its
//! hand assert is the only check it has. Vertex
//! payloads are the other exclusion: a vertex input binds by attribute index,
//! not byte offset, so `Vertex` / `SkinnedVertex` / `MorphEntry` /
//! `TextVertex` / `LineVertex` reflect no layout at all -- where a kernel
//! byte-addresses those payloads instead, `byte_offsets` locks its constants to
//! the mirrors, which reflection cannot do.
//!
//! World Shaders declare no layout of their own: they compile from the engine's
//! own main-pass files with their hooks spliced in, so these mirrors cover them.

mod byte_offsets;
mod mirror;
mod mirrors;
mod programs;

use concinnity_core::platform::Platform;
use mirror::Case;
use programs::Program;

// Reflect `program` on every target its mirrors name and compare each against
// what that target's layout rules produced. Skipped when dxc is absent.
//
// A target no mirror names is not compiled at all. That is how a program opts
// out of a target it cannot build on, which is otherwise indistinguishable from
// a layout failure.
fn check(program: &Program, cases: &[Case]) {
    if !concinnity_shader::dxc_available() {
        return;
    }
    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  "),
    );
}

// The object record has one declaration, `object_common.hlsl`, spliced into
// every pass that strides the per-frame object buffer. A shader that grows its
// own copy is exactly the drift the splice exists to prevent, on any backend,
// so the single-source set and the hand-written Metal directory are both
// scanned as text.
#[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(),
    );
}

// The Metal main pass reaches every texture and sampler through the argument
// buffers at buffer(7) and buffer(10), never through a discrete slot: an
// indirect command carries no texture binding, so a discrete one would be
// unreachable from the ICB-executed draws the pass is made of. The Metal
// encoder binds buffers only (`bind_main_pass_shared` in metal/draw/main.rs),
// so a texture or sampler added to the CN_BACKEND_METAL block would compile to a
// parameter nothing fills and sample undefined contents.
#[test]
fn the_metal_main_pass_declares_no_discrete_texture_or_sampler() {
    if !concinnity_shader::dxc_available() {
        return;
    }
    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"
        );
    }
    // The check is only meaningful if it saw the argument buffers at all.
    for set in ["[[buffer(7)]]", "[[buffer(10)]]"] {
        assert!(
            signature.contains(set),
            "the Metal entry takes no argument buffer at {set}: {signature}"
        );
    }
}

// Metal lays an argument buffer out by the members its function declares, and
// the host encodes the two main-pass buffers whole, at the ids the engine's own
// fragment declares. A world Shader's fragment whose `shade` reads one late
// member has to declare every member, each at that same id.
#[test]
fn a_world_fragment_declares_the_whole_argument_buffers() {
    if !concinnity_shader::dxc_available() {
        return;
    }
    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 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::GBUFFER_PREPASS_VERT,
        &mirrors::geometry::gbuffer_vertex(),
    );
}

#[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_GATHER, &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());
}