Skip to main content

shuyi_platform/
tick.rs

1//! [`TickClock`]: real time turned into fixed simulation steps (ADR-0003
2//! ยง2): 60 a second, at most 4 in one frame, after which the game slows
3//! down rather than spiral. Whole nanoseconds, so it counts the same on
4//! every machine.
5
6use core::time::Duration;
7
8/// Fixed simulation steps a second: [`shuyi_core::timer::STEPS_PER_SECOND`].
9pub const STEPS_PER_SECOND: u32 = shuyi_core::timer::STEPS_PER_SECOND;
10
11/// The most steps one frame runs: a frame later than this drops the rest
12/// of its time.
13pub const MAX_STEPS: u32 = 4;
14
15/// Nanoseconds in a second, in the units [`TickClock`] counts in.
16const SECOND: u64 = 1_000_000_000;
17
18/// The time a frame brings, turned into fixed steps; what is left of a step
19/// waits for the next frame.
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub struct TickClock {
22    /// Time not yet stepped, in sixtieths of a nanosecond, so a step is a
23    /// whole number of them ([`SECOND`]).
24    owed: u64,
25}
26
27impl TickClock {
28    /// A clock owing nothing.
29    pub const fn new() -> Self {
30        Self { owed: 0 }
31    }
32
33    /// How many steps `elapsed` brings, at most [`MAX_STEPS`]. When a frame
34    /// is that late its extra time is dropped, keeping only the part of a
35    /// step it was into.
36    pub fn advance(&mut self, elapsed: Duration) -> u32 {
37        let nanos = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
38        self.owed = self
39            .owed
40            .saturating_add(nanos.saturating_mul(u64::from(STEPS_PER_SECOND)));
41        let due = self.owed / SECOND;
42        self.owed %= SECOND;
43        // At most MAX_STEPS, which fits.
44        u32::try_from(due.min(u64::from(MAX_STEPS))).unwrap_or(MAX_STEPS)
45    }
46
47    /// Forgets the time owed: after a sleep, or a load that took seconds.
48    pub fn reset(&mut self) {
49        self.owed = 0;
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::alloc_prelude::*;
57
58    const STEP_NS: u64 = 16_666_667;
59
60    #[test]
61    fn a_frame_of_a_sixtieth_of_a_second_is_one_step() {
62        let mut clock = TickClock::new();
63        let frame = Duration::from_nanos(STEP_NS);
64        let steps: u32 = (0..600).map(|_| clock.advance(frame)).sum();
65        assert_eq!(steps, 600);
66    }
67
68    #[test]
69    fn whole_milliseconds_add_up_to_sixty_steps_a_second() {
70        let mut clock = TickClock::new();
71        // 16, 17, 17 ms: 50 ms every three frames, as a millisecond clock at
72        // 60 frames a second gives.
73        let frames = [16, 17, 17].map(Duration::from_millis);
74        let steps: Vec<u32> = frames
75            .iter()
76            .cycle()
77            .take(60)
78            .map(|&frame| clock.advance(frame))
79            .collect();
80        assert_eq!(steps.iter().sum::<u32>(), 60);
81        assert!(steps.iter().all(|&s| s <= 2), "{steps:?}");
82        // A 120 Hz display: two frames a step.
83        let mut fast = TickClock::new();
84        let steps: Vec<u32> = (0..120)
85            .map(|_| fast.advance(Duration::from_nanos(STEP_NS / 2 + 1)))
86            .collect();
87        assert_eq!(steps.iter().sum::<u32>(), 60);
88        assert!(steps.chunks(2).all(|pair| pair.iter().sum::<u32>() == 1));
89    }
90
91    #[test]
92    fn a_late_frame_runs_at_most_four_steps_and_drops_the_rest() {
93        let mut clock = TickClock::new();
94        assert_eq!(clock.advance(Duration::from_secs(600)), MAX_STEPS);
95        // Nothing of the ten minutes is left over.
96        assert_eq!(clock.advance(Duration::from_millis(1)), 0);
97        assert_eq!(clock.advance(Duration::MAX), MAX_STEPS);
98        assert_eq!(clock.advance(Duration::ZERO), 0);
99        // Three steps' worth is three steps.
100        assert_eq!(clock.advance(Duration::from_nanos(3 * STEP_NS)), 3);
101    }
102
103    #[test]
104    fn reset_forgets_a_part_step() {
105        let mut clock = TickClock::new();
106        assert_eq!(clock.advance(Duration::from_millis(10)), 0);
107        clock.reset();
108        assert_eq!(clock.advance(Duration::from_millis(10)), 0);
109        assert_eq!(clock.advance(Duration::from_millis(10)), 1);
110    }
111}