Skip to main content

concinnity_core/render/
pass_timing.rs

1//! Per-pass GPU timing slot arithmetic, shared by every backend that times
2//! passes with a timestamp query pool.
3//!
4//! The pool holds one block of [`SLOTS_PER_FRAME`] u64 slots per in-flight
5//! frame. Within a block the whole-frame timer owns slots [0, 1] and one
6//! (start, end) pair per [`PassId`] follows, so `pass` occupies
7//! `2 + 2 * pass` and `3 + 2 * pass`. Keeping the whole-frame pair at the front
8//! lets a whole-frame readback stay the first pair of the block, independent of
9//! how many passes exist.
10//!
11//! This is index arithmetic with no device type in it, so it lives here rather
12//! than in a backend and its layout tests run on every platform's CI. The
13//! DirectX and Vulkan backends re-export it under their own `pass_timing`.
14
15use crate::render::render_graph::{PASS_COUNT, PassId};
16
17/// Per-frame block: `[whole_frame_start, whole_frame_end, pass0_start,
18/// pass0_end, ..., pass(PASS_COUNT-1)_end]`.
19pub const SLOTS_PER_FRAME: usize = 2 * (PASS_COUNT + 1);
20
21/// Bytes one frame's block occupies in a readback buffer of u64 timestamps.
22pub const FRAME_BLOCK_BYTES: u64 = (SLOTS_PER_FRAME * 8) as u64;
23
24/// First slot of `frame`'s block. Also the start of the range a per-frame
25/// resolve should walk, paired with [`SLOTS_PER_FRAME`] as the count.
26pub const fn frame_block_base(frame: usize) -> u32 {
27    (frame * SLOTS_PER_FRAME) as u32
28}
29
30/// Byte offset into the readback buffer where `frame`'s block begins.
31pub const fn frame_readback_byte_offset(frame: usize) -> u64 {
32    frame as u64 * FRAME_BLOCK_BYTES
33}
34
35/// (start, end) slots for `frame`'s whole-frame pair.
36pub const fn whole_frame_pair(frame: usize) -> (u32, u32) {
37    let base = frame_block_base(frame);
38    (base, base + 1)
39}
40
41/// (start, end) slots for `pass` within `frame`'s block.
42pub const fn pass_pair(frame: usize, pass: PassId) -> (u32, u32) {
43    let base = frame_block_base(frame) + 2 + 2 * (pass as u32);
44    (base, base + 1)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn whole_frame_pair_heads_each_block() {
53        assert_eq!(whole_frame_pair(0), (0, 1));
54        assert_eq!(
55            whole_frame_pair(1),
56            (SLOTS_PER_FRAME as u32, SLOTS_PER_FRAME as u32 + 1)
57        );
58        assert_eq!(frame_block_base(0), 0);
59        assert_eq!(frame_block_base(2), 2 * SLOTS_PER_FRAME as u32);
60        assert_eq!(frame_readback_byte_offset(0), 0);
61        assert_eq!(frame_readback_byte_offset(1), FRAME_BLOCK_BYTES);
62    }
63
64    #[test]
65    fn pass_pair_skips_the_whole_frame_pair() {
66        assert_eq!(pass_pair(0, PassId::Cull), (2, 3));
67    }
68
69    #[test]
70    fn every_pass_owns_a_distinct_pair_inside_the_block() {
71        // Driven by `PassId::ALL`, so a new pass is covered here the moment it
72        // is registered rather than when someone remembers to extend a list.
73        let mut seen = hashbrown::HashSet::new();
74        // The whole-frame pair owns slots 0, 1.
75        assert!(seen.insert(0) && seen.insert(1));
76        for pass in PassId::ALL {
77            let (s, e) = pass_pair(0, pass);
78            assert!(seen.insert(s), "duplicate start slot for {pass:?}");
79            assert!(seen.insert(e), "duplicate end slot for {pass:?}");
80            assert!((e as usize) < SLOTS_PER_FRAME);
81        }
82        // The block is exactly the whole-frame pair plus one pair per pass.
83        assert_eq!(seen.len(), SLOTS_PER_FRAME);
84    }
85}