concinnity_render/vulkan/pass_timing.rs
1//! Per-pass GPU timing on Vulkan via TIMESTAMP queries. The query pool holds one
2//! per-frame block of `SLOTS_PER_FRAME` slots; the whole-frame timer lives in
3//! slots [0, 1] of each block and one (start, end) pair per `PassId` follows
4//! (start at slot 2 + 2*i, end at slot 3 + 2*i). Mirrors directx/pass_timing.rs.
5//!
6//! The start buffer resets the whole block and writes the whole-frame start; each
7//! per-pass command buffer writes its own (start, end) pair around its encode;
8//! the end buffer writes the whole-frame end. The CPU reads the previous trip's
9//! block at the top of `draw_frame` (after the matching fence wait gates the GPU
10//! writes) and publishes the per-pass microseconds into `RenderStats.pass_times_us`.
11//!
12//! Vulkan note. Unlike D3D12 (which can pre-write every slot so a pass that did
13//! not run still reads a value), Vulkan forbids writing a timestamp to a query
14//! that is already written without an intervening reset. So a pass absent from
15//! this frame's graph leaves its (reset-but-unwritten) slots `unavailable`; the
16//! readback uses `WITH_AVAILABILITY` and reports 0 for any slot whose pair is not
17//! both available. The shared `StatHud.passes_text` then filters the zero slots.
18//!
19//! Layout reasoning. Keeping the whole-frame pair at the front of each block lets
20//! the existing `gpu_frame_us` readback stay the first pair of the frame's block;
21//! only the per-frame stride changes (from 2 to `SLOTS_PER_FRAME`).
22//!
23//! This is GPU-free slot-index arithmetic (no ash/vk types), so it lives in
24//! concinnity-render and its layout tests count toward coverage; the Vulkan
25//! backend re-exports it under `crate::vulkan::pass_timing`.
26
27use crate::render_graph::{PASS_COUNT, PassId};
28
29/// Per-frame block: [whole_frame_start, whole_frame_end, pass0_start, pass0_end,
30/// ..., pass(PASS_COUNT-1)_start, pass(PASS_COUNT-1)_end]. 2 * (PASS_COUNT + 1)
31/// u64 query slots.
32pub const SLOTS_PER_FRAME: usize = 2 * (PASS_COUNT + 1);
33
34/// First query slot of frame `frame`'s block.
35pub const fn frame_block_base(frame: usize) -> u32 {
36 (frame * SLOTS_PER_FRAME) as u32
37}
38
39/// (start, end) query slots for the whole-frame pair of `frame`. Matches the
40/// legacy layout (whole-frame at the first pair of each block).
41pub const fn whole_frame_pair(frame: usize) -> (u32, u32) {
42 let base = frame_block_base(frame);
43 (base, base + 1)
44}
45
46/// (start, end) query slots for `pass` within `frame`'s block.
47pub const fn pass_pair(frame: usize, pass: PassId) -> (u32, u32) {
48 let base = frame_block_base(frame) + 2 + 2 * (pass as u32);
49 (base, base + 1)
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn layout_preserves_legacy_whole_frame_indexing() {
58 // Frame 0's whole-frame pair sits at slots 0,1 (the legacy layout); the
59 // per-frame stride is now SLOTS_PER_FRAME, not 2.
60 assert_eq!(whole_frame_pair(0), (0, 1));
61 assert_eq!(
62 whole_frame_pair(1),
63 (SLOTS_PER_FRAME as u32, SLOTS_PER_FRAME as u32 + 1)
64 );
65 assert_eq!(frame_block_base(0), 0);
66 assert_eq!(frame_block_base(2), 2 * SLOTS_PER_FRAME as u32);
67 }
68
69 #[test]
70 fn pass_pair_skips_the_whole_frame_pair() {
71 // The first pass starts at slot 2 (offset past the whole-frame pair).
72 assert_eq!(pass_pair(0, PassId::Cull), (2, 3));
73 }
74
75 #[test]
76 fn pass_pairs_are_unique_within_a_frame() {
77 use hashbrown::HashSet;
78 let mut seen: HashSet<u32> = HashSet::new();
79 // The whole-frame pair owns slots 0, 1.
80 seen.insert(0);
81 seen.insert(1);
82 for variant in [
83 PassId::Cull,
84 PassId::Shadow,
85 PassId::SsrPrepass,
86 PassId::SsaoPrepass,
87 PassId::SsaoKernel,
88 PassId::SsaoBlur,
89 PassId::Main,
90 PassId::AutoExposure,
91 PassId::Decals,
92 PassId::Fog,
93 PassId::ParticlesSim,
94 PassId::ParticlesDraw,
95 PassId::SsrResolve,
96 PassId::Velocity,
97 PassId::TaaResolve,
98 PassId::Bloom,
99 PassId::Composite,
100 PassId::FogFroxel,
101 PassId::Upscale,
102 PassId::Transparent,
103 PassId::Raymarch,
104 PassId::HizBuild,
105 PassId::Cull2,
106 PassId::Main2,
107 PassId::Ssgi,
108 PassId::RtReflections,
109 PassId::GBufferPrepass,
110 PassId::ReflectionComposite,
111 PassId::LightCull,
112 PassId::SpotShadow,
113 PassId::Lines,
114 PassId::HizFinal,
115 ] {
116 let (s, e) = pass_pair(0, variant);
117 assert!(seen.insert(s), "duplicate start slot for {variant:?}");
118 assert!(seen.insert(e), "duplicate end slot for {variant:?}");
119 assert!((e as usize) < SLOTS_PER_FRAME);
120 }
121 // Every slot of the block is accounted for (whole-frame pair + one pair
122 // per pass).
123 assert_eq!(seen.len(), SLOTS_PER_FRAME);
124 }
125}