Skip to main content

loam_app/session/
pacing.rs

1use std::time::Duration;
2
3use web_time::Instant;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum Pace {
7    Run,
8    Wait(Instant),
9}
10
11#[derive(Clone, Copy, Debug, Default)]
12pub struct Pacer {
13    period: Option<Duration>,
14    anchor: Option<Instant>,
15}
16
17impl Pacer {
18    pub fn set_target_fps(&mut self, fps: f32) {
19        self.period = (fps.is_finite() && fps > 0.0)
20            .then(|| Duration::from_nanos(((1_000_000_000.0 / f64::from(fps)) as u64).max(1)));
21    }
22
23    pub fn target_fps(&self) -> f32 {
24        self.period.map_or(0.0, |period| 1.0 / period.as_secs_f32())
25    }
26
27    pub fn target_period(&self) -> Option<Duration> {
28        self.period
29    }
30
31    pub fn reset(&mut self) {
32        self.anchor = None;
33    }
34
35    pub fn deadline(&self) -> Option<Instant> {
36        Some(self.anchor? + self.period?)
37    }
38
39    /// `Wait` until the deadline; a late wake anchors the next period on the deadline, not on itself.
40    pub fn decide(&mut self, now: Instant) -> Pace {
41        let Some(period) = self.period else {
42            self.anchor = None;
43            return Pace::Run;
44        };
45        let Some(anchor) = self.anchor else {
46            self.anchor = Some(now);
47            return Pace::Run;
48        };
49        let deadline = anchor + period;
50        if now < deadline {
51            return Pace::Wait(deadline);
52        }
53        let floor = now.checked_sub(period).unwrap_or(now);
54        self.anchor = Some(deadline.max(floor));
55        Pace::Run
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn a_capped_loop_anchors_on_the_deadline_it_waited_for_not_on_the_late_wake_up() {
65        let period = Duration::from_millis(10);
66        let mut pacer = Pacer::default();
67        pacer.set_target_fps(100.0);
68        let start = Instant::now();
69
70        assert_eq!(pacer.decide(start), Pace::Run);
71        assert_eq!(
72            pacer.decide(start + Duration::from_millis(3)),
73            Pace::Wait(start + period)
74        );
75        assert_eq!(pacer.decide(start + Duration::from_millis(12)), Pace::Run);
76        assert_eq!(
77            pacer.decide(start + Duration::from_millis(18)),
78            Pace::Wait(start + period * 2)
79        );
80
81        pacer.set_target_fps(0.0);
82        assert_eq!(pacer.decide(start + Duration::from_millis(18)), Pace::Run);
83    }
84}