Skip to main content

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}