concinnity-device 0.18.69

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
// src/directx/resize.rs
//
// D3D12 swapchain + render-target resize handler. Polls `win_state.width/height`
// at the top of each frame and, when the dimensions diverged from the live
// render-target sizing, rebuilds every render-resolution-sized GPU resource and
// rewrites the descriptors that point at them. The descriptor *slots* never move
// (only the resources they point at) so the live root signatures + pipelines +
// pre-bound GPU descriptor handles keep working without a re-bind.
//
// Mirrors the Vulkan `rebuild_swapchain` flow in src/vulkan/swapchain.rs: a
// `wait_idle` gate, a wholesale drop + recreate, then per-effect resource
// rebuilds (TAA / SSAO / SSR / bloom).
//
// Bloom mip count is held fixed at init's value rather than recomputed at the
// new resolution. `bloom_mip_count` only changes for very small windows (<128
// pixels in the smaller dimension), and keeping the count stable keeps the
// SRV/RTV heap layout stable so everything past the bloom block stays at its
// originally-allocated slot.

use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::*;

use crate::directx::context::{DxContext, FRAMES};
use crate::directx::post::bloom::{create_bloom_mips_at, write_color_rtv};
use crate::directx::texture::{
    HDR_FORMAT, create_hdr_color_target, create_hdr_resolve_target, create_main_depth_texture,
    write_hdr_srv,
};

impl DxContext {
    // Poll the window state and, if the client area resized since the last
    // `draw_frame`, rebuild every render-target-sized GPU resource. Called at
    // the top of `draw_frame` before any rendering happens. Returns `Ok(())`
    // when no work was needed, when the work succeeded, or when the window is
    // minimised (one or both dimensions zero: we just skip the frame's
    // resize cycle and leave the targets at their previous size; the next
    // non-zero size restores them).
    pub(super) fn maybe_handle_resize(&mut self) -> Result<(), String> {
        let new_w = self.win().width.max(0) as u32;
        let new_h = self.win().height.max(0) as u32;
        if new_w == 0 || new_h == 0 {
            return Ok(());
        }
        // Compare against the *drawable* dims, not the render dims; with
        // temporal upscaling on, `extent.render_width`/`extent.render_height` are a
        // fraction of the window size and would never equal it, firing a
        // pointless rebuild every frame.
        if new_w == self.extent.output_width && new_h == self.extent.output_height {
            return Ok(());
        }
        self.handle_resize(new_w, new_h)
    }

    // (Re)acquire the swapchain back buffers and write their RTVs into the
    // pre-reserved RTV heap slots. Used by the resize path on both success (the
    // freshly-sized buffers) and failure (the unchanged old buffers), so a
    // failed `ResizeBuffers` never leaves `swapchain.back_buffers` empty.
    fn populate_back_buffers(&mut self) -> Result<(), String> {
        self.swapchain.back_buffers.clear();
        // SAFETY: a property query on a live descriptor heap; it only reads.
        let rtv_base = unsafe { self.swapchain.rtv_heap.GetCPUDescriptorHandleForHeapStart() };
        for i in 0..FRAMES {
            // SAFETY: a query on a live COM object; the descriptor it reads and the out-parameters
            // it fills are live locals that outlive the call.
            let buf: ID3D12Resource = unsafe { self.swapchain.handle.GetBuffer(i as u32) }
                .map_err(|e| format!("GetBuffer[{i}]: {e}"))?;
            let rtv_handle = D3D12_CPU_DESCRIPTOR_HANDLE {
                ptr: rtv_base.ptr + i * self.swapchain.rtv_descriptor_size,
            };
            // 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 { self.device.CreateRenderTargetView(&buf, None, rtv_handle) };
            self.swapchain.back_buffers.push(buf);
        }
        Ok(())
    }

    // Wholesale resize. Caller has already validated `new_w` / `new_h` are
    // non-zero and differ from the live size. The flow mirrors the Vulkan
    // rebuild: `wait_idle`, drop the old resources, recreate at the new
    // resolution, rewrite every dependent SRV/RTV/DSV at its existing heap
    // slot, and refresh `extent.render_width` / `extent.render_height`.
    fn handle_resize(&mut self, new_w: u32, new_h: u32) -> Result<(), String> {
        self.wait_idle();

        // 0) Temporal upscaler. `new_w`/`new_h` are the new drawable dims;
        //    rebuild the FFX context for them (its `max_render`/`max_upscale`
        //    sizes are baked at creation, so a resize needs a fresh context)
        //    and recompute the off-screen scene render dims from its quality
        //    scale. The output texture is recreated at the new drawable size
        //    and its UAV/SRV are rewritten into the same pre-reserved heap
        //    slots, so the live `scene_srv_for_post` binding stays valid.
        //    A failed rebuild degrades to native-resolution rendering (render
        //    == output). Everything downstream sizes off `render_w`/`render_h`.
        let (render_w, render_h) = if let Some(old) = self.upscale.backend.as_ref() {
            let scale = old.upscale_scale();
            let (uav, srv_cpu, srv_gpu) = old.output_descriptors();
            let backend = self.upscale.requested;
            // Drop the old context before building the replacement (its
            // max_render / max_upscale sizes are baked at creation).
            self.upscale.backend = None;
            let rebuilt = crate::directx::post::upscale::build_upscaler(
                &self.device,
                &self.command_queue,
                new_w,
                new_h,
                scale,
                crate::directx::post::upscale::UpscalerDescriptors {
                    uav_cpu: uav,
                    srv_cpu,
                    srv_gpu,
                },
                backend,
            )?
            .0;
            let dims = match &rebuilt {
                Some(u) => u.render_dims(),
                None => (new_w, new_h),
            };
            self.upscale.backend = rebuilt;
            dims
        } else {
            (new_w, new_h)
        };

        // 1) Swapchain back-buffers. `ResizeBuffers` requires every reference to
        //    the back-buffer resources to be released first. Besides the
        //    `swapchain.back_buffers` Vec, the composite pass records each back buffer onto
        //    its slot's end command list, and those recorded references persist
        //    until the list is reset. `maybe_handle_resize` runs at the top of
        //    `draw_frame` before this frame's lists are reset, so every in-flight
        //    slot's end list still pins a back buffer; reset + close them here
        //    (the GPU is already drained by `wait_idle`) so no reference outlives
        //    the clear. Without this, `ResizeBuffers` fails with
        //    DXGI_ERROR_INVALID_CALL and the window can never be resized.
        for i in 0..FRAMES {
            // SAFETY: the fence for this frame slot was already waited on, so no submission still
            // references what is being reset.
            unsafe {
                let _ = self.commands.end_command_allocators[i].Reset();
                if self.commands.end_command_lists[i]
                    .Reset(&self.commands.end_command_allocators[i], None)
                    .is_ok()
                {
                    let _ = self.commands.end_command_lists[i].Close();
                }
            }
        }
        self.swapchain.back_buffers.clear();
        // ResizeBuffers must be passed the same flags the swapchain was created
        // with, so an ALLOW_TEARING (uncapped) swapchain keeps the flag here.
        let resize_flags = if self.swapchain.allow_tearing {
            DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING
        } else {
            DXGI_SWAP_CHAIN_FLAG(0)
        };
        // SAFETY: the swapchain is live, every back-buffer reference this context held was dropped
        // above, and the call takes only scalars besides the format and flags.
        if let Err(e) = unsafe {
            self.swapchain.handle.ResizeBuffers(
                FRAMES as u32,
                new_w,
                new_h,
                self.swapchain.format,
                resize_flags,
            )
        } {
            // The resize failed; `ResizeBuffers` leaves the swapchain at its
            // current size, so re-acquire the existing back buffers + RTVs. This
            // keeps the renderer presenting at the old size instead of indexing
            // an empty `swapchain.back_buffers` (a panic) on the next frame; the resize
            // poll retries on the following frame.
            self.populate_back_buffers()?;
            return Err(format!("ResizeBuffers: {e}"));
        }
        self.populate_back_buffers()?;
        // The recreated back buffers invalidate any previously captured present
        // index; a `screenshot` before the next present then returns a clean
        // error instead of reading a stale buffer.
        self.swapchain.last_present_index = None;

        // 2) Main HDR colour + (optional) HDR resolve + depth. The HDR scene
        //    SRV (`hdr_srv_gpu`) and the decal/fog main-depth SRV
        //    (`decal_depth_srv_gpu` on `DecalResources` / `FogResources`) are
        //    rewritten into their existing heap slots, so the consumers don't
        //    need a re-bind.
        self.hdr.color = create_hdr_color_target(
            &self.device,
            render_w,
            render_h,
            self.hdr.msaa_samples,
            self.hdr.color_rtv,
            self.view.clear_color,
        )?;
        if self.hdr.msaa_samples > 1 {
            let resolve = create_hdr_resolve_target(&self.device, render_w, render_h)?;
            // Rewrite the resolve target's RTV at the slot the decal pass
            // already binds. The CPU handle was captured at init when MSAA
            // turned the slot on; it stays valid across resize.
            if let Some(rtv) = self.hdr.resolve_rtv {
                let rtv_desc = D3D12_RENDER_TARGET_VIEW_DESC {
                    Format: HDR_FORMAT,
                    ViewDimension: D3D12_RTV_DIMENSION_TEXTURE2D,
                    ..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 {
                    self.device
                        .CreateRenderTargetView(&resolve, Some(&rtv_desc), rtv)
                };
            }
            self.hdr.resolve = Some(resolve);
        }
        // Recreate main depth (shader-readable so the decal/fog/auto-exposure
        // paths can sample it). The DSV is rewritten at the same slot.
        self.depth.resource = create_main_depth_texture(
            &self.device,
            render_w,
            render_h,
            self.depth.dsv,
            self.hdr.msaa_samples,
            true,
        )?;

        // Cache the SRV-heap CPU/GPU bases so the per-resource SRV rewrites
        // below can derive the CPU handle from each stored GPU handle without
        // borrowing `self` again (the per-effect rebuilds need `&mut self`).
        // SAFETY: a property query on a live descriptor heap; it only reads.
        let srv_cpu_base = unsafe {
            self.descriptors
                .srv_heap
                .GetCPUDescriptorHandleForHeapStart()
        };
        // SAFETY: a property query on a live descriptor heap; it only reads.
        let srv_gpu_base = unsafe {
            self.descriptors
                .srv_heap
                .GetGPUDescriptorHandleForHeapStart()
        };
        let srv_cpu_of = |gpu: D3D12_GPU_DESCRIPTOR_HANDLE| D3D12_CPU_DESCRIPTOR_HANDLE {
            ptr: srv_cpu_base.ptr + (gpu.ptr - srv_gpu_base.ptr) as usize,
        };

        // 3) Refresh SRVs that point at the recreated resources. The GPU
        //    handles stored on the various Resources structs already match
        //    these heap slots; we just rewrite the descriptors in place.
        write_hdr_srv(
            &self.device,
            self.hdr.resolve.as_ref().unwrap_or(&self.hdr.color),
            srv_cpu_of(self.hdr.srv_gpu),
        );

        // The main-depth SRV is shared by the decal pass and the fog pass.
        // Both store the same `depth_srv_gpu`; rewrite the descriptor once
        // and both consumers pick it up.
        if let Some(decals) = self.decal.state.as_ref() {
            crate::directx::decal::write_main_depth_srv(
                &self.device,
                &self.depth.resource,
                srv_cpu_of(decals.depth_srv_gpu),
                self.hdr.msaa_samples,
            );
        }

        // Rebuild the transient pool (`bloom_top` + `ao_output`) at the new
        // resolution up front, before the consumers below read it back: the
        // bloom chain takes its pooled `mips[0]` and SSAO re-points its
        // `ao_output` RTV/SRV from it. The device is idle at the top of resize,
        // so dropping the old placed resources + heaps is sound.
        let ssao_on = self.ssao.resources.is_some();
        let gbuffer_on = self.gbuffer.is_some();
        self.transient_pool.rebuild(
            &self.device,
            &self.command_queue,
            &super::transient_pool::transient_slots(
                ssao_on,
                gbuffer_on,
                (render_w, render_h),
                (new_w, new_h),
            )?,
        )?;

        // 4) Bloom mip chain. Keep the count fixed at the init-time value
        //    (`self.bloom.mips.len()`) so the heap layout past the bloom
        //    block (LUT, TAA SRVs, SSAO SRVs, ...) stays anchored. `mips[0]`
        //    (`bloom_top`) is the pooled placed resource; the finer mips are
        //    committed.
        let bloom_count = self.bloom.mips.len();
        if bloom_count > 0 {
            let bloom_top = self
                .transient_pool
                .resource_for("bloom_top")
                .ok_or("transient pool missing bloom_top on resize")?
                .clone();
            let new_mips =
                create_bloom_mips_at(&self.device, new_w, new_h, bloom_count, bloom_top)?;
            self.bloom.mips = new_mips.0;
            self.bloom.mip_extents = new_mips.1;
            // Rewrite each mip's RTV + SRV into the existing slots.
            for i in 0..bloom_count {
                write_color_rtv(&self.device, &self.bloom.mips[i], self.bloom.mip_rtvs[i]);
                write_hdr_srv(
                    &self.device,
                    &self.bloom.mips[i],
                    srv_cpu_of(self.bloom.mip_srv_gpus[i]),
                );
            }
        }

        // 5) TAA: velocity + private depth + ping-pong history. Rebuild
        //    resources at the new size; the `frame` counter resets so the
        //    resolve pass treats the next frame as the first-after-resize
        //    (history is unreliable across a resize: the reprojection
        //    coordinates were generated at the old resolution).
        if let Some(taa) = self.taa.as_mut() {
            taa.resize_to(&self.device, render_w, render_h, srv_cpu_base, srv_gpu_base)?;
        }

        // 6) SSAO: pre-pass G-buffer + private depth + raw/blurred AO. The
        // blurred `ao_output` is pooled and was rebuilt above; SSAO rewrites its
        // RTV + SRV from the new pooled resource.
        if let Some(ao_resource) = self.transient_pool.resource_for("ao_output").cloned()
            && let Some(ssao) = self.ssao.resources.as_mut()
        {
            ssao.resize_to(
                &self.device,
                render_w,
                render_h,
                srv_cpu_base,
                srv_gpu_base,
                &ao_resource,
            )?;
        }

        // 7) SSR: pre-pass G-buffer + roughness + private depth + resolve output.
        if let Some(ssr) = self.ssr.as_mut() {
            ssr.resize_to(&self.device, render_w, render_h, srv_cpu_base, srv_gpu_base)?;
        }

        // 7-gbuffer) Unified G-buffer pre-pass: the three colour targets are
        // pooled and were relocated by the rebuild above, so this re-points
        // their views; the private depth is feature-owned and recreated.
        if let Some(pooled) = self.transient_pool.gbuffer_pooled()
            && let Some(gbuffer) = self.gbuffer.as_mut()
        {
            gbuffer.resize_to(
                &self.device,
                render_w,
                render_h,
                srv_cpu_base,
                srv_gpu_base,
                &pooled,
            )?;
        }

        // 7-ssgi) SSGI gather target. Re-uses its pre-reserved RTV/SRV slots;
        // the live pass binding (which points at the SRV slot's GPU handle)
        // stays valid after the in-place descriptor rewrite.
        if let Some(ssgi) = self.ssgi.as_mut() {
            ssgi.resize_to(&self.device, render_w, render_h, srv_cpu_base, srv_gpu_base)?;
        }

        // 7-rt) RT reflections output target. Re-uses its pre-reserved RTV/SRV
        //     slots; the acceleration structure is resolution-independent and is
        //     left untouched.
        if let Some(rt) = self.rt_reflections.as_mut() {
            rt.resize_to(&self.device, render_w, render_h, srv_cpu_base, srv_gpu_base)?;
        }

        // 7-refl) Reflection composite: the full-res composited output + the
        //     reduced-res roughness blur. Re-uses its pre-reserved RTV/SRV slots,
        //     so the live scene binding (which points at the output SRV slot) stays
        //     valid after the in-place descriptor rewrite.
        if let Some(rc) = self.reflection_composite.as_mut() {
            rc.resize_to(&self.device, render_w, render_h, srv_cpu_base, srv_gpu_base)?;
        }

        // 7a) Raymarch: recreate the `hdr_resolve_copy` scene snapshot at
        //     the new dims and rewrite its SRV descriptor in place. The
        //     descriptor slot itself doesn't move, so the live raymarch
        //     root-table binding stays valid without a re-bind.
        if let Some(rm) = self.raymarch.as_mut() {
            rm.resize_to(&self.device, render_w, render_h)?;
        }

        // 7b) Hi-Z (depth-mip pyramid). Resource sized to the depth buffer; the
        //     mip count adapts to the new dimensions. Re-uses the pre-reserved
        //     SRV / UAV heap slots; the live cull binding (which points at
        //     the SRV slot's GPU handle) stays valid. The pyramid is invalid
        //     until the next frame rebuilds it, so flip `hiz_valid` back to
        //     false so the next cull dispatch skips the occlusion test
        //     (NDC coords from the old resolution would mis-sample the new
        //     mip dimensions otherwise).
        if let Some(hiz) = self.cull.hiz.as_mut() {
            hiz.resize_to(&self.device, render_w, render_h)?;
        }
        self.cull.hiz_valid.set(false);

        // 7c) The transparent pass: recreate the scene snapshot at the new dims
        //     and rewrite its SRV in place. The depth SRV the pass also binds is
        //     the main-depth slot, rewritten by the decal path.
        if let Some(transparent) = self.transparent.as_mut() {
            transparent.resize_to(&self.device, render_w, render_h)?;
        }

        // 7d) Planar reflections: recreate the shared mirror colour + depth + the
        //     per-plane resolves at the new render dims and rewrite their RTV / DSV /
        //     SRVs in place, so each reflector's resolve binding stays valid.
        if let Some(planar) = self.planar_reflection.as_mut() {
            planar.resize_to(&self.device, render_w, render_h)?;
        }

        // 8) Commit the new dimensions. `render_*` drives the scene-pass
        //    viewports + the sub-pixel jitter; `output_*` drives the
        //    composite viewport and is what the next resize poll compares
        //    against. They differ only while temporal upscaling is active.
        self.extent.render_width = render_w;
        self.extent.render_height = render_h;
        self.extent.output_width = new_w;
        self.extent.output_height = new_h;

        // 9) Reset the swapchain back-buffer index. After `ResizeBuffers` the
        //    swapchain's notion of "current back buffer" is the next one to be
        //    presented; we don't need to mirror that here because
        //    `current_frame` indexes our internal frames-in-flight ring (not
        //    the swapchain), and `GetCurrentBackBufferIndex` is queried fresh
        //    each frame inside `draw_frame`.

        Ok(())
    }
}