1use std::thread;
5use std::time::{Duration, Instant};
6
7#[derive(Debug)]
9pub struct FrameClock {
10 frame_time: Duration,
11 deadline: Instant,
12 frame: u64,
13}
14
15impl FrameClock {
16 #[must_use]
18 pub fn new(frames_per_second: u32) -> Self {
19 let rate = f64::from(frames_per_second.max(1));
20 Self {
21 frame_time: Duration::from_secs_f64(1.0 / rate),
22 deadline: Instant::now(),
23 frame: 0,
24 }
25 }
26
27 #[must_use]
29 pub fn frame_time(&self) -> Duration {
30 self.frame_time
31 }
32
33 #[must_use]
35 pub fn frame(&self) -> u64 {
36 self.frame
37 }
38
39 pub fn wait_for_next_frame(&mut self) {
42 let remaining = self.advance(Instant::now());
43 if !remaining.is_zero() {
44 thread::sleep(remaining);
45 }
46 }
47
48 fn advance(&mut self, now: Instant) -> Duration {
50 self.frame += 1;
51 self.deadline += self.frame_time;
52 if self.deadline < now {
53 self.deadline = now;
54 }
55 self.deadline.duration_since(now)
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use std::time::Duration;
62
63 use super::FrameClock;
64
65 #[test]
66 fn a_sixty_fps_clock_allots_sixteen_milli_frames() {
67 let clock = FrameClock::new(60);
68 let frame_time = clock.frame_time();
69 assert!(frame_time >= Duration::from_millis(16));
70 assert!(frame_time <= Duration::from_millis(17));
71 }
72
73 #[test]
74 fn a_stall_restarts_the_schedule_instead_of_bursting() {
75 let mut clock = FrameClock::new(10);
76 let stalled_until = clock.deadline + Duration::from_secs(5);
77 assert!(
78 clock.advance(stalled_until).is_zero(),
79 "the late frame runs now"
80 );
81 assert_eq!(
82 clock.advance(stalled_until),
83 Duration::from_millis(100),
84 "the next frame gets a whole slot, not a catch-up"
85 );
86 }
87
88 #[test]
89 fn waiting_counts_frames_without_panicking() {
90 let mut clock = FrameClock::new(1000);
91 clock.wait_for_next_frame();
92 clock.wait_for_next_frame();
93 assert_eq!(clock.frame(), 2);
94 }
95}