concinnity-device 0.19.24

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
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
// src/directx/decal.rs
//
// Projected (deferred) decals for the D3D12 backend. Each decal is drawn as a
// unit cube (positions in `[-0.5, 0.5]^3`) transformed by its world model
// matrix and the camera VP; the fragment shader samples the main pass's depth
// attachment to reconstruct the world-space sample point at each pixel and
// tests it against the decal's local bounding box, stamping the texture onto
// whatever fills the box.
//
// Runs after the main HDR resolve and before SSR resolve / TAA, so decals
// are reflected and tracked by the temporal history just like the rest of
// the scene. Mirrors src/metal/decal.rs.

use concinnity_core::gfx::transform::mat4_inverse;
use windows::Win32::Foundation::RECT;
use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::*;

use super::allocator::{DeviceAllocator, PooledBuffer};
use super::com;
use crate::directx::context::{DxContext, FRAMES, align256, dump_on_err};
use crate::directx::pipeline::serialize_desc_and_create;
use crate::directx::slang_builtins;
use crate::directx::slang_builtins::SlangCompile;
use crate::directx::texture::{HDR_FORMAT, create_buffer, upload_buffer, write_texture_srv};
use crate::gfx::decal::DecalRecord;

// Compile the decal vertex + fragment shaders; the MSAA variant keeps the
// fragment shader's depth SRV declaration in sync with the resource's
// sample count. Used by [`DecalResources::new`] at init and by shader hot-
// reload to rebuild the decal PSO.
pub(in crate::directx) fn compile_decal_shaders(
    msaa_samples: u32,
    hot_reload: bool,
) -> Result<(Vec<u8>, Vec<u8>), String> {
    let frag = if msaa_samples > 1 {
        &slang_builtins::DECAL_FRAG_MSAA
    } else {
        &slang_builtins::DECAL_FRAG
    };
    let vs = slang_builtins::DECAL_VERT.compile(hot_reload)?;
    let ps = frag.compile(hot_reload)?;
    Ok((vs, ps))
}

// Rebuild the decal PSO against fresh shader source. Called from the
// DirectX shader hot-reload pass. The root signature is reused; the new
// PSO is returned for the caller to swap in atomically.
pub(in crate::directx) fn rebuild_decal_pso(
    device: &ID3D12Device,
    root_sig: &ID3D12RootSignature,
    msaa_samples: u32,
    hot_reload: bool,
    info_queue: Option<&ID3D12InfoQueue>,
) -> Result<ID3D12PipelineState, String> {
    let (vs, ps) = compile_decal_shaders(msaa_samples, hot_reload)?;
    dump_on_err(info_queue, create_decal_pso(device, root_sig, &vs, &ps))
}

// Cap on the number of active decals: the SRV heap reserves a fixed block
// of `MAX_DECALS` per-decal albedo descriptors at init, so runtime adds past
// this many return an error. 256 is well under the 1024 SRV slot heap cap
// the existing backend allocates.
pub(in crate::directx) const MAX_DECALS: usize = 256;

// Eight unit-cube corners in `[-0.5, 0.5]^3`. Matches the Metal vertex list.
const CUBE_VERTS: [f32; 24] = [
    -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5,
    0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
];

// 36 indices forming 12 triangles wound CCW outward. Matches the Metal
// index list so the rasterised cube exactly mirrors the reference.
const CUBE_INDICES: [u16; 36] = [
    // -Z face                +Z face
    0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, // -Y                     +Y
    0, 1, 5, 0, 5, 4, 3, 6, 2, 3, 7, 6, // -X                     +X
    0, 4, 7, 0, 7, 3, 1, 2, 6, 1, 6, 5,
];

// `DecalView` (per-frame) and `DecalParams` (per-decal) are GPU-free layout
// structs that live in `core::render`; re-export them so
// `crate::directx::decal::{DecalView,DecalParams}` are unchanged.
pub(in crate::directx) use concinnity_core::render::uniforms::DecalParams;
pub(in crate::directx) use concinnity_core::render::uniforms::DecalView;

// Root-signature layout (binds 1:1 with the `decal.slang` declarations, whose
// registers slangc assigns from declaration order; `SLANG_DXIL_ENTRY_ABI` in
// build.rs pins each one):
//   [0] root CBV b0   DecalView    (per-frame)
//   [1] root CBV b1   DecalParams  (per-decal)
//   [2] table  t0     scene depth SRV (Texture2D[MS]<float>)
//   [3] table  t1     decal albedo SRV
//   static sampler s0 : linear clamp
fn create_decal_root_signature(device: &ID3D12Device) -> Result<ID3D12RootSignature, String> {
    let depth_range = D3D12_DESCRIPTOR_RANGE {
        RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
        NumDescriptors: 1,
        BaseShaderRegister: 0, // t0
        RegisterSpace: 0,
        OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
    };
    let albedo_range = D3D12_DESCRIPTOR_RANGE {
        RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
        NumDescriptors: 1,
        BaseShaderRegister: 1, // t1
        RegisterSpace: 0,
        OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
    };
    let params = [
        D3D12_ROOT_PARAMETER {
            ParameterType: D3D12_ROOT_PARAMETER_TYPE_CBV,
            Anonymous: D3D12_ROOT_PARAMETER_0 {
                Descriptor: D3D12_ROOT_DESCRIPTOR {
                    ShaderRegister: 0,
                    RegisterSpace: 0,
                },
            },
            ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
        },
        D3D12_ROOT_PARAMETER {
            ParameterType: D3D12_ROOT_PARAMETER_TYPE_CBV,
            Anonymous: D3D12_ROOT_PARAMETER_0 {
                Descriptor: D3D12_ROOT_DESCRIPTOR {
                    ShaderRegister: 1,
                    RegisterSpace: 0,
                },
            },
            ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
        },
        D3D12_ROOT_PARAMETER {
            ParameterType: D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE,
            Anonymous: D3D12_ROOT_PARAMETER_0 {
                DescriptorTable: D3D12_ROOT_DESCRIPTOR_TABLE {
                    NumDescriptorRanges: 1,
                    pDescriptorRanges: &depth_range,
                },
            },
            ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
        },
        D3D12_ROOT_PARAMETER {
            ParameterType: D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE,
            Anonymous: D3D12_ROOT_PARAMETER_0 {
                DescriptorTable: D3D12_ROOT_DESCRIPTOR_TABLE {
                    NumDescriptorRanges: 1,
                    pDescriptorRanges: &albedo_range,
                },
            },
            ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
        },
    ];
    let samp = D3D12_STATIC_SAMPLER_DESC {
        Filter: D3D12_FILTER_MIN_MAG_MIP_LINEAR,
        AddressU: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
        AddressV: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
        AddressW: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
        ComparisonFunc: D3D12_COMPARISON_FUNC_ALWAYS,
        BorderColor: D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK,
        MinLOD: 0.0,
        MaxLOD: f32::MAX,
        ShaderRegister: 0,
        RegisterSpace: 0,
        ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
        ..Default::default()
    };
    let desc = D3D12_ROOT_SIGNATURE_DESC {
        NumParameters: params.len() as u32,
        pParameters: params.as_ptr(),
        NumStaticSamplers: 1,
        pStaticSamplers: &samp,
        Flags: D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT,
    };
    serialize_desc_and_create(device, &desc, "decal root sig")
}

fn decal_input_layout() -> [D3D12_INPUT_ELEMENT_DESC; 1] {
    [D3D12_INPUT_ELEMENT_DESC {
        SemanticName: windows::core::s!("POSITION"),
        SemanticIndex: 0,
        Format: DXGI_FORMAT_R32G32B32_FLOAT,
        InputSlot: 0,
        AlignedByteOffset: 0,
        InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,
        InstanceDataStepRate: 0,
    }]
}

// PSO for the decal pass. Writes the resolved HDR target with src-alpha /
// inv-src-alpha blending: the fragment shader emits `tint * tex.rgb` at the
// computed fade-weighted alpha and the blend composites it onto the scene.
// No depth attachment; the unit-cube + reconstructed-position clip in the
// fragment shader does the volumetric culling.
fn create_decal_pso(
    device: &ID3D12Device,
    root_sig: &ID3D12RootSignature,
    vs: &[u8],
    ps: &[u8],
) -> Result<ID3D12PipelineState, String> {
    let layout = decal_input_layout();
    let pso_desc = D3D12_GRAPHICS_PIPELINE_STATE_DESC {
        pRootSignature: com::borrowed(root_sig),
        VS: D3D12_SHADER_BYTECODE {
            pShaderBytecode: vs.as_ptr() as _,
            BytecodeLength: vs.len(),
        },
        PS: D3D12_SHADER_BYTECODE {
            pShaderBytecode: ps.as_ptr() as _,
            BytecodeLength: ps.len(),
        },
        InputLayout: D3D12_INPUT_LAYOUT_DESC {
            pInputElementDescs: layout.as_ptr(),
            NumElements: layout.len() as u32,
        },
        PrimitiveTopologyType: D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE,
        NumRenderTargets: 1,
        RTVFormats: {
            let mut a = [DXGI_FORMAT_UNKNOWN; 8];
            a[0] = HDR_FORMAT;
            a
        },
        DSVFormat: DXGI_FORMAT_UNKNOWN,
        SampleDesc: DXGI_SAMPLE_DESC {
            Count: 1,
            Quality: 0,
        },
        SampleMask: u32::MAX,
        RasterizerState: D3D12_RASTERIZER_DESC {
            FillMode: D3D12_FILL_MODE_SOLID,
            // Cull front faces: the camera may be inside a decal box. With
            // back-face culling on (the default) entering the volume would
            // make the unit cube disappear; culling the front face keeps the
            // back faces rasterised in both cases.
            CullMode: D3D12_CULL_MODE_FRONT,
            FrontCounterClockwise: true.into(),
            DepthClipEnable: false.into(),
            ..Default::default()
        },
        DepthStencilState: D3D12_DEPTH_STENCIL_DESC {
            DepthEnable: false.into(),
            DepthWriteMask: D3D12_DEPTH_WRITE_MASK_ZERO,
            StencilEnable: false.into(),
            ..Default::default()
        },
        BlendState: D3D12_BLEND_DESC {
            RenderTarget: {
                let mut arr = [D3D12_RENDER_TARGET_BLEND_DESC::default(); 8];
                arr[0] = D3D12_RENDER_TARGET_BLEND_DESC {
                    BlendEnable: true.into(),
                    SrcBlend: D3D12_BLEND_SRC_ALPHA,
                    DestBlend: D3D12_BLEND_INV_SRC_ALPHA,
                    BlendOp: D3D12_BLEND_OP_ADD,
                    SrcBlendAlpha: D3D12_BLEND_SRC_ALPHA,
                    DestBlendAlpha: D3D12_BLEND_INV_SRC_ALPHA,
                    BlendOpAlpha: D3D12_BLEND_OP_ADD,
                    RenderTargetWriteMask: D3D12_COLOR_WRITE_ENABLE_ALL.0 as u8,
                    ..Default::default()
                };
                arr
            },
            ..Default::default()
        },
        ..Default::default()
    };
    // SAFETY: `desc` outlives this synchronous call, and so do the root signature, shader bytecode
    // and input-element array whose raw pointers it borrows.
    unsafe { crate::directx::pso_library::create_graphics(device, &pso_desc) }
        .map_err(|e| format!("create decal PSO: {e}"))
}

// Owned by `DxContext` exactly once: the decal pipeline, the unit-cube
// vertex / index buffers, and the per-frame uniform ring (one big upload
// buffer per frame split into per-decal regions). The decal slot table lives on
// `DxContext` itself (mirroring the Metal context layout).
pub(in crate::directx) struct DecalResources {
    pub(in crate::directx) root_sig: ID3D12RootSignature,
    pub(in crate::directx) pso: ID3D12PipelineState,

    // Resources held to keep the GPU memory alive while the views below
    // reference them; the encoder binds through the views.
    #[expect(
        dead_code,
        reason = "held to keep the GPU memory alive; the encoder binds through vertex_buffer_view"
    )]
    pub(in crate::directx) vertex_buffer: PooledBuffer,
    pub(in crate::directx) vertex_buffer_view: D3D12_VERTEX_BUFFER_VIEW,
    #[expect(
        dead_code,
        reason = "held to keep the GPU memory alive; the encoder binds through index_buffer_view"
    )]
    pub(in crate::directx) index_buffer: PooledBuffer,
    pub(in crate::directx) index_buffer_view: D3D12_INDEX_BUFFER_VIEW,

    // Per-frame view UBO (single 144-byte block), persistently mapped.
    pub(in crate::directx) view_ubo_resources: Vec<PooledBuffer>,
    pub(in crate::directx) view_ubo_ptrs: Vec<*mut u8>,
    // Per-frame `MAX_DECALS`-slot params ring. Each slot is `align256(160)`
    // = 256 bytes wide so the per-decal CBV GPU address is naturally aligned.
    pub(in crate::directx) params_ubo_resources: Vec<PooledBuffer>,
    pub(in crate::directx) params_ubo_ptrs: Vec<*mut u8>,
    pub(in crate::directx) params_stride: u64,

    // Heap slot of the first per-decal albedo SRV; slot `i` is the SRV for
    // decal id `i`. Written by `add_decal` / refreshed by `update_texture_slot`
    // when a streamed texture lands.
    pub(in crate::directx) decal_srv_base_slot: usize,
    // Heap slot of the main-depth SRV. Bound at decal pass t0; the resource
    // is transitioned to PIXEL_SHADER_RESOURCE around the pass.
    pub(in crate::directx) depth_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
}

impl DecalResources {
    // Build the decal pipeline + unit-cube buffers + per-frame uniform
    // rings. Called from `DxContext::new` when the world declares any
    // `Decal` OR unconditionally so runtime `add_decal` works from a world
    // that started empty; the cost is one PSO + a few small buffers.
    pub(in crate::directx) fn new(
        alloc: &DeviceAllocator,
        msaa_samples: u32,
        decal_srv_base_slot: usize,
        depth_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
        info_queue: Option<&ID3D12InfoQueue>,
        hot_reload: bool,
    ) -> Result<Self, String> {
        let device = alloc.device();
        let (vs, ps) = compile_decal_shaders(msaa_samples, hot_reload)?;

        let root_sig = dump_on_err(info_queue, create_decal_root_signature(device))?;
        let pso = dump_on_err(info_queue, create_decal_pso(device, &root_sig, &vs, &ps))?;

        // Unit-cube vertex + index buffers.
        let vbytes = bytemuck::cast_slice(CUBE_VERTS.as_slice());
        let ibytes = bytemuck::cast_slice(CUBE_INDICES.as_slice());
        let vertex_buffer = upload_buffer(
            alloc,
            vbytes,
            D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
        )?;
        let index_buffer = upload_buffer(alloc, ibytes, D3D12_RESOURCE_STATE_INDEX_BUFFER)?;
        let vertex_buffer_view = D3D12_VERTEX_BUFFER_VIEW {
            BufferLocation: com::gpu_va(&vertex_buffer),
            SizeInBytes: vbytes.len() as u32,
            StrideInBytes: 12,
        };
        let index_buffer_view = D3D12_INDEX_BUFFER_VIEW {
            BufferLocation: com::gpu_va(&index_buffer),
            SizeInBytes: ibytes.len() as u32,
            Format: DXGI_FORMAT_R16_UINT,
        };

        // Per-frame view UBO.
        let view_size = align256(std::mem::size_of::<DecalView>() as u64);
        let mut view_ubo_resources: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
        let mut view_ubo_ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
        for _ in 0..FRAMES {
            let buf = create_buffer(
                alloc,
                view_size,
                D3D12_HEAP_TYPE_UPLOAD,
                D3D12_RESOURCE_STATE_GENERIC_READ,
            )?;
            let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
            // SAFETY: the resource is a live CPU-visible buffer, and the out-parameter is a live
            // local that receives the mapping.
            unsafe { buf.Map(0, None, Some(&mut ptr)) }
                .map_err(|e| format!("map decal view ubo: {e}"))?;
            view_ubo_ptrs.push(ptr as *mut u8);
            view_ubo_resources.push(buf);
        }

        // Per-frame per-decal params ring. One CBV is 256-aligned, so size
        // each slot to align256(sizeof(DecalParams)).
        let params_stride = align256(std::mem::size_of::<DecalParams>() as u64);
        let params_total = params_stride * MAX_DECALS as u64;
        let mut params_ubo_resources: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
        let mut params_ubo_ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
        for _ in 0..FRAMES {
            let buf = create_buffer(
                alloc,
                params_total,
                D3D12_HEAP_TYPE_UPLOAD,
                D3D12_RESOURCE_STATE_GENERIC_READ,
            )?;
            let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
            // SAFETY: the resource is a live CPU-visible buffer, and the out-parameter is a live
            // local that receives the mapping.
            unsafe { buf.Map(0, None, Some(&mut ptr)) }
                .map_err(|e| format!("map decal params ubo: {e}"))?;
            params_ubo_ptrs.push(ptr as *mut u8);
            params_ubo_resources.push(buf);
        }

        Ok(Self {
            root_sig,
            pso,
            vertex_buffer,
            vertex_buffer_view,
            index_buffer,
            index_buffer_view,
            view_ubo_resources,
            view_ubo_ptrs,
            params_ubo_resources,
            params_ubo_ptrs,
            params_stride,
            decal_srv_base_slot,
            depth_srv_gpu,
        })
    }
}

// Helpers for writing the main-depth SRV (so the runtime can rebuild it on
// resize if a future change adds that path) and the per-decal albedo SRVs.

// Write the main depth resource's SRV. MSAA: `Texture2DMS<float>`;
// otherwise plain `Texture2D<float>`. Format is the typed view of
// `R32_TYPELESS` (the depth resource's underlying format): `R32_FLOAT`.
pub(in crate::directx) fn write_main_depth_srv(
    device: &ID3D12Device,
    depth: &ID3D12Resource,
    srv_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
    sample_count: u32,
) {
    let srv_desc = if sample_count > 1 {
        D3D12_SHADER_RESOURCE_VIEW_DESC {
            Format: DXGI_FORMAT_R32_FLOAT,
            ViewDimension: D3D12_SRV_DIMENSION_TEXTURE2DMS,
            Shader4ComponentMapping: D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
            Anonymous: D3D12_SHADER_RESOURCE_VIEW_DESC_0 {
                Texture2DMS: D3D12_TEX2DMS_SRV {
                    UnusedField_NothingToDefine: 0,
                },
            },
        }
    } else {
        D3D12_SHADER_RESOURCE_VIEW_DESC {
            Format: DXGI_FORMAT_R32_FLOAT,
            ViewDimension: D3D12_SRV_DIMENSION_TEXTURE2D,
            Shader4ComponentMapping: D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
            Anonymous: D3D12_SHADER_RESOURCE_VIEW_DESC_0 {
                Texture2D: D3D12_TEX2D_SRV {
                    MipLevels: 1,
                    ..Default::default()
                },
            },
        }
    };
    // SAFETY: the view descriptor and the resource it names are live for the call, and the
    // destination handle addresses a slot this context reserved for the view in a heap it owns.
    unsafe { device.CreateShaderResourceView(depth, Some(&srv_desc), srv_cpu) };
}

// Encoder

impl DxContext {
    // Encode the projected-decal pass. Called between the main HDR resolve
    // and the SSR resolve so a decal is reflected by SSR and tracked by
    // TAA's history buffer like the rest of the scene.
    //
    // `vp` is the same jittered view-projection the main pass rasterised
    // with; the inverse drives the world-space reconstruction in the
    // fragment shader.
    pub(in crate::directx) fn encode_decals(
        &self,
        cmd: &ID3D12GraphicsCommandList,
        frame_idx: usize,
        vp: [[f32; 4]; 4],
        frustum: &crate::gfx::frustum::Frustum,
    ) {
        let decals = match &self.decal.state {
            Some(s) => s,
            None => return,
        };
        // Frustum-cull first so a frame where every live decal lands
        // off-screen skips the pass, including the depth-transition
        // barriers. Peeking answers that without testing any decal twice.
        let mut visible = self.decal.set.visible(frustum).peekable();
        if visible.peek().is_none() {
            return;
        }

        // Upload this frame's view UBO.
        let inv_vp = mat4_inverse(vp);
        let viewport = [
            self.extent.render_width as f32,
            self.extent.render_height as f32,
        ];
        let view_uni = DecalView {
            vp,
            inv_vp,
            viewport,
            _pad: [0.0; 2],
        };
        // SAFETY: the destination is the persistent mapping of an UPLOAD-heap constant buffer that
        // init sized for this payload, and the source is a separate live value, so the ranges
        // cannot overlap.
        unsafe {
            std::ptr::copy_nonoverlapping(
                &view_uni as *const DecalView as *const u8,
                decals.view_ubo_ptrs[frame_idx],
                std::mem::size_of::<DecalView>(),
            );
        }
        let view_gva = com::gpu_va(&decals.view_ubo_resources[frame_idx]);
        let params_base_gva = com::gpu_va(&decals.params_ubo_resources[frame_idx]);

        // Main depth is already in a shader-resource state for the fragment's
        // sample: the graph declares this pass's depth read and the executor emits
        // the transition ahead of this command list.

        // The scene spine is a graph resource: this pass declares its
        // read-modify-write, so the executor has already put it in RENDER_TARGET
        // and the next consumer's barrier takes it back out.
        let scene_rtv = self.hdr_scene_rtv();

        let w = self.extent.render_width;
        let h = self.extent.render_height;
        // SAFETY: the command list is in the recording state, and every resource, descriptor and
        // slice these commands name is live for the call.
        unsafe {
            cmd.OMSetRenderTargets(1, Some(&scene_rtv), false, None);
            let vp = D3D12_VIEWPORT {
                TopLeftX: 0.0,
                TopLeftY: 0.0,
                Width: w as f32,
                Height: h as f32,
                MinDepth: 0.0,
                MaxDepth: 1.0,
            };
            cmd.RSSetViewports(&[vp]);
            let scissor = RECT {
                left: 0,
                top: 0,
                right: w as i32,
                bottom: h as i32,
            };
            cmd.RSSetScissorRects(&[scissor]);
            cmd.IASetPrimitiveTopology(
                windows::Win32::Graphics::Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
            );
            cmd.IASetVertexBuffers(0, Some(&[decals.vertex_buffer_view]));
            cmd.IASetIndexBuffer(Some(&decals.index_buffer_view));

            cmd.SetPipelineState(&decals.pso);
            cmd.SetGraphicsRootSignature(&decals.root_sig);
            cmd.SetDescriptorHeaps(&[Some(self.descriptors.srv_heap.clone())]);
            cmd.SetGraphicsRootConstantBufferView(0, view_gva);
            cmd.SetGraphicsRootDescriptorTable(2, decals.depth_srv_gpu);
        }

        // Base of this pass's per-decal albedo SRVs, written into the heap at
        // `decal_srv_base_slot + id` by `add_decal`. The heap start is fixed
        // for the heap's lifetime, so the COM query is hoisted out of the draw
        // loop and each decal's handle is a stride from here.
        // SAFETY: a property query on a live descriptor heap; it only reads.
        let srv_gpu_base = unsafe {
            self.descriptors
                .srv_heap
                .GetGPUDescriptorHandleForHeapStart()
        };
        let albedo_base_ptr = srv_gpu_base.ptr
            + (decals.decal_srv_base_slot * self.descriptors.srv_descriptor_size) as u64;

        for decal in visible {
            // This frame's ring slot keeps what an earlier frame wrote, so a
            // decal whose record has not changed since is already uploaded.
            if decal.take_upload(frame_idx) {
                // SAFETY: each ring slot is `params_stride * MAX_DECALS` bytes and `add_decal`
                // refuses records past `MAX_DECALS`, so `id * params_stride` stays inside this
                // frame's mapping.
                let dst = unsafe {
                    decals.params_ubo_ptrs[frame_idx]
                        .add((decal.id as u64 * decals.params_stride) as usize)
                };
                // SAFETY: the mapping covers an UPLOAD-heap buffer created to hold this payload,
                // and the source is a separate allocation, so the ranges cannot overlap.
                unsafe {
                    std::ptr::copy_nonoverlapping(
                        decal.params as *const DecalParams as *const u8,
                        dst,
                        std::mem::size_of::<DecalParams>(),
                    );
                }
            }
            let params_gva = params_base_gva + decal.id as u64 * decals.params_stride;
            let albedo_srv_gpu = D3D12_GPU_DESCRIPTOR_HANDLE {
                ptr: albedo_base_ptr + (decal.id * self.descriptors.srv_descriptor_size) as u64,
            };
            // SAFETY: the command list is in the recording state, and every resource, descriptor
            // and slice these commands name is live for the call.
            unsafe {
                cmd.SetGraphicsRootConstantBufferView(1, params_gva);
                cmd.SetGraphicsRootDescriptorTable(3, albedo_srv_gpu);
                cmd.DrawIndexedInstanced(36, 1, 0, 0, 0);
            }
            self.inc_draw_calls(1);
        }
    }
}

// Runtime mutation (RenderBackend::add_decal / remove_decal)

// Runtime-mutation surface driven only by the cn-debug command server
// ([debug/runtime_spawn.rs]). This module tree is compiled into both the FFI
// library crate (`concinnity_dev`) and the `concinnity` binary; the cn-debug
// chain is reachable from the binary's `main` but not from the library crate's
// (FFI) roots, so dead-code flags these in the lib build. Not backend-specific.
// `allow` (not `expect`) because the same source is live in the binary, where
// an `expect` would be unfulfilled. Suppressing the methods also marks them live
// roots, so the slot-table / SRV-slot fields they touch stay un-flagged on their
// own.
impl DxContext {
    // Append a runtime decal. Writes the per-decal albedo SRV into the
    // reserved heap region; the encoder reads it next frame. Reuses
    // tombstoned slots from a prior `remove_decal` before growing the vec.
    pub(crate) fn add_decal(&mut self, record: DecalRecord) -> Result<usize, String> {
        let state = self
            .decal
            .state
            .as_ref()
            .ok_or_else(|| "add_decal: decal pipeline unavailable".to_string())?;
        let base_slot = state.decal_srv_base_slot;

        let last_tex = self.descriptors.textures.len().saturating_sub(1);
        let tex_idx = record.texture_slot.min(last_tex);

        // Write the SRV for the chosen texture into this decal's heap slot.
        // The slot may be reused from a prior tombstone, in which case the
        // old descriptor is just overwritten.
        let id = self
            .decal
            .set
            .insert(record)
            .map_err(|_| format!("add_decal: MAX_DECALS ({MAX_DECALS}) exceeded"))?;
        let srv_cpu = D3D12_CPU_DESCRIPTOR_HANDLE {
            // SAFETY: a property query on a live descriptor heap; it only reads.
            ptr: unsafe {
                self.descriptors
                    .srv_heap
                    .GetCPUDescriptorHandleForHeapStart()
            }
            .ptr + (base_slot + id) * self.descriptors.srv_descriptor_size,
        };
        write_texture_srv(&self.device, &self.descriptors.textures[tex_idx], srv_cpu);
        Ok(id)
    }

    // Tombstone a runtime decal slot. The id becomes invalid; the next
    // `add_decal` may reuse it. Returns an error when the id is out of
    // range or already tombstoned.
    pub(crate) fn remove_decal(&mut self, decal_id: usize) -> Result<(), String> {
        self.decal
            .set
            .remove(decal_id)
            .map_err(|e| format!("remove_decal: id {decal_id} {e}"))
    }
}