Skip to main content

concinnity_render/directx/
pass_timing.rs

1//! Per-pass GPU timing on D3D12 via TIMESTAMP queries. The whole-frame timer
2//! lives in slots [0, 1] of each frame's block; one pair per `PassId` follows
3//! (start at slot 2 + 2*i, end at slot 3 + 2*i). The shared query heap is
4//! sized for `FRAMES` such blocks; `execute_graph` issues an `EndQuery`
5//! before and after each pass's `encode_*`, and the resolve at the end of
6//! the command list copies the whole block into the persistently-mapped
7//! readback buffer. The CPU reads the previous frame's block at the top of
8//! `draw_frame` (after the matching fence wait gates the GPU writes) and
9//! publishes the per-pass microseconds into `RenderStats.pass_times_us`.
10//!
11//! Layout reasoning. Keeping the whole-frame pair at the front of each
12//! block preserves the legacy `gpu_frame_us` indexing: the existing
13//! readback reads the first u64 pair of the frame's block, exactly as
14//! before; only the per-frame stride changes. SsaoPrepass / SsaoKernel /
15//! ParticlesSim are bundled inside their parent encoders (see
16//! graph_exec.rs) so the sub-pass slots stay zero; the FogFroxel /
17//! Upscale / Transparent / Raymarch arms are no-ops on DirectX so their
18//! slots also stay zero. The shared `StatHud.passes_text` helper picks
19//! the top six non-zero entries by descending microseconds, so zero
20//! slots are naturally filtered out of the on-screen chip.
21//!
22//! This is GPU-free slot-index arithmetic (no D3D12 types), so it lives in
23//! concinnity-render and its layout tests count toward coverage; the DirectX
24//! backend re-exports it under `crate::directx::pass_timing`.
25
26use crate::render_graph::{PASS_COUNT, PassId};
27
28/// Per-frame block: [whole_frame_start, whole_frame_end, pass0_start,
29/// pass0_end, ..., pass(PASS_COUNT-1)_start, pass(PASS_COUNT-1)_end].
30/// 2 * (PASS_COUNT + 1) u64 slots.
31pub const SLOTS_PER_FRAME: usize = 2 * (PASS_COUNT + 1);
32
33/// Bytes consumed by one frame's block in the readback buffer. Each slot
34/// is a u64 timestamp.
35pub const FRAME_BLOCK_BYTES: u64 = (SLOTS_PER_FRAME * 8) as u64;
36
37/// Slot indices for the whole-frame timestamp pair within the heap. Matches
38/// the legacy layout (whole-frame still at the first pair of each frame's
39/// block) so the existing `gpu_frame_us` readback only needs a stride
40/// adjustment.
41pub const fn whole_frame_pair(frame: usize) -> (u32, u32) {
42    let base = (frame * SLOTS_PER_FRAME) as u32;
43    (base, base + 1)
44}
45
46/// Slot indices for `pass`'s start + end timestamps within the heap.
47pub const fn pass_pair(frame: usize, pass: PassId) -> (u32, u32) {
48    let base = (frame * SLOTS_PER_FRAME + 2 + 2 * (pass as usize)) as u32;
49    (base, base + 1)
50}
51
52/// First heap slot the per-frame ResolveQueryData should walk. Pair this
53/// with `SLOTS_PER_FRAME` as the count.
54pub const fn frame_resolve_start(frame: usize) -> u32 {
55    (frame * SLOTS_PER_FRAME) as u32
56}
57
58/// Byte offset into the readback buffer where this frame's block begins.
59pub const fn frame_readback_byte_offset(frame: usize) -> u64 {
60    (frame * SLOTS_PER_FRAME * 8) as u64
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn layout_preserves_legacy_whole_frame_indexing() {
69        // Frame 0's whole-frame pair sits at slots 0,1: the legacy
70        // layout. The per-frame stride is now SLOTS_PER_FRAME, not 2.
71        assert_eq!(whole_frame_pair(0), (0, 1));
72        assert_eq!(
73            whole_frame_pair(1),
74            (SLOTS_PER_FRAME as u32, SLOTS_PER_FRAME as u32 + 1)
75        );
76        assert_eq!(frame_resolve_start(0), 0);
77        assert_eq!(frame_resolve_start(1), SLOTS_PER_FRAME as u32);
78        assert_eq!(frame_readback_byte_offset(0), 0);
79        assert_eq!(frame_readback_byte_offset(1), FRAME_BLOCK_BYTES);
80    }
81
82    #[test]
83    fn pass_pair_skips_the_whole_frame_pair() {
84        // First pass starts at slot 2 (offset by the whole-frame pair).
85        let (a, b) = pass_pair(0, PassId::Cull);
86        assert_eq!((a, b), (2, 3));
87    }
88
89    #[test]
90    fn pass_pairs_are_unique_within_a_frame() {
91        use hashbrown::HashSet;
92        let mut seen: HashSet<u32> = HashSet::new();
93        // The whole-frame pair owns slots 0, 1.
94        seen.insert(0);
95        seen.insert(1);
96        for variant in [
97            PassId::Cull,
98            PassId::Shadow,
99            PassId::SsrPrepass,
100            PassId::SsaoPrepass,
101            PassId::SsaoKernel,
102            PassId::SsaoBlur,
103            PassId::Main,
104            PassId::AutoExposure,
105            PassId::Decals,
106            PassId::Fog,
107            PassId::ParticlesSim,
108            PassId::ParticlesDraw,
109            PassId::SsrResolve,
110            PassId::Velocity,
111            PassId::TaaResolve,
112            PassId::Bloom,
113            PassId::Composite,
114            PassId::FogFroxel,
115            PassId::Upscale,
116            PassId::Transparent,
117            PassId::Raymarch,
118        ] {
119            let (s, e) = pass_pair(0, variant);
120            assert!(seen.insert(s), "duplicate start slot for {variant:?}");
121            assert!(seen.insert(e), "duplicate end slot for {variant:?}");
122            assert!((s as usize) < SLOTS_PER_FRAME);
123            assert!((e as usize) < SLOTS_PER_FRAME);
124        }
125    }
126}