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}