loam_app/session/
pacing.rs1use 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 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}