concinnity-device 0.19.119

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! 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]) {
    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  "),
    );
}

// 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() {
    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"
        );
    }
    // 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() {
    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"
        );
    }
}

// A world hook that calls `material_param` reads the table at the slot the
// Metal encoder binds it, from either stage.
#[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(),
    );
}

// The Metal host binds the pre-pass's own buffers at the slots core names,
// the view block to both stages; the emitted MSL of each entry is where the
// shader actually reads them.
#[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
            );
        }
    }
}

// The `[[buffer(n)]]` an entry parameter named `name` is declared at.
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());
}