concinnity-device 0.18.66

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
Documentation
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
// src/directx/init/heap_layout.rs
//
// CBV/SRV/UAV heap slot layout for the DirectX backend. The shader-visible
// descriptor heap is a flat array of fixed slots assigned in a positional
// cascade: each block's base is the previous block's base plus its
// reservation. Keeping the cascade in one function (instead of two dozen
// inline `let x_slot = prev_slot + prev_extra` bindings) lets a unit test
// assert it stays gap-free and that the total matches the created heap size,
// so a stray offset edit fails a test instead of silently misbinding a
// descriptor at shader time (a visual glitch or an out-of-bounds heap write
// with no compile-time signal).
//
// Heap order:
//   [0]                            shadow map array SRV (Texture2DArray)
//   [1]                            IBL irradiance cube SRV
//   [2]                            IBL prefilter cube SRV
//   [object_base_slot..]           per-object (albedo, normal) pairs (2N)
//   [.. +2C]                       per-cluster (albedo, normal) pairs
//   [.. +A]                        text atlas SRVs
//   [hdr_srv_slot]                 HDR scene target SRV (composite pass)
//   [bloom_srv_base_slot..]        bloom mip SRVs
//   [lut_srv_slot]                 3D colour-grading LUT SRV
//   [taa_srv_base_slot..]          (TAA) 2 ping-pong history SRVs
//   [ssao_srv_base_slot..]         (SSAO) ao_raw + ao_blurred
//   [ssao_white_srv_slot]          1x1 white occlusion fallback (always)
//   [ssr_srv_base_slot..]          (SSR) resolve output
//   [decal_depth_srv_slot]         main-depth SRV (decal + glass + line passes)
//   [decal_srv_base_slot..]        MAX_DECALS per-decal albedo SRVs
//   [chunk_srv_base_slot..+2]      VoxelWorld chunk material (albedo, normal)
//   [skinned_srv_base_slot..]      MAX_SKINNED_OBJECTS (albedo, normal) pairs
//   [particle_srv_base_slot..]     MAX_EMITTERS emitter albedo SRVs
//   [clone_srv_base_slot..]        MAX_CLONE_DRAWS (albedo, normal) pairs
//   [fog_froxel_uav_slot]          froxel-volume UAV
//   [fog_froxel_srv_slot]          froxel-volume SRV
//   [upscale_uav_slot]             temporal-upscale output UAV
//   [upscale_srv_slot]             temporal-upscale output SRV
//   [raymarch_srv_base_slot..+4]   raymarch t0..t3 (shadow, irr, prefilter, scene)
//   [hiz_srv_slot]                 Hi-Z pyramid SRV (covers every mip)
//   [hiz_uav_base_slot..]          HIZ_MAX_MIPS per-mip UAVs
//   [transparent_scene_copy_srv_slot] pre-transparent scene snapshot SRV
//   [ssgi_gi_srv_slot..]           (SSGI) gather-target SRV
//   [spot_shadow_srv_slot]         spot shadow depth array SRV (Texture2DArray)
//   [ltc_srv_base_slot..+2]        area-light LTC tables (matrix, magnitude)
//   srv_slots                      total descriptor count (heap size)

use crate::directx::context::{FRAMES, MAX_CLONE_DRAWS, MAX_SKINNED_OBJECTS};
use crate::directx::decal::MAX_DECALS;
use crate::directx::particle::MAX_EMITTERS;

use super::HIZ_MAX_MIPS;

// Per-world feature counts that size the variable-length blocks of the SRV
// heap. The fixed-size blocks (decals, skinned, clones, particles, raymarch,
// Hi-Z, fog, upscale) use module constants and are not parameters.
pub(in crate::directx) struct SrvHeapParams {
    pub n_objects: usize,
    pub n_clusters: usize,
    pub n_atlases: usize,
    pub bloom_count: usize,
    // Per-effect SRV reservations when enabled, else 0: TAA = 2 (history
    // ping-pong), SSAO = 2 (raw + blurred occlusion), SSR = 1 (resolve output).
    // The view normal / depth / roughness / velocity all come from the unified
    // G-buffer pre-pass (`gbuffer_srv_extra`).
    pub taa_srv_extra: usize,
    pub ssao_srv_extra: usize,
    pub ssr_srv_extra: usize,
    // 1 when SSGI is enabled, else 0.
    pub ssgi_srv_extra: usize,
    // 3 (normal+depth, roughness, velocity) when the unified G-buffer pre-pass
    // is active, else 0.
    pub gbuffer_srv_extra: usize,
    // 1 when hardware ray-traced reflections are enabled (the RT output target's
    // SRV), else 0.
    pub rt_output_srv_extra: usize,
    // 2 (composited output + reduced-res blur) when the reflection composite is
    // built (SSR resolve or RT reflections authored), else 0.
    pub refl_composite_srv_extra: usize,
    // One resolve SRV per distinct planar reflector plane (0..MAX_PLANAR_PLANES),
    // reserved when the world has glass panes assigned to a planar slot, else 0.
    // The glass pass binds these per pane.
    pub planar_resolve_srv_extra: usize,
    // Flat deduplicated bindless pool sizes: one SRV per distinct albedo-pool
    // texture (incl. emissive / ORM maps) followed by one per distinct normal
    // map. The bindless main pass and the RT hit shader address this region by a
    // flat index (`albedo = texture_slot`, `normal = albedo_count + normal_slot`),
    // mirroring Vulkan/Metal. `albedo_count` is the albedo resource count
    // (>= 1: a 1x1 white fallback stands in when no albedo textures exist);
    // `normal_count` includes the slot-0 flat-normal fallback.
    pub albedo_count: usize,
    pub normal_count: usize,
}

// Resolved slot indices into the CBV/SRV/UAV heap. Field order matches the
// heap order documented above; `srv_slots` is the total descriptor count the
// heap is created with.
pub(in crate::directx) struct SrvHeapLayout {
    pub object_base_slot: usize,
    pub hdr_srv_slot: usize,
    pub bloom_srv_base_slot: usize,
    pub lut_srv_slot: usize,
    pub taa_srv_base_slot: usize,
    pub ssao_srv_base_slot: usize,
    pub ssao_white_srv_slot: usize,
    pub ssr_srv_base_slot: usize,
    pub decal_depth_srv_slot: usize,
    pub decal_srv_base_slot: usize,
    pub chunk_srv_base_slot: usize,
    pub skinned_srv_base_slot: usize,
    pub particle_srv_base_slot: usize,
    pub clone_srv_base_slot: usize,
    pub fog_froxel_uav_slot: usize,
    pub fog_froxel_srv_slot: usize,
    pub upscale_uav_slot: usize,
    pub upscale_srv_slot: usize,
    pub raymarch_srv_base_slot: usize,
    pub hiz_srv_slot: usize,
    pub hiz_uav_base_slot: usize,
    pub transparent_scene_copy_srv_slot: usize,
    pub ssgi_gi_srv_slot: usize,
    pub gbuffer_srv_base_slot: usize,
    pub rt_output_srv_slot: usize,
    // Reflection-composite SRVs: [0] composited output, [1] reduced-res blur.
    pub refl_composite_srv_base_slot: usize,
    // Planar reflection resolve SRVs (one per distinct reflector plane).
    pub planar_resolve_srv_base_slot: usize,
    pub flat_pool_base_slot: usize,
    // Contiguous MAX_PROBES-slot block of reflection-probe cube SRVs (the bindless
    // main shader's `TextureCube probe_cubes[MAX_PROBES]` table). Filled with the sky
    // prefilter cube at init; a baked probe overwrites its slot.
    pub probe_cube_base_slot: usize,
    // Spot shadow depth array SRV. A single slot rather than one of the three
    // fixed globals: the main root signatures reach the CSM array and the IBL
    // cubes as one contiguous 3-slot table, so slots 0..3 cannot take a fourth
    // member without splitting that table.
    pub spot_shadow_srv_slot: usize,
    // The two area-light LTC lookup tables, contiguous so one 2-descriptor
    // table covers both: [0] the inverse-transform matrix (RGBA32F), [1] the
    // magnitude / Fresnel pair (RG32F).
    pub ltc_srv_base_slot: usize,
    pub srv_slots: usize,
}

// The three global SRVs (shadow array, IBL irradiance, IBL prefilter) occupy
// slots [0, 3); the first per-world block starts here.
const GLOBAL_SRV_COUNT: usize = 3;

// Reflection-probe cube array length (must equal `concinnity_render::uniforms::MAX_PROBES`).
const PROBE_CUBE_COUNT: usize = concinnity_render::uniforms::MAX_PROBES;

impl SrvHeapLayout {
    pub(in crate::directx) fn compute(p: &SrvHeapParams) -> Self {
        let object_base_slot = GLOBAL_SRV_COUNT;
        // Per-object + per-cluster (albedo, normal) pairs, then text atlases.
        // `n_atlases.max(1)` reserves one slot even with no atlas so the HDR
        // SRV that follows always lands at a stable offset.
        let hdr_srv_slot =
            object_base_slot + p.n_objects * 2 + p.n_clusters * 2 + p.n_atlases.max(1);
        // The composite pass binds {HDR, bloom mip 0} as one contiguous
        // 2-descriptor table, so bloom mip 0 sits right after the HDR SRV.
        let bloom_srv_base_slot = hdr_srv_slot + 1;
        let lut_srv_slot = bloom_srv_base_slot + p.bloom_count;
        let taa_srv_base_slot = lut_srv_slot + 1;
        let ssao_srv_base_slot = taa_srv_base_slot + p.taa_srv_extra;
        // The white fallback always sits one slot past the SSAO block (present
        // whether SSAO is on or off) so the main pass can bind a pass-through
        // occlusion when SSAO is disabled.
        let ssao_white_srv_slot = ssao_srv_base_slot + p.ssao_srv_extra;
        let ssr_srv_base_slot = ssao_white_srv_slot + 1;
        let decal_depth_srv_slot = ssr_srv_base_slot + p.ssr_srv_extra;
        let decal_srv_base_slot = decal_depth_srv_slot + 1;
        let chunk_srv_base_slot = decal_srv_base_slot + MAX_DECALS;
        let skinned_srv_base_slot = chunk_srv_base_slot + 2;
        let particle_srv_base_slot = skinned_srv_base_slot + MAX_SKINNED_OBJECTS * 2;
        let clone_srv_base_slot = particle_srv_base_slot + MAX_EMITTERS;
        let fog_froxel_uav_slot = clone_srv_base_slot + MAX_CLONE_DRAWS * 2;
        let fog_froxel_srv_slot = fog_froxel_uav_slot + 1;
        let upscale_uav_slot = fog_froxel_srv_slot + 1;
        let upscale_srv_slot = upscale_uav_slot + 1;
        let raymarch_srv_base_slot = upscale_srv_slot + 1;
        let hiz_srv_slot = raymarch_srv_base_slot + 4;
        let hiz_uav_base_slot = hiz_srv_slot + 1;
        let transparent_scene_copy_srv_slot = hiz_uav_base_slot + HIZ_MAX_MIPS;
        let ssgi_gi_srv_slot = transparent_scene_copy_srv_slot + 1;
        // Unified G-buffer SRVs (normal+depth, roughness, velocity). 3 slots
        // when any screen-space consumer drives the pre-pass, else 0.
        let gbuffer_srv_base_slot = ssgi_gi_srv_slot + p.ssgi_srv_extra;
        // RT-reflection output SRV: one slot at the heap tail when RT is on.
        let rt_output_srv_slot = gbuffer_srv_base_slot + p.gbuffer_srv_extra;
        // Reflection-composite SRVs (composited output + reduced-res blur): 2 slots
        // when SSR resolve or RT is authored.
        let refl_composite_srv_base_slot = rt_output_srv_slot + p.rt_output_srv_extra;
        // Planar reflection resolve SRVs: one per distinct reflector plane, bound
        // per pane by the glass pass.
        let planar_resolve_srv_base_slot =
            refl_composite_srv_base_slot + p.refl_composite_srv_extra;
        // Flat deduplicated bindless pool: [albedo SRVs..] ++ [normal SRVs..],
        // one full copy per frame in flight. The bindless main pass and the RT
        // hit shader bind the current frame's copy and index it by a flat slot.
        // Per-frame copies let a streamed texture swap rewrite the copy whose
        // frame just fence-waited (provably unreferenced) instead of draining
        // the device to rewrite one shared region while lists reference it.
        let flat_pool_base_slot = planar_resolve_srv_base_slot + p.planar_resolve_srv_extra;
        // Reflection-probe cube array at the heap tail (MAX_PROBES contiguous cube
        // SRVs); a single descriptor table covers the whole block.
        let probe_cube_base_slot = flat_pool_base_slot + FRAMES * (p.albedo_count + p.normal_count);
        // Spot shadow array SRV. Always reserved: a world with no shadowed spot
        // binds a 1x1 fallback array there so the descriptor is never unwritten.
        let spot_shadow_srv_slot = probe_cube_base_slot + PROBE_CUBE_COUNT;
        // Area-light LTC tables. Scene-independent (fitted at build time), so
        // they are always reserved and always uploaded.
        let ltc_srv_base_slot = spot_shadow_srv_slot + 1;
        let srv_slots = ltc_srv_base_slot + 2;
        Self {
            object_base_slot,
            hdr_srv_slot,
            bloom_srv_base_slot,
            lut_srv_slot,
            taa_srv_base_slot,
            ssao_srv_base_slot,
            ssao_white_srv_slot,
            ssr_srv_base_slot,
            decal_depth_srv_slot,
            decal_srv_base_slot,
            chunk_srv_base_slot,
            skinned_srv_base_slot,
            particle_srv_base_slot,
            clone_srv_base_slot,
            fog_froxel_uav_slot,
            fog_froxel_srv_slot,
            upscale_uav_slot,
            upscale_srv_slot,
            raymarch_srv_base_slot,
            hiz_srv_slot,
            hiz_uav_base_slot,
            transparent_scene_copy_srv_slot,
            ssgi_gi_srv_slot,
            gbuffer_srv_base_slot,
            rt_output_srv_slot,
            refl_composite_srv_base_slot,
            planar_resolve_srv_base_slot,
            flat_pool_base_slot,
            probe_cube_base_slot,
            spot_shadow_srv_slot,
            ltc_srv_base_slot,
            srv_slots,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Walk the cascade for a given feature set: pair each block's reported
    // base with the size that block is independently known to occupy, then
    // assert every base equals the running total of all earlier reservations
    // (gap-free, no overlap) and that `srv_slots` covers the whole chain.
    //
    // The reservations here are derived independently of `compute`'s
    // arithmetic, so an offset slip in `compute` (a `+ 2` where `+ 1` was
    // meant, or a block sized off the wrong constant) makes a base disagree
    // with the running total and fails the assert.
    fn assert_gap_free(p: &SrvHeapParams) {
        let l = SrvHeapLayout::compute(p);
        let blocks: [(usize, usize); 31] = [
            (
                l.object_base_slot,
                p.n_objects * 2 + p.n_clusters * 2 + p.n_atlases.max(1),
            ),
            (l.hdr_srv_slot, 1),
            (l.bloom_srv_base_slot, p.bloom_count),
            (l.lut_srv_slot, 1),
            (l.taa_srv_base_slot, p.taa_srv_extra),
            (l.ssao_srv_base_slot, p.ssao_srv_extra),
            (l.ssao_white_srv_slot, 1),
            (l.ssr_srv_base_slot, p.ssr_srv_extra),
            (l.decal_depth_srv_slot, 1),
            (l.decal_srv_base_slot, MAX_DECALS),
            (l.chunk_srv_base_slot, 2),
            (l.skinned_srv_base_slot, MAX_SKINNED_OBJECTS * 2),
            (l.particle_srv_base_slot, MAX_EMITTERS),
            (l.clone_srv_base_slot, MAX_CLONE_DRAWS * 2),
            (l.fog_froxel_uav_slot, 1),
            (l.fog_froxel_srv_slot, 1),
            (l.upscale_uav_slot, 1),
            (l.upscale_srv_slot, 1),
            (l.raymarch_srv_base_slot, 4),
            (l.hiz_srv_slot, 1),
            (l.hiz_uav_base_slot, HIZ_MAX_MIPS),
            (l.transparent_scene_copy_srv_slot, 1),
            (l.ssgi_gi_srv_slot, p.ssgi_srv_extra),
            (l.gbuffer_srv_base_slot, p.gbuffer_srv_extra),
            (l.rt_output_srv_slot, p.rt_output_srv_extra),
            (l.refl_composite_srv_base_slot, p.refl_composite_srv_extra),
            (l.planar_resolve_srv_base_slot, p.planar_resolve_srv_extra),
            (
                l.flat_pool_base_slot,
                FRAMES * (p.albedo_count + p.normal_count),
            ),
            (l.probe_cube_base_slot, PROBE_CUBE_COUNT),
            (l.spot_shadow_srv_slot, 1),
            (l.ltc_srv_base_slot, 2),
        ];
        let mut expected_base = GLOBAL_SRV_COUNT;
        for (i, (base, count)) in blocks.iter().enumerate() {
            assert_eq!(
                *base, expected_base,
                "block {i} base {base} should sit at running total {expected_base}",
            );
            expected_base += count;
        }
        assert_eq!(
            l.srv_slots, expected_base,
            "srv_slots must cover every block exactly",
        );
        // The heap always reserves at least the three global SRVs.
        assert!(l.srv_slots >= GLOBAL_SRV_COUNT);
    }

    #[test]
    fn layout_gap_free_all_features_on() {
        assert_gap_free(&SrvHeapParams {
            n_objects: 7,
            n_clusters: 3,
            n_atlases: 2,
            bloom_count: 6,
            taa_srv_extra: 2,
            ssao_srv_extra: 2,
            ssr_srv_extra: 1,
            ssgi_srv_extra: 1,
            gbuffer_srv_extra: 3,
            rt_output_srv_extra: 1,
            refl_composite_srv_extra: 2,
            planar_resolve_srv_extra: 2,
            albedo_count: 9,
            normal_count: 4,
        });
    }

    #[test]
    fn layout_gap_free_all_features_off() {
        assert_gap_free(&SrvHeapParams {
            n_objects: 0,
            n_clusters: 0,
            n_atlases: 0,
            bloom_count: 0,
            taa_srv_extra: 0,
            ssao_srv_extra: 0,
            ssr_srv_extra: 0,
            ssgi_srv_extra: 0,
            gbuffer_srv_extra: 0,
            rt_output_srv_extra: 0,
            refl_composite_srv_extra: 0,
            planar_resolve_srv_extra: 0,
            albedo_count: 1,
            normal_count: 1,
        });
    }

    #[test]
    fn layout_gap_free_mixed_features() {
        assert_gap_free(&SrvHeapParams {
            n_objects: 100,
            n_clusters: 0,
            n_atlases: 1,
            bloom_count: 5,
            taa_srv_extra: 2,
            ssao_srv_extra: 0,
            ssr_srv_extra: 1,
            ssgi_srv_extra: 0,
            gbuffer_srv_extra: 3,
            rt_output_srv_extra: 1,
            refl_composite_srv_extra: 2,
            planar_resolve_srv_extra: 1,
            albedo_count: 50,
            normal_count: 12,
        });
    }

    // The per-world blocks must start past the three fixed global SRVs
    // regardless of feature set, so slot 0/1/2 are never reused.
    #[test]
    fn first_block_clears_the_global_srvs() {
        let l = SrvHeapLayout::compute(&SrvHeapParams {
            n_objects: 0,
            n_clusters: 0,
            n_atlases: 0,
            bloom_count: 0,
            taa_srv_extra: 0,
            ssao_srv_extra: 0,
            ssr_srv_extra: 0,
            ssgi_srv_extra: 0,
            gbuffer_srv_extra: 0,
            rt_output_srv_extra: 0,
            refl_composite_srv_extra: 0,
            planar_resolve_srv_extra: 0,
            albedo_count: 1,
            normal_count: 1,
        });
        assert_eq!(l.object_base_slot, GLOBAL_SRV_COUNT);
        assert!(l.hdr_srv_slot >= GLOBAL_SRV_COUNT);
    }
}