Skip to main content

ferroui_base/rendering/
default_render_timer.rs

1use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
2
3use super::{IRenderTimer, RenderTimerTick};
4use crate::reactive::IDisposable;
5
6/// The running timer returned by [`DefaultRenderTimerImpl::start_core`];
7/// disposing it stops the ticks. It is shared between threads, unlike the
8/// `Rc`-based disposables of the object model.
9pub type RenderTimerSubscription = Arc<dyn IDisposable + Send + Sync>;
10
11/// The overridable members of [`DefaultRenderTimer`].
12pub trait DefaultRenderTimerImpl: Send + Sync {
13    /// Indicates if the timer ticks on a non-UI thread.
14    fn runs_in_background(&self) -> bool {
15        true
16    }
17
18    /// Provides the implementation of starting the timer.
19    ///
20    /// `tick` is the method to call on each tick. This can be overridden by
21    /// platform implementations to use a specialized timer implementation.
22    fn start_core(&self, timer: &DefaultRenderTimer, tick: RenderTimerTick) -> RenderTimerSubscription;
23}
24
25/// Defines a default render timer that uses a standard timer.
26///
27/// Platform implementations can supply their own
28/// [`DefaultRenderTimerImpl`] to use a specialized timer implementation.
29pub struct DefaultRenderTimer {
30    /// Shared with the callback handed to `start_core`.
31    tick: Arc<Mutex<Option<RenderTimerTick>>>,
32    subscription: Mutex<Option<RenderTimerSubscription>>,
33    frames_per_second: i32,
34    overrides: Box<dyn DefaultRenderTimerImpl>,
35}
36
37fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
38    mutex.lock().unwrap_or_else(PoisonError::into_inner)
39}
40
41impl DefaultRenderTimer {
42    /// Initializes a new instance of the [`DefaultRenderTimer`] class that
43    /// ticks on a thread of its own.
44    ///
45    /// `frames_per_second` is the number of frames per second at which the
46    /// loop should run.
47    #[cfg(not(target_family = "wasm"))]
48    pub fn new(frames_per_second: i32) -> Self {
49        Self::with_impl(frames_per_second, Box::new(standard_timer::StandardTimerImpl))
50    }
51
52    /// Initializes a timer whose overridable members are provided by
53    /// `overrides`.
54    pub fn with_impl(frames_per_second: i32, overrides: Box<dyn DefaultRenderTimerImpl>) -> Self {
55        Self { tick: Arc::new(Mutex::new(None)), subscription: Mutex::new(None), frames_per_second, overrides }
56    }
57
58    /// Gets the number of frames per second at which the loop runs.
59    pub fn frames_per_second(&self) -> i32 {
60        self.frames_per_second
61    }
62
63    fn internal_tick(&self) -> RenderTimerTick {
64        let slot = self.tick.clone();
65        Arc::new(move |tick_count| {
66            let tick = lock(&slot).clone();
67            if let Some(tick) = tick {
68                tick(tick_count);
69            }
70        })
71    }
72}
73
74impl IRenderTimer for DefaultRenderTimer {
75    fn tick(&self) -> Option<RenderTimerTick> {
76        lock(&self.tick).clone()
77    }
78
79    fn set_tick(&self, value: Option<RenderTimerTick>) {
80        let mut subscription = lock(&self.subscription);
81        match value {
82            Some(value) => {
83                *lock(&self.tick) = Some(value);
84                if subscription.is_none() {
85                    *subscription = Some(self.overrides.start_core(self, self.internal_tick()));
86                }
87            }
88            None => {
89                if let Some(subscription) = subscription.take() {
90                    subscription.dispose();
91                }
92                *lock(&self.tick) = None;
93            }
94        }
95    }
96
97    fn runs_in_background(&self) -> bool {
98        self.overrides.runs_in_background()
99    }
100}
101
102#[cfg(not(target_family = "wasm"))]
103mod standard_timer {
104    use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError};
105    use std::thread;
106    use std::time::{Duration, Instant};
107
108    use super::{DefaultRenderTimer, DefaultRenderTimerImpl, RenderTimerSubscription};
109    use crate::reactive::IDisposable;
110    use crate::rendering::RenderTimerTick;
111
112    /// The time since the first use of a standard timer in this process, in
113    /// whole milliseconds.
114    fn tick_count() -> Duration {
115        static START: OnceLock<Instant> = OnceLock::new();
116        Duration::from_millis(START.get_or_init(Instant::now).elapsed().as_millis() as u64)
117    }
118
119    struct PeriodicTimer {
120        stopped: Mutex<bool>,
121        wakeup: Condvar,
122    }
123
124    impl IDisposable for PeriodicTimer {
125        fn dispose(&self) {
126            *self.stopped.lock().unwrap_or_else(PoisonError::into_inner) = true;
127            self.wakeup.notify_all();
128        }
129    }
130
131    /// The base behaviour: a periodic timer on a thread of its own.
132    pub(super) struct StandardTimerImpl;
133
134    impl DefaultRenderTimerImpl for StandardTimerImpl {
135        fn start_core(&self, timer: &DefaultRenderTimer, tick: RenderTimerTick) -> RenderTimerSubscription {
136            let interval = match Duration::try_from_secs_f64(1.0 / f64::from(timer.frames_per_second())) {
137                Ok(interval) => interval,
138                Err(_) => panic!("frames_per_second must be a positive number."),
139            };
140
141            let state = Arc::new(PeriodicTimer { stopped: Mutex::new(false), wakeup: Condvar::new() });
142            let thread_state = state.clone();
143            // Start the clock before the first tick.
144            tick_count();
145            thread::Builder::new()
146                .name("RenderTimer".to_string())
147                .spawn(move || {
148                    let mut due = Instant::now() + interval;
149                    loop {
150                        {
151                            let mut stopped = thread_state.stopped.lock().unwrap_or_else(PoisonError::into_inner);
152                            loop {
153                                if *stopped {
154                                    return;
155                                }
156                                let now = Instant::now();
157                                if now >= due {
158                                    break;
159                                }
160                                stopped = thread_state
161                                    .wakeup
162                                    .wait_timeout(stopped, due - now)
163                                    .unwrap_or_else(PoisonError::into_inner)
164                                    .0;
165                            }
166                        }
167
168                        tick(tick_count());
169
170                        due += interval;
171                        let now = Instant::now();
172                        if due < now {
173                            // The callback took longer than the period.
174                            due = now;
175                        }
176                    }
177                })
178                .expect("failed to start the render timer thread");
179            state
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use std::sync::atomic::{AtomicI32, Ordering};
188    use std::time::Duration;
189
190    #[derive(Default)]
191    struct Recorded {
192        starts: AtomicI32,
193        disposals: AtomicI32,
194        core_tick: Mutex<Option<RenderTimerTick>>,
195    }
196
197    struct RecordingImpl(Arc<Recorded>);
198
199    struct RecordingSubscription(Arc<Recorded>);
200
201    impl IDisposable for RecordingSubscription {
202        fn dispose(&self) {
203            self.0.disposals.fetch_add(1, Ordering::SeqCst);
204        }
205    }
206
207    impl DefaultRenderTimerImpl for RecordingImpl {
208        fn runs_in_background(&self) -> bool {
209            false
210        }
211
212        fn start_core(&self, timer: &DefaultRenderTimer, tick: RenderTimerTick) -> RenderTimerSubscription {
213            assert_eq!(timer.frames_per_second(), 30);
214            self.0.starts.fetch_add(1, Ordering::SeqCst);
215            *lock(&self.0.core_tick) = Some(tick);
216            Arc::new(RecordingSubscription(self.0.clone()))
217        }
218    }
219
220    #[test]
221    fn setting_tick_starts_once_and_clearing_it_disposes_the_subscription() {
222        let recorded = Arc::new(Recorded::default());
223        let timer = DefaultRenderTimer::with_impl(30, Box::new(RecordingImpl(recorded.clone())));
224        assert!(!timer.runs_in_background());
225        assert!(timer.tick().is_none());
226
227        let first = Arc::new(AtomicI32::new(0));
228        let second = Arc::new(AtomicI32::new(0));
229        let f = first.clone();
230        timer.set_tick(Some(Arc::new(move |time| {
231            assert_eq!(time, Duration::from_millis(7));
232            f.fetch_add(1, Ordering::SeqCst);
233        })));
234        assert_eq!(recorded.starts.load(Ordering::SeqCst), 1);
235        assert!(timer.tick().is_some());
236
237        let core_tick = lock(&recorded.core_tick).clone().unwrap();
238        core_tick(Duration::from_millis(7));
239        assert_eq!(first.load(Ordering::SeqCst), 1);
240
241        // Replacing the callback keeps the running subscription.
242        let s = second.clone();
243        timer.set_tick(Some(Arc::new(move |_| {
244            s.fetch_add(1, Ordering::SeqCst);
245        })));
246        assert_eq!(recorded.starts.load(Ordering::SeqCst), 1);
247        core_tick(Duration::from_millis(7));
248        assert_eq!((first.load(Ordering::SeqCst), second.load(Ordering::SeqCst)), (1, 1));
249
250        timer.set_tick(None);
251        assert_eq!(recorded.disposals.load(Ordering::SeqCst), 1);
252        assert!(timer.tick().is_none());
253        // A late tick of the stopped timer goes nowhere.
254        core_tick(Duration::from_millis(7));
255        assert_eq!(second.load(Ordering::SeqCst), 1);
256
257        timer.set_tick(None);
258        assert_eq!(recorded.disposals.load(Ordering::SeqCst), 1);
259
260        timer.set_tick(Some(Arc::new(|_| {})));
261        assert_eq!(recorded.starts.load(Ordering::SeqCst), 2);
262    }
263
264    #[cfg(not(target_family = "wasm"))]
265    #[test]
266    fn standard_timer_ticks_in_the_background_until_stopped() {
267        let timer = DefaultRenderTimer::new(500);
268        assert!(timer.runs_in_background());
269        assert_eq!(timer.frames_per_second(), 500);
270
271        let (sender, receiver) = std::sync::mpsc::channel();
272        let sender = Mutex::new(sender);
273        timer.set_tick(Some(Arc::new(move |time| {
274            let _ = lock(&sender).send(time);
275        })));
276
277        let first = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
278        let second = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
279        assert!(second >= first);
280
281        timer.set_tick(None);
282        // Clearing the callback drops it, which closes the channel.
283        let deadline = std::time::Instant::now() + Duration::from_secs(10);
284        loop {
285            match receiver.recv_timeout(Duration::from_millis(50)) {
286                Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
287                _ => assert!(std::time::Instant::now() < deadline, "the timer thread did not stop"),
288            }
289        }
290    }
291}