Skip to main content

blockworx_web/
pacing.rs

1//! When the next frame runs.
2//!
3//! A frame is booked by something happening — an input, a resize, a chrome
4//! press — or by the kernel asking for one. Nothing else runs a frame, and at
5//! most one booking stands at a time, so a burst of pointer moves inside one
6//! animation frame paints once. The rules are a state machine over plain
7//! values with no browser in them; the shell performs what they answer.
8
9use core::time::Duration;
10
11/// Which booking a timer callback belongs to. A timer that fires after
12/// something sooner has already booked the frame is stale, and a ticket is
13/// how it finds that out.
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
15pub struct Ticket(u64);
16
17/// What the shell should book.
18#[derive(Clone, Copy, PartialEq, Eq, Debug)]
19pub enum Schedule {
20    /// Nothing: a frame is already booked, or none is owed.
21    Idle,
22    /// The next animation frame, which is the soonest a frame can run.
23    Frame,
24    /// A frame after `after`, which is longer than the browser's next frame.
25    Timer { ticket: Ticket, after: Duration },
26}
27
28/// What the loop is waiting on.
29#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
30enum Waiting {
31    #[default]
32    Nothing,
33    /// An animation frame is booked; nothing can run sooner.
34    Frame,
35    /// A timer is booked for a frame the kernel asked for later.
36    Timer(Ticket),
37}
38
39/// The frame loop's booking, as a value.
40#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
41pub struct Pacing {
42    waiting: Waiting,
43    minted: u64,
44}
45
46impl Pacing {
47    /// Something happened, and the frame that reads it is owed now. A timer
48    /// booked for later is overtaken rather than waited out.
49    pub fn said(&mut self) -> Schedule {
50        match self.waiting {
51            Waiting::Frame => Schedule::Idle,
52            Waiting::Nothing | Waiting::Timer(_) => self.books_a_frame(),
53        }
54    }
55
56    /// The booked frame is running: its booking is spent, so whatever the
57    /// frame itself says can book the next one.
58    pub fn entered(&mut self) -> Schedule {
59        self.waiting = Waiting::Nothing;
60        Schedule::Idle
61    }
62
63    /// The frame ran, and the kernel asked for another in `repaint`. A frame
64    /// booked by a handler that fired during this one stands: it will read
65    /// whatever this one would have asked for again.
66    pub fn ran(&mut self, repaint: Option<Duration>) -> Schedule {
67        if self.waiting != Waiting::Nothing {
68            return Schedule::Idle;
69        }
70        match repaint {
71            None => Schedule::Idle,
72            Some(Duration::ZERO) => self.books_a_frame(),
73            Some(after) => {
74                self.minted += 1;
75                let ticket = Ticket(self.minted);
76                self.waiting = Waiting::Timer(ticket);
77                Schedule::Timer { ticket, after }
78            }
79        }
80    }
81
82    /// A booked timer fired. Its frame runs unless something overtook it.
83    pub fn woke(&mut self, ticket: Ticket) -> Schedule {
84        if self.waiting == Waiting::Timer(ticket) {
85            self.books_a_frame()
86        } else {
87            Schedule::Idle
88        }
89    }
90
91    fn books_a_frame(&mut self) -> Schedule {
92        self.waiting = Waiting::Frame;
93        Schedule::Frame
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    const SOON: Duration = Duration::from_millis(16);
102
103    #[test]
104    fn the_first_thing_said_books_a_frame_and_the_next_things_ride_it() {
105        let mut pacing = Pacing::default();
106        assert_eq!(pacing.said(), Schedule::Frame);
107        assert_eq!(pacing.said(), Schedule::Idle);
108        assert_eq!(pacing.said(), Schedule::Idle);
109    }
110
111    #[test]
112    fn a_frame_that_asks_for_nothing_leaves_the_loop_idle() {
113        let mut pacing = Pacing::default();
114        assert_eq!(pacing.said(), Schedule::Frame);
115        assert_eq!(pacing.entered(), Schedule::Idle);
116        assert_eq!(pacing.ran(None), Schedule::Idle);
117        // And the loop is startable again: nothing is left booked.
118        assert_eq!(pacing.said(), Schedule::Frame);
119    }
120
121    #[test]
122    fn a_repaint_owed_at_once_books_the_next_animation_frame() {
123        let mut pacing = Pacing::default();
124        pacing.said();
125        pacing.entered();
126        assert_eq!(pacing.ran(Some(Duration::ZERO)), Schedule::Frame);
127        assert_eq!(pacing.said(), Schedule::Idle);
128    }
129
130    #[test]
131    fn a_repaint_owed_later_books_a_timer_that_runs_when_it_fires() {
132        let mut pacing = Pacing::default();
133        pacing.said();
134        pacing.entered();
135        let Schedule::Timer { ticket, after } = pacing.ran(Some(SOON)) else {
136            panic!("a later repaint books a timer");
137        };
138        assert_eq!(after, SOON);
139        assert_eq!(pacing.woke(ticket), Schedule::Frame);
140    }
141
142    /// An input arriving while a timer is pending paints now rather than
143    /// waiting the timer out — and the timer, when it fires, finds its
144    /// frame already run.
145    #[test]
146    fn something_said_overtakes_a_pending_timer_and_leaves_it_stale() {
147        let mut pacing = Pacing::default();
148        pacing.said();
149        pacing.entered();
150        let Schedule::Timer { ticket, .. } = pacing.ran(Some(SOON)) else {
151            panic!("a later repaint books a timer");
152        };
153        assert_eq!(pacing.said(), Schedule::Frame);
154        assert_eq!(pacing.woke(ticket), Schedule::Idle);
155    }
156
157    /// A handler firing while the frame is in flight books the next frame;
158    /// what that frame asks for must not book a second one on top of it.
159    #[test]
160    fn a_frame_booked_during_the_run_is_the_only_one_booked() {
161        let mut pacing = Pacing::default();
162        pacing.said();
163        pacing.entered();
164        assert_eq!(pacing.said(), Schedule::Frame);
165        assert_eq!(pacing.ran(Some(Duration::ZERO)), Schedule::Idle);
166        assert_eq!(pacing.ran(Some(SOON)), Schedule::Idle);
167    }
168}