Skip to main content

alpha2_gpu/
rate.rs

1//! Device throughput in milliseconds per unit of effect cost per pixel.
2//!
3//! Renderers split frames into submissions under [`crate::submission_budget_ms`], predicting
4//! each pass as cost times pixels times this rate. A calibration draw seeds it at twice the
5//! measurement; heavy frames then lower it by at most a tenth per frame, never below the
6//! calibration.
7//!
8//! Frame GPU time comes from the completion callback, which fires at the next poll, so it is
9//! only an upper bound. Measurements can therefore lower the rate but never raise it. The rate
10//! affects only submission splitting, never pixels.
11
12use std::sync::Mutex;
13
14/// The rate assumed before calibration, in milliseconds per unit pixel: twice the slowest
15/// measured rate (integrated GPUs), so early frames are split too finely rather than too
16/// coarsely.
17pub const UNCALIBRATED_MS_PER_UNIT_PIXEL: f64 = 2.0e-8;
18
19#[derive(Debug, Default)]
20struct RateState {
21    ms_per_unit_pixel: Option<f64>,
22    /// The calibration measurement, which is the floor of the rate.
23    calibrated: Option<f64>,
24    calibrating: bool,
25}
26
27/// One device's rate, shared by every context clone and renderer on it.
28#[derive(Debug, Default)]
29pub struct GpuRate {
30    state: Mutex<RateState>,
31}
32
33impl GpuRate {
34    fn state(&self) -> std::sync::MutexGuard<'_, RateState> {
35        self.state
36            .lock()
37            .unwrap_or_else(std::sync::PoisonError::into_inner)
38    }
39
40    /// The rate in milliseconds per unit pixel, once seeded.
41    #[must_use]
42    pub fn ms_per_unit_pixel(&self) -> Option<f64> {
43        self.state().ms_per_unit_pixel
44    }
45
46    /// Returns true once, telling the first caller before seeding to run the calibration draw.
47    pub fn begin_calibration(&self) -> bool {
48        let mut state = self.state();
49        if state.ms_per_unit_pixel.is_some() || state.calibrating {
50            return false;
51        }
52        state.calibrating = true;
53        true
54    }
55
56    /// Seeds the rate at twice `measured_ms_per_unit_pixel`, allowing for a cold device.
57    pub fn seed(&self, measured_ms_per_unit_pixel: f64) {
58        let mut state = self.state();
59        state.calibrating = false;
60        if !(measured_ms_per_unit_pixel.is_finite() && measured_ms_per_unit_pixel > 0.0) {
61            return;
62        }
63        state.calibrated = Some(measured_ms_per_unit_pixel);
64        let seeded = 2.0 * measured_ms_per_unit_pixel;
65        state.ms_per_unit_pixel = Some(
66            state
67                .ms_per_unit_pixel
68                .map_or(seeded, |rate| rate.max(seeded)),
69        );
70    }
71
72    /// Allows a later caller to calibrate again after a failed draw.
73    pub fn abandon_calibration(&self) {
74        self.state().calibrating = false;
75    }
76
77    /// Folds in a frame of `unit_pixels` predicted cost that took at most `measured_ms`. A
78    /// higher rate drops by at most a tenth, never below the calibration.
79    pub fn observe(&self, measured_ms: f64, unit_pixels: f64) {
80        if !(measured_ms.is_finite() && unit_pixels.is_finite() && unit_pixels > 0.0) {
81            return;
82        }
83        let measured = measured_ms / unit_pixels;
84        let mut state = self.state();
85        let (Some(rate), Some(floor)) = (state.ms_per_unit_pixel, state.calibrated) else {
86            return;
87        };
88        if measured < rate {
89            state.ms_per_unit_pixel = Some((rate * 0.9).max(measured).max(floor));
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::GpuRate;
97
98    #[test]
99    fn the_rate_falls_slowly_to_the_calibration_and_never_rises_on_a_measurement() {
100        let rate = GpuRate::default();
101        assert_eq!(rate.ms_per_unit_pixel(), None);
102        rate.observe(10.0, 1.0);
103        assert_eq!(
104            rate.ms_per_unit_pixel(),
105            None,
106            "nothing to fold into before a seed"
107        );
108        assert!(rate.begin_calibration());
109        assert!(!rate.begin_calibration(), "one calibration at a time");
110        rate.seed(1.0);
111        assert_eq!(rate.ms_per_unit_pixel(), Some(2.0));
112        // A late completion overstates GPU time, so it never raises the rate.
113        rate.observe(8.0, 2.0);
114        assert_eq!(rate.ms_per_unit_pixel(), Some(2.0));
115        rate.observe(0.0, 2.0);
116        assert_eq!(rate.ms_per_unit_pixel(), Some(1.8));
117        for _ in 0..100 {
118            rate.observe(0.0, 2.0);
119        }
120        assert_eq!(rate.ms_per_unit_pixel(), Some(1.0));
121        assert!(!rate.begin_calibration());
122    }
123}