Skip to main content

ferroui_base/rendering/
sleep_loop_render_timer.rs

1// The timer owns a thread and sleeps on it; neither exists on bare
2// WebAssembly.
3#![cfg(not(target_family = "wasm"))]
4
5use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
6use std::sync::{Arc, Mutex, PoisonError};
7use std::thread;
8use std::time::{Duration, Instant};
9
10use super::auto_reset_event::AutoResetEvent;
11use super::{IRenderTimer, RenderTimerTick};
12
13/// The state shared with the timer thread.
14struct Shared {
15    tick: Mutex<Option<RenderTimerTick>>,
16    stopped: AtomicBool,
17    wake_event: AutoResetEvent,
18    st: Instant,
19    desired_fps: AtomicI32,
20    /// Set when the timer is dropped, to let the thread exit.
21    released: AtomicBool,
22}
23
24/// A render timer that ticks on a thread of its own, sleeping between the
25/// ticks.
26pub struct SleepLoopRenderTimer {
27    shared: Arc<Shared>,
28    thread_started: Mutex<bool>,
29}
30
31impl SleepLoopRenderTimer {
32    /// # Panics
33    /// Panics when `fps` is less than 1.
34    pub fn new(fps: i32) -> Self {
35        let timer = Self {
36            shared: Arc::new(Shared {
37                tick: Mutex::new(None),
38                stopped: AtomicBool::new(true),
39                wake_event: AutoResetEvent::new(false),
40                st: Instant::now(),
41                desired_fps: AtomicI32::new(0),
42                released: AtomicBool::new(false),
43            }),
44            thread_started: Mutex::new(false),
45        };
46        timer.set_desired_fps(fps);
47        timer
48    }
49
50    pub fn desired_fps(&self) -> i32 {
51        self.shared.desired_fps.load(Ordering::SeqCst)
52    }
53
54    /// # Panics
55    /// Panics when `value` is less than 1.
56    pub fn set_desired_fps(&self, value: i32) {
57        if value < 1 {
58            panic!("value ('{value}') must be greater than or equal to '1'. (Parameter 'value')");
59        }
60        self.shared.desired_fps.store(value, Ordering::SeqCst);
61    }
62}
63
64impl Shared {
65    fn loop_proc(&self) {
66        let mut last_tick = self.st.elapsed();
67        loop {
68            if self.stopped.load(Ordering::SeqCst) {
69                self.wake_event.wait_one();
70            }
71            if self.released.load(Ordering::SeqCst) {
72                return;
73            }
74
75            let now = self.st.elapsed();
76            let tick_interval = Duration::from_secs_f64(1.0 / f64::from(self.desired_fps.load(Ordering::SeqCst)));
77            // Negative when the next tick is overdue.
78            if let Some(time_till_next_tick) = (last_tick + tick_interval).checked_sub(now) {
79                if time_till_next_tick.as_secs_f64() * 1000.0 > 1.0 {
80                    self.wake_event.wait_one_timeout(time_till_next_tick);
81                }
82            }
83            if self.released.load(Ordering::SeqCst) {
84                return;
85            }
86            let now = self.st.elapsed();
87            last_tick = now;
88
89            let tick = self.tick.lock().unwrap_or_else(PoisonError::into_inner).clone();
90            if let Some(tick) = tick {
91                tick(now);
92            }
93        }
94    }
95}
96
97impl IRenderTimer for SleepLoopRenderTimer {
98    fn tick(&self) -> Option<RenderTimerTick> {
99        self.shared.tick.lock().unwrap_or_else(PoisonError::into_inner).clone()
100    }
101
102    fn set_tick(&self, value: Option<RenderTimerTick>) {
103        match value {
104            Some(value) => {
105                *self.shared.tick.lock().unwrap_or_else(PoisonError::into_inner) = Some(value);
106                self.shared.stopped.store(false, Ordering::SeqCst);
107                let mut thread_started = self.thread_started.lock().unwrap_or_else(PoisonError::into_inner);
108                if !*thread_started {
109                    *thread_started = true;
110                    let shared = self.shared.clone();
111                    thread::Builder::new()
112                        .name("SleepLoopRenderTimer".to_string())
113                        .spawn(move || shared.loop_proc())
114                        .expect("failed to start the render timer thread");
115                } else {
116                    self.shared.wake_event.set();
117                }
118            }
119            None => {
120                self.shared.stopped.store(true, Ordering::SeqCst);
121                *self.shared.tick.lock().unwrap_or_else(PoisonError::into_inner) = None;
122            }
123        }
124    }
125
126    fn runs_in_background(&self) -> bool {
127        true
128    }
129}
130
131impl Drop for SleepLoopRenderTimer {
132    fn drop(&mut self) {
133        self.shared.released.store(true, Ordering::SeqCst);
134        self.shared.stopped.store(true, Ordering::SeqCst);
135        self.shared.wake_event.set();
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use std::sync::mpsc;
143
144    #[test]
145    #[should_panic(expected = "must be greater than or equal to '1'")]
146    fn fps_must_be_positive() {
147        SleepLoopRenderTimer::new(0);
148    }
149
150    #[test]
151    fn ticks_with_increasing_timestamps_until_stopped() {
152        let timer = SleepLoopRenderTimer::new(500);
153        assert!(timer.runs_in_background());
154        assert_eq!(timer.desired_fps(), 500);
155        timer.set_desired_fps(1000);
156        assert_eq!(timer.desired_fps(), 1000);
157        assert!(timer.tick().is_none());
158
159        let (sender, receiver) = mpsc::channel();
160        let sender = Mutex::new(sender);
161        timer.set_tick(Some(Arc::new(move |time| {
162            let _ = sender.lock().unwrap().send(time);
163        })));
164        assert!(timer.tick().is_some());
165
166        let first = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
167        let second = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
168        assert!(second > first);
169
170        timer.set_tick(None);
171        assert!(timer.tick().is_none());
172        // The callback (and with it the sender) is released by the timer and,
173        // at the latest after the tick in flight, by the thread.
174        loop {
175            match receiver.recv_timeout(Duration::from_secs(10)) {
176                Ok(_) => {}
177                Err(mpsc::RecvTimeoutError::Disconnected) => break,
178                Err(mpsc::RecvTimeoutError::Timeout) => panic!("the timer did not stop"),
179            }
180        }
181
182        // Restarting wakes the sleeping thread up.
183        let (sender, receiver) = mpsc::channel();
184        let sender = Mutex::new(sender);
185        timer.set_tick(Some(Arc::new(move |time| {
186            let _ = sender.lock().unwrap().send(time);
187        })));
188        assert!(receiver.recv_timeout(Duration::from_secs(10)).unwrap() > second);
189    }
190}