concinnity-device 0.19.2

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
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
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
// src/directx/fog.rs
//
// Volumetric fog for the D3D12 backend. Frostbite-style froxel volume:
//
//   * The `fog_froxel_kernel` compute pass (`encode_fog_froxel`) populates a
//     screen-aligned `(80 × 45 × 64)` 3D `RGBA16Float` volume of
//     `(scattered_rgb, 1 - T)` across the view frustum. One thread per
//     (x, y) tile; each thread walks Z front-to-back, accumulating the
//     per-slab scatter + transmittance with a CSM shadow tap per slice.
//
//   * The fullscreen `Fog` render pass (`encode_fog`) samples the volume by
//     `(screen_uv, view_z)` instead of marching per-pixel and composites
//     `(scattered, 1 - T)` over the resolved HDR target with the standard
//     `over` blend (`final = scene * T + scattered`).
//
// Runs between the projected-decal pass and the SSR resolve so the fog wraps
// the decal-stamped scene and SSR reflects through it; TAA history then
// reprojects the integrated fog colour and transmittance.
//
// Mirrors src/metal/fog.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};
use crate::gfx::render_graph::{FOG_FROXEL_X, FOG_FROXEL_Y, FOG_FROXEL_Z};
use crate::gfx::render_types::{FogFroxelParams, FogParams};

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

// Compile the froxel-volume compute kernel.
pub(in crate::directx) fn compile_fog_froxel_shader(hot_reload: bool) -> Result<Vec<u8>, String> {
    slang_builtins::FOG_FROXEL.compile(hot_reload)
}

// Rebuild the fog PSO against fresh shader source. Called from the DirectX
// shader hot-reload pass; reuses the existing root signature.
pub(in crate::directx) fn rebuild_fog_pso(
    device: &ID3D12Device,
    root_sig: &ID3D12RootSignature,
    msaa_samples: u32,
    hot_reload: bool,
    info_queue: Option<&ID3D12InfoQueue>,
) -> Result<ID3D12PipelineState, String> {
    let (vs, ps) = compile_fog_shaders(msaa_samples, hot_reload)?;
    dump_on_err(info_queue, create_fog_pso(device, root_sig, &vs, &ps))
}

// Rebuild the froxel compute PSO against fresh shader source.
pub(in crate::directx) fn rebuild_fog_froxel_pso(
    device: &ID3D12Device,
    root_sig: &ID3D12RootSignature,
    hot_reload: bool,
    info_queue: Option<&ID3D12InfoQueue>,
) -> Result<ID3D12PipelineState, String> {
    let cs = compile_fog_froxel_shader(hot_reload)?;
    dump_on_err(info_queue, create_fog_froxel_pso(device, root_sig, &cs))
}

// Fog render-pass root signature:
//   [0] root CBV b0   FogParams         (per-frame)
//   [1] root CBV b1   FogFroxelParams   (per-frame)
//   [2] table  t0     scene depth SRV (Texture2D[MS]<float>)
//   [3] table  t1     froxel volume SRV (Texture3D<float4>)
// Static linear-clamp sampler s0 for the trilinear volume sample.
fn create_fog_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 volume_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_PIXEL,
        },
        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_PIXEL,
        },
        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: &volume_range,
                },
            },
            ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
        },
    ];
    let volume_sampler = 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,
        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: &volume_sampler,
        // The fullscreen pass uses SV_VertexID; no input assembler is needed.
        Flags: D3D12_ROOT_SIGNATURE_FLAG_NONE,
    };
    serialize_desc_and_create(device, &desc, "fog root sig")
}

// Froxel compute root signature:
//   [0] root CBV b0   FogParams         (per-frame)
//   [1] root CBV b1   FogFroxelParams   (per-frame)
//   [2] root CBV b2   ShadowUniforms    (per-frame, shared with Main / Shadow)
//   [3] table  t0     shadow map SRV (Texture2DArray<float>)
//   [4] table  u0     froxel volume UAV (RWTexture3D<float4>)
// Static comparison sampler s0 for the shadow tap.
fn create_fog_froxel_root_signature(device: &ID3D12Device) -> Result<ID3D12RootSignature, String> {
    let shadow_range = D3D12_DESCRIPTOR_RANGE {
        RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
        NumDescriptors: 1,
        BaseShaderRegister: 0, // t0
        RegisterSpace: 0,
        OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
    };
    let volume_range = D3D12_DESCRIPTOR_RANGE {
        RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_UAV,
        NumDescriptors: 1,
        BaseShaderRegister: 0, // u0
        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_CBV,
            Anonymous: D3D12_ROOT_PARAMETER_0 {
                Descriptor: D3D12_ROOT_DESCRIPTOR {
                    ShaderRegister: 2,
                    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: &shadow_range,
                },
            },
            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: &volume_range,
                },
            },
            ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
        },
    ];
    // Comparison sampler matching the existing `shadow_sampler_gpu` static
    // sampler. Clamp on every axis so cascades that fail their NDC bounds
    // check fall back to 1.0 via the explicit `if (any(uv < 0.0)...)` guard
    // in the kernel anyway.
    let shadow_sampler = D3D12_STATIC_SAMPLER_DESC {
        Filter: D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT,
        AddressU: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
        AddressV: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
        AddressW: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
        ComparisonFunc: D3D12_COMPARISON_FUNC_LESS_EQUAL,
        MaxLOD: f32::MAX,
        ShaderRegister: 0,
        RegisterSpace: 0,
        ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
        ..Default::default()
    };
    let desc = D3D12_ROOT_SIGNATURE_DESC {
        NumParameters: params.len() as u32,
        pParameters: params.as_ptr(),
        NumStaticSamplers: 1,
        pStaticSamplers: &shadow_sampler,
        Flags: D3D12_ROOT_SIGNATURE_FLAG_NONE,
    };
    serialize_desc_and_create(device, &desc, "fog froxel root sig")
}

// PSO for the fog pass. Writes the resolved HDR target with `(scattered,
// 1 - T)` over scene blending: the fragment emits the in-scattered colour
// at `1 - transmittance` alpha and the blend resolves to
// `scene * T + scattered`. No depth attachment; the shader handles the
// depth-based ray-length termination itself.
fn create_fog_pso(
    device: &ID3D12Device,
    root_sig: &ID3D12RootSignature,
    vs: &[u8],
    ps: &[u8],
) -> Result<ID3D12PipelineState, String> {
    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(),
        },
        // No input layout; the fullscreen triangle is emitted by SV_VertexID.
        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,
            CullMode: D3D12_CULL_MODE_NONE,
            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_ONE,
                    DestBlend: D3D12_BLEND_INV_SRC_ALPHA,
                    BlendOp: D3D12_BLEND_OP_ADD,
                    SrcBlendAlpha: D3D12_BLEND_ONE,
                    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 fog PSO: {e}"))
}

// Compute PSO for the froxel kernel.
fn create_fog_froxel_pso(
    device: &ID3D12Device,
    root_sig: &ID3D12RootSignature,
    cs: &[u8],
) -> Result<ID3D12PipelineState, String> {
    let desc = D3D12_COMPUTE_PIPELINE_STATE_DESC {
        pRootSignature: com::borrowed(root_sig),
        CS: D3D12_SHADER_BYTECODE {
            pShaderBytecode: cs.as_ptr() as _,
            BytecodeLength: cs.len(),
        },
        ..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_compute(device, &desc) }
        .map_err(|e| format!("create fog froxel PSO: {e}"))
}

// Create the 3D `RGBA16Float` froxel volume. Rests in `PIXEL_SHADER_RESOURCE`
// between frames: the graph's FogFroxel producer barrier transitions it to
// `UNORDERED_ACCESS` for the compute write, and the Fog consumer barrier returns
// it to `PIXEL_SHADER_RESOURCE` for the trilinear sample. Both transitions are
// graph-driven (no inline cross-frame reset); creating it sampled makes frame 0's
// producer barrier (sampled -> UAV) start from the resource's real state.
fn create_fog_froxel_volume(
    device: &ID3D12Device,
    uav_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
    srv_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
) -> Result<ID3D12Resource, String> {
    let heap_props = D3D12_HEAP_PROPERTIES {
        Type: D3D12_HEAP_TYPE_DEFAULT,
        ..Default::default()
    };
    let desc = D3D12_RESOURCE_DESC {
        Dimension: D3D12_RESOURCE_DIMENSION_TEXTURE3D,
        Width: FOG_FROXEL_X as u64,
        Height: FOG_FROXEL_Y,
        DepthOrArraySize: FOG_FROXEL_Z as u16,
        MipLevels: 1,
        Format: DXGI_FORMAT_R16G16B16A16_FLOAT,
        SampleDesc: DXGI_SAMPLE_DESC {
            Count: 1,
            Quality: 0,
        },
        Flags: D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS,
        ..Default::default()
    };
    let mut tex_opt: Option<ID3D12Resource> = None;
    // SAFETY: the create descriptor and every pointer it borrows are live for the call, and the new
    // COM object lands in a binding that owns it.
    unsafe {
        device.CreateCommittedResource(
            &heap_props,
            D3D12_HEAP_FLAG_NONE,
            &desc,
            // Rest sampled; the graph drives both transitions (see the fn doc).
            D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
            None,
            &mut tex_opt,
        )
    }
    .map_err(|e| format!("create fog froxel volume: {e}"))?;
    let resource = tex_opt.ok_or_else(|| "create fog froxel volume returned None".to_string())?;

    let uav_desc = D3D12_UNORDERED_ACCESS_VIEW_DESC {
        Format: DXGI_FORMAT_R16G16B16A16_FLOAT,
        ViewDimension: D3D12_UAV_DIMENSION_TEXTURE3D,
        Anonymous: D3D12_UNORDERED_ACCESS_VIEW_DESC_0 {
            Texture3D: D3D12_TEX3D_UAV {
                MipSlice: 0,
                FirstWSlice: 0,
                WSize: FOG_FROXEL_Z,
            },
        },
    };
    // 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.CreateUnorderedAccessView(&resource, None, Some(&uav_desc), uav_cpu);
    }

    let srv_desc = D3D12_SHADER_RESOURCE_VIEW_DESC {
        Format: DXGI_FORMAT_R16G16B16A16_FLOAT,
        ViewDimension: D3D12_SRV_DIMENSION_TEXTURE3D,
        Shader4ComponentMapping: D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
        Anonymous: D3D12_SHADER_RESOURCE_VIEW_DESC_0 {
            Texture3D: D3D12_TEX3D_SRV {
                MostDetailedMip: 0,
                MipLevels: 1,
                ResourceMinLODClamp: 0.0,
            },
        },
    };
    // 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(&resource, Some(&srv_desc), srv_cpu) };

    Ok(resource)
}

// Owned by `DxContext` exactly when the world declared a `VolumetricFog`:
// the fog pipeline + per-frame uniform rings + the froxel volume + the
// kernel that populates it. The depth SRV the fog pass reads through t0
// is shared with the projected-decal pass.
pub(in crate::directx) struct FogResources {
    pub(in crate::directx) root_sig: ID3D12RootSignature,
    pub(in crate::directx) pso: ID3D12PipelineState,

    pub(in crate::directx) froxel_root_sig: ID3D12RootSignature,
    pub(in crate::directx) froxel_pso: ID3D12PipelineState,

    // Per-frame FogParams ring (176-byte block, persistently mapped).
    pub(in crate::directx) params_ubo_resources: Vec<PooledBuffer>,
    pub(in crate::directx) params_ubo_ptrs: Vec<*mut u8>,

    // Per-frame FogFroxelParams ring (96-byte block, persistently mapped).
    pub(in crate::directx) froxel_params_ubo_resources: Vec<PooledBuffer>,
    pub(in crate::directx) froxel_params_ubo_ptrs: Vec<*mut u8>,

    // 3D `RGBA16Float` volume the kernel writes and the fragment shader
    // samples. The handle backs the graph-driven UAV ↔ PIXEL_SHADER_RESOURCE
    // producer + consumer barriers (resolved by the executor's barrier
    // registry); the shader reads/writes go through the heap-stored UAV + SRV
    // descriptors.
    pub(in crate::directx) volume_resource: ID3D12Resource,
    pub(in crate::directx) volume_uav_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
    pub(in crate::directx) volume_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,

    // Heap GPU handle of the main-depth SRV. Bound at fog pass t0; the graph
    // declares the Fog pass's depth read, so the executor puts the resource in a
    // shader-resource state before this pass and restores DEPTH_WRITE at the end
    // of the frame.
    pub(in crate::directx) depth_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,

    // Heap GPU handle of the shadow map array SRV. Bound at the froxel
    // kernel's t0 so each slab can do a CSM tap. Shared with the rest of
    // the engine; the resource is transitioned to PIXEL_SHADER_RESOURCE
    // by `encode_shadow_pass` ahead of every later pass, including this one.
    pub(in crate::directx) shadow_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
}

// CPU + GPU descriptor handles for the froxel volume: the compute kernel's UAV
// (write) and the fog pass's SRV (read).
#[derive(Clone, Copy)]
pub(in crate::directx) struct FogVolumeDescriptors {
    pub uav_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
    pub uav_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
    pub srv_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
    pub srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
}

// GPU descriptor handles for the scene depth + shadow map the fog shaders sample.
#[derive(Clone, Copy)]
pub(in crate::directx) struct FogShaderResourceHandles {
    pub depth_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
    pub shadow_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
}

// Device-level fog build config: MSAA sample count (compiled into the depth SRV
// declarations) and the shader hot-reload toggle.
#[derive(Clone, Copy)]
pub(in crate::directx) struct FogDeviceParams {
    pub msaa_samples: u32,
    pub hot_reload: bool,
}

impl FogResources {
    // Build the fog pipeline + per-frame uniform rings + the froxel volume
    // + the compute kernel. Called from `DxContext::new` only when the
    // world declared a `VolumetricFog`. The depth SRV is already written
    // into the heap by the decal-init path (the projected-decal pass
    // writes the main-depth SRV unconditionally so runtime `add_decal`
    // works from a world that started empty); the fog pass reuses the
    // same descriptor. `volume.uav_cpu` / `volume.srv_cpu` are dedicated
    // SRV-heap slots reserved by init for the froxel volume.
    pub(in crate::directx) fn new(
        alloc: &DeviceAllocator,
        volume: FogVolumeDescriptors,
        shader_resources: FogShaderResourceHandles,
        params: FogDeviceParams,
        info_queue: Option<&ID3D12InfoQueue>,
    ) -> Result<Self, String> {
        let device = alloc.device();
        let FogVolumeDescriptors {
            uav_cpu: volume_uav_cpu,
            uav_gpu: volume_uav_gpu,
            srv_cpu: volume_srv_cpu,
            srv_gpu: volume_srv_gpu,
        } = volume;
        let FogShaderResourceHandles {
            depth_srv_gpu,
            shadow_srv_gpu,
        } = shader_resources;
        let FogDeviceParams {
            msaa_samples,
            hot_reload,
        } = params;
        let (vs, ps) = compile_fog_shaders(msaa_samples, hot_reload)?;
        let cs = compile_fog_froxel_shader(hot_reload)?;

        let root_sig = dump_on_err(info_queue, create_fog_root_signature(device))?;
        let pso = dump_on_err(info_queue, create_fog_pso(device, &root_sig, &vs, &ps))?;

        let froxel_root_sig = dump_on_err(info_queue, create_fog_froxel_root_signature(device))?;
        let froxel_pso = dump_on_err(
            info_queue,
            create_fog_froxel_pso(device, &froxel_root_sig, &cs),
        )?;

        let volume_resource = create_fog_froxel_volume(device, volume_uav_cpu, volume_srv_cpu)?;

        // Per-frame FogParams ring.
        let params_ubo_size = align256(std::mem::size_of::<FogParams>() 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_ubo_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 fog params ubo: {e}"))?;
            params_ubo_ptrs.push(ptr as *mut u8);
            params_ubo_resources.push(buf);
        }

        // Per-frame FogFroxelParams ring.
        let froxel_ubo_size = align256(std::mem::size_of::<FogFroxelParams>() as u64);
        let mut froxel_params_ubo_resources: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
        let mut froxel_params_ubo_ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
        for _ in 0..FRAMES {
            let buf = create_buffer(
                alloc,
                froxel_ubo_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 fog froxel params ubo: {e}"))?;
            froxel_params_ubo_ptrs.push(ptr as *mut u8);
            froxel_params_ubo_resources.push(buf);
        }

        Ok(Self {
            root_sig,
            pso,
            froxel_root_sig,
            froxel_pso,
            params_ubo_resources,
            params_ubo_ptrs,
            froxel_params_ubo_resources,
            froxel_params_ubo_ptrs,
            volume_resource,
            volume_uav_gpu,
            volume_srv_gpu,
            depth_srv_gpu,
            shadow_srv_gpu,
        })
    }
}

impl DxContext {
    // Hot-reload entry point for the volumetric-fog tunables. Writes the new
    // `Option<FogSettings>` into `self.fog.settings`; the next frame's
    // `encode_fog_froxel` + `encode_fog` re-derive `FogParams` /
    // `FogFroxelParams` from it (both bail to a no-op when it is `None`, so a
    // `None` here disables the pass). Mirrors `MtlContext::update_fog_settings`.
    //
    // If the world started with no `VolumetricFog` (so `self.fog.resources` is
    // `None` and the froxel + fog PSOs were never built), a `Some` update logs
    // once and is dropped: re-enabling fog mid-run requires a relaunch.
    //
    // The only caller is the bin-only `cn debug` world hot-reload
    // (`debug::hot_reload::passes`), reached through the `RenderBackend`
    // vtable. Mirrors the other bin-only runtime-mutation seams on this
    // backend.
    pub(crate) fn update_fog_settings(
        &mut self,
        settings: Option<crate::gfx::volumetric_fog::FogSettings>,
    ) {
        if settings.is_some() && self.fog.resources.is_none() {
            tracing::warn!(
                "VolumetricFog hot-reload: world started without fog, so the fog \
                 pipeline was never built: re-enabling fog mid-run is not \
                 supported (relaunch required). Ignoring update."
            );
            return;
        }
        self.fog.settings = settings;
    }

    // Compute the per-frame `FogFroxelParams` block. Mirrors the Metal
    // `draw::mod::record_frame` block: view matrix + volume dimensions +
    // near/far. `near` is the camera near-plane (clamped to ≥ 1e-3 so the
    // linear-Z mapping stays finite), and `z_far` is the fog's authored
    // `max_distance` (the volume covers `[z_near, max_distance]`).
    fn fog_froxel_params(&self, near: f32) -> Option<FogFroxelParams> {
        let fog = self.fog.settings?;
        Some(FogFroxelParams {
            view: self.view.matrix,
            froxel_dims: [FOG_FROXEL_X, FOG_FROXEL_Y, FOG_FROXEL_Z],
            _pad_align: 0,
            z_near: near.max(1e-3),
            z_far: fog.max_distance,
            _pad: [0.0; 2],
        })
    }

    // Encode the volumetric-fog froxel-volume compute pass. Populates the
    // 3D `(scattered, 1 - T)` volume the fog fragment shader samples. The
    // caller (`execute_graph`) seeds this PassId before `Fog` so the RAW
    // edge in the shared graph orders the dispatch correctly.
    pub(in crate::directx) fn encode_fog_froxel(
        &self,
        cmd: &ID3D12GraphicsCommandList,
        frame_idx: usize,
        near: f32,
        vp: [[f32; 4]; 4],
        cam_pos: [f32; 3],
        shadow_ubo_gva: u64,
    ) {
        let fog_settings = match &self.fog.settings {
            Some(s) => *s,
            None => return,
        };
        let fog = match &self.fog.resources {
            Some(f) => f,
            None => return,
        };
        let froxel_params = match self.fog_froxel_params(near) {
            Some(p) => p,
            None => return,
        };

        // Write per-frame `FogParams` + `FogFroxelParams` into their ring
        // slots. The `Fog` render pass below reads from the same slot, so
        // both passes see the same params this frame.
        let inv_vp = mat4_inverse(vp);
        let viewport = [
            self.extent.render_width as f32,
            self.extent.render_height as f32,
        ];
        let params = fog_settings.params(
            inv_vp,
            cam_pos,
            self.fog.sun_dir,
            self.fog.sun_color,
            viewport,
        );
        // 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(
                &params as *const FogParams as *const u8,
                fog.params_ubo_ptrs[frame_idx],
                std::mem::size_of::<FogParams>(),
            );
            std::ptr::copy_nonoverlapping(
                &froxel_params as *const FogFroxelParams as *const u8,
                fog.froxel_params_ubo_ptrs[frame_idx],
                std::mem::size_of::<FogFroxelParams>(),
            );
        }
        let params_gva = com::gpu_va(&fog.params_ubo_resources[frame_idx]);
        let froxel_params_gva = com::gpu_va(&fog.froxel_params_ubo_resources[frame_idx]);

        // The shadow map's cascade tap is a declared read of this pass, so the
        // Shadow consumer barrier's stage union already carries the non-pixel
        // shader-resource state the compute kernel needs. The volume stays in
        // `UNORDERED_ACCESS`; the graph's Fog consumer barrier is what returns it
        // to a shader-resource state for the render pass.

        // 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.SetComputeRootSignature(&fog.froxel_root_sig);
            cmd.SetPipelineState(&fog.froxel_pso);
            cmd.SetDescriptorHeaps(&[Some(self.descriptors.srv_heap.clone())]);
            cmd.SetComputeRootConstantBufferView(0, params_gva);
            cmd.SetComputeRootConstantBufferView(1, froxel_params_gva);
            cmd.SetComputeRootConstantBufferView(2, shadow_ubo_gva);
            cmd.SetComputeRootDescriptorTable(3, fog.shadow_srv_gpu);
            cmd.SetComputeRootDescriptorTable(4, fog.volume_uav_gpu);

            // 8×8 threadgroups, one thread per (x, y) froxel.
            let groups_x = FOG_FROXEL_X.div_ceil(8);
            let groups_y = FOG_FROXEL_Y.div_ceil(8);
            cmd.Dispatch(groups_x, groups_y, 1);
        }

        // The shadow map stays in the state the graph's read run put it in; the
        // executor's end-of-frame restore returns it to resting.
    }

    // Encode the volumetric-fog pass. Samples the 3D froxel volume the
    // `FogFroxel` compute pass populated this frame. Caller has already
    // ended the main HDR pass + the projected-decal pass (if any), so
    // `depth.resource` (MSAA when MSAA is on) holds the scene depth and
    // the resolved scene target holds the resolved scene + decal colour.
    // The pass alpha-blends `(scattered, 1 - T)` over the resolved HDR
    // target.
    pub(in crate::directx) fn encode_fog(
        &self,
        cmd: &ID3D12GraphicsCommandList,
        frame_idx: usize,
        _vp: [[f32; 4]; 4],
        _cam_pos: [f32; 3],
    ) {
        let _ = match &self.fog.settings {
            Some(s) => *s,
            None => return,
        };
        let fog = match &self.fog.resources {
            Some(f) => f,
            None => return,
        };

        // `FogParams` / `FogFroxelParams` were uploaded by `encode_fog_froxel`
        // for this frame's slot, so we only read their GVAs here.
        let params_gva = com::gpu_va(&fog.params_ubo_resources[frame_idx]);
        let froxel_params_gva = com::gpu_va(&fog.froxel_params_ubo_resources[frame_idx]);

        // Main depth and the froxel volume are both graph resources, so both
        // transitions are already in place: the executor emits this pass's depth
        // consumer barrier and the froxel volume's UNORDERED_ACCESS →
        // PIXEL_SHADER_RESOURCE close before 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.
        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_state = 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_state]);
            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.SetPipelineState(&fog.pso);
            cmd.SetGraphicsRootSignature(&fog.root_sig);
            cmd.SetDescriptorHeaps(&[Some(self.descriptors.srv_heap.clone())]);
            cmd.SetGraphicsRootConstantBufferView(0, params_gva);
            cmd.SetGraphicsRootConstantBufferView(1, froxel_params_gva);
            cmd.SetGraphicsRootDescriptorTable(2, fog.depth_srv_gpu);
            cmd.SetGraphicsRootDescriptorTable(3, fog.volume_srv_gpu);
            cmd.DrawInstanced(3, 1, 0, 0);
        }

        // The scene spine, main depth and the froxel volume are all graph
        // resources; nothing here is left to restore.
    }
}