Skip to main content

ferroui_base/rendering/
ui_thread_render_timer.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, Mutex, PoisonError};
6use std::time::Duration;
7
8use super::{
9    DefaultRenderTimer, DefaultRenderTimerImpl, IRenderTimer, RenderTimerSubscription, RenderTimerTick,
10};
11use crate::reactive::IDisposable;
12use crate::threading::{Dispatcher, DispatcherPriority, DispatcherTimer};
13
14/// The stopwatch of the timer, driven by the clock of the dispatcher the
15/// timer ticks on. It starts the first time the timer is started.
16#[derive(Default)]
17struct Clock {
18    started_at: Mutex<Option<i64>>,
19}
20
21impl Clock {
22    fn elapsed(&self, dispatcher: &Dispatcher) -> Duration {
23        let now = dispatcher.now();
24        let mut started_at = self.started_at.lock().unwrap_or_else(PoisonError::into_inner);
25        let started_at = *started_at.get_or_insert(now);
26        Duration::from_millis(now.saturating_sub(started_at).max(0) as u64)
27    }
28}
29
30crate::ferro_thread_local! {
31    /// The dispatcher timers of the running instances of this thread. They
32    /// are `Rc`-based, so they stay on the thread they were started on; the
33    /// thread-safe subscription only carries the key.
34    static INSTANCES: RefCell<HashMap<u64, Rc<DispatcherTimer>>> = RefCell::new(HashMap::new());
35}
36
37static NEXT_INSTANCE_ID: AtomicU64 = AtomicU64::new(1);
38
39const MIN_INTERVAL: Duration = Duration::from_millis(1);
40
41/// Sets the interval of the next tick when the tick callback returns,
42/// however it returns.
43struct RescheduleOnExit<'a> {
44    timer: &'a DispatcherTimer,
45    clock: &'a Clock,
46    next_tick_at: Duration,
47}
48
49impl Drop for RescheduleOnExit<'_> {
50    fn drop(&mut self) {
51        let after_tick = self.clock.elapsed(self.timer.dispatcher());
52        let interval = match self.next_tick_at.checked_sub(after_tick) {
53            Some(interval) if interval >= MIN_INTERVAL => interval,
54            // We are way overdue, but shouldn't cause starvation in other areas
55            _ => MIN_INTERVAL,
56        };
57        self.timer.set_interval(interval);
58    }
59}
60
61struct TimerInstance {
62    id: u64,
63    dispatcher: Arc<Dispatcher>,
64}
65
66impl TimerInstance {
67    fn new(frames_per_second: i32, clock: Arc<Clock>, tick: RenderTimerTick) -> TimerInstance {
68        let timer = DispatcherTimer::with_priority(DispatcherPriority::RENDER);
69        let dispatcher = timer.dispatcher().clone();
70        // Starts the stopwatch.
71        clock.elapsed(&dispatcher);
72
73        let interval = match Duration::try_from_secs_f64(1.0 / f64::from(frames_per_second)) {
74            Ok(interval) => interval,
75            Err(_) => panic!("frames_per_second must be a positive number."),
76        };
77
78        timer.tick(move |timer| {
79            let ticked_at = clock.elapsed(timer.dispatcher());
80            let _reschedule = RescheduleOnExit { timer, clock: &clock, next_tick_at: ticked_at + interval };
81            tick(ticked_at);
82        });
83        // The first tick is not delayed: the timer starts with a zero
84        // interval, the frame interval applies from the first tick on.
85        timer.set_interval(Duration::ZERO);
86        timer.start();
87
88        let id = NEXT_INSTANCE_ID.fetch_add(1, Ordering::Relaxed);
89        INSTANCES.with(|instances| instances.borrow_mut().insert(id, timer));
90        TimerInstance { id, dispatcher }
91    }
92
93    fn stop(id: u64) {
94        let timer = INSTANCES.try_with(|instances| instances.borrow_mut().remove(&id)).ok().flatten();
95        if let Some(timer) = timer {
96            timer.stop();
97        }
98    }
99}
100
101impl IDisposable for TimerInstance {
102    fn dispose(&self) {
103        let id = self.id;
104        if self.dispatcher.check_access() {
105            Self::stop(id);
106        } else {
107            // The dispatcher timer belongs to the thread it was started on.
108            self.dispatcher.post(move || Self::stop(id), DispatcherPriority::SEND);
109        }
110    }
111}
112
113struct UiThreadRenderTimerImpl {
114    clock: Arc<Clock>,
115}
116
117impl DefaultRenderTimerImpl for UiThreadRenderTimerImpl {
118    fn runs_in_background(&self) -> bool {
119        false
120    }
121
122    fn start_core(&self, timer: &DefaultRenderTimer, tick: RenderTimerTick) -> RenderTimerSubscription {
123        Arc::new(TimerInstance::new(timer.frames_per_second(), self.clock.clone(), tick))
124    }
125}
126
127/// Render timer that ticks on UI thread. Useful for debugging or
128/// bootstrapping on new platforms.
129///
130/// The timer ticks on the dispatcher of the thread that starts it (the
131/// thread that sets the tick callback), which is expected to be the UI
132/// thread.
133pub struct UiThreadRenderTimer {
134    base: DefaultRenderTimer,
135}
136
137impl UiThreadRenderTimer {
138    /// Initializes a new instance of the [`UiThreadRenderTimer`] class.
139    ///
140    /// `frames_per_second` is the number of frames per second at which the
141    /// loop should run.
142    pub fn new(frames_per_second: i32) -> Self {
143        Self {
144            base: DefaultRenderTimer::with_impl(
145                frames_per_second,
146                Box::new(UiThreadRenderTimerImpl { clock: Arc::new(Clock::default()) }),
147            ),
148        }
149    }
150
151    /// Gets the number of frames per second at which the loop runs.
152    pub fn frames_per_second(&self) -> i32 {
153        self.base.frames_per_second()
154    }
155}
156
157impl IRenderTimer for UiThreadRenderTimer {
158    fn tick(&self) -> Option<RenderTimerTick> {
159        self.base.tick()
160    }
161
162    fn set_tick(&self, value: Option<RenderTimerTick>) {
163        self.base.set_tick(value);
164    }
165
166    fn runs_in_background(&self) -> bool {
167        self.base.runs_in_background()
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::threading::{DispatcherImplEvent, IDispatcherImpl};
175    use std::sync::atomic::AtomicI64;
176    use std::thread::{self, ThreadId};
177
178    /// A dispatcher implementation whose clock and events are driven by the
179    /// test.
180    struct ManualDispatcherImpl {
181        loop_thread: ThreadId,
182        now: Arc<AtomicI64>,
183        next_timer: Mutex<Option<i64>>,
184        signaled: DispatcherImplEvent,
185        timer: DispatcherImplEvent,
186    }
187
188    impl ManualDispatcherImpl {
189        fn new() -> Arc<Self> {
190            Arc::new(Self {
191                loop_thread: thread::current().id(),
192                now: Arc::new(AtomicI64::new(0)),
193                next_timer: Mutex::new(None),
194                signaled: DispatcherImplEvent::new(),
195                timer: DispatcherImplEvent::new(),
196            })
197        }
198
199        /// The due time of the only dispatcher timer of the thread.
200        fn due_time(&self) -> Option<i64> {
201            let timers = Dispatcher::snapshot_timers_for_unit_tests();
202            assert!(timers.len() <= 1);
203            timers.first().map(|timer| timer.due_time_in_ms())
204        }
205
206        /// Moves the clock to `now` and reports the timer as due.
207        fn fire_timer_at(&self, now: i64) {
208            self.now.store(now, Ordering::SeqCst);
209            self.timer.raise();
210        }
211    }
212
213    /// The dispatcher owns its implementation through an `Rc`; the test
214    /// also drives the same implementation from tick callbacks.
215    struct Forward(Arc<ManualDispatcherImpl>);
216
217    impl IDispatcherImpl for Forward {
218        fn current_thread_is_loop_thread(&self) -> bool {
219            self.0.current_thread_is_loop_thread()
220        }
221        fn signal(&self) {
222            self.0.signal()
223        }
224        fn signal_handle(&self) -> std::sync::Arc<dyn crate::threading::IDispatcherSignal> {
225            self.0.signal_handle()
226        }
227        fn signaled(&self) -> &DispatcherImplEvent {
228            self.0.signaled()
229        }
230        fn timer(&self) -> &DispatcherImplEvent {
231            self.0.timer()
232        }
233        fn now(&self) -> i64 {
234            self.0.now()
235        }
236        fn update_timer(&self, due_time_in_ms: Option<i64>) {
237            self.0.update_timer(due_time_in_ms)
238        }
239    }
240
241    impl IDispatcherImpl for ManualDispatcherImpl {
242        fn current_thread_is_loop_thread(&self) -> bool {
243            thread::current().id() == self.loop_thread
244        }
245
246        fn signal(&self) {}
247
248        fn signal_handle(&self) -> std::sync::Arc<dyn crate::threading::IDispatcherSignal> {
249            struct NoSignal;
250            impl crate::threading::IDispatcherSignal for NoSignal {
251                fn signal(&self) {}
252            }
253            std::sync::Arc::new(NoSignal)
254        }
255
256        fn signaled(&self) -> &DispatcherImplEvent {
257            &self.signaled
258        }
259
260        fn timer(&self) -> &DispatcherImplEvent {
261            &self.timer
262        }
263
264        fn now(&self) -> i64 {
265            self.now.load(Ordering::SeqCst)
266        }
267
268        fn update_timer(&self, due_time_in_ms: Option<i64>) {
269            *self.next_timer.lock().unwrap() = due_time_in_ms;
270        }
271    }
272
273    #[test]
274    fn ticks_on_the_dispatcher_with_the_dispatcher_clock() {
275        let _scope = Dispatcher::unit_test_scope();
276        let platform = ManualDispatcherImpl::new();
277        let dispatcher = Dispatcher::new(Some(std::rc::Rc::new(Forward(platform.clone()))));
278        platform.now.store(1000, Ordering::SeqCst);
279
280        let timer = UiThreadRenderTimer::new(50);
281        assert!(!timer.runs_in_background());
282        assert_eq!(timer.frames_per_second(), 50);
283
284        let ticks = Arc::new(Mutex::new(Vec::new()));
285        let t = ticks.clone();
286        let tick_now = platform.now.clone();
287        let work = Arc::new(AtomicI64::new(5));
288        let w = work.clone();
289        timer.set_tick(Some(Arc::new(move |time| {
290            t.lock().unwrap().push(time);
291            // Rendering takes some time.
292            tick_now.fetch_add(w.load(Ordering::SeqCst), Ordering::SeqCst);
293        })));
294        assert_eq!(DispatcherTimer::active_timers_count(), 1);
295        assert!(ticks.lock().unwrap().is_empty());
296
297        // The first tick is immediate and starts the stopwatch.
298        dispatcher.run_jobs(None);
299        assert_eq!(*ticks.lock().unwrap(), [Duration::ZERO]);
300        // 20ms per frame, 5 of which were spent in the tick.
301        assert_eq!(platform.due_time(), Some(1005 + 15));
302
303        dispatcher.run_jobs(None);
304        assert_eq!(ticks.lock().unwrap().len(), 1);
305
306        platform.fire_timer_at(1021);
307        dispatcher.run_jobs(None);
308        assert_eq!(*ticks.lock().unwrap(), [Duration::ZERO, Duration::from_millis(21)]);
309        assert_eq!(platform.due_time(), Some(1026 + 15));
310
311        // An overdue tick is rescheduled with the minimum interval.
312        work.store(100, Ordering::SeqCst);
313        platform.fire_timer_at(1042);
314        dispatcher.run_jobs(None);
315        assert_eq!(ticks.lock().unwrap().len(), 3);
316        assert_eq!(platform.due_time(), Some(1142 + 1));
317
318        timer.set_tick(None);
319        assert_eq!(DispatcherTimer::active_timers_count(), 0);
320        assert_eq!(platform.due_time(), None);
321        platform.fire_timer_at(2000);
322        dispatcher.run_jobs(None);
323        assert_eq!(ticks.lock().unwrap().len(), 3);
324
325        // Restarting ticks immediately again, on the same stopwatch.
326        work.store(0, Ordering::SeqCst);
327        timer.set_tick(Some(Arc::new({
328            let t = ticks.clone();
329            move |time| t.lock().unwrap().push(time)
330        })));
331        dispatcher.run_jobs(None);
332        assert_eq!(ticks.lock().unwrap()[3], Duration::from_millis(1000));
333        timer.set_tick(None);
334        assert_eq!(DispatcherTimer::active_timers_count(), 0);
335    }
336
337    #[test]
338    fn can_be_stopped_from_another_thread() {
339        let _scope = Dispatcher::unit_test_scope();
340        let platform = ManualDispatcherImpl::new();
341        let dispatcher = Dispatcher::new(Some(std::rc::Rc::new(Forward(platform.clone()))));
342
343        let timer = Arc::new(UiThreadRenderTimer::new(60));
344        timer.set_tick(Some(Arc::new(|_| {})));
345        assert_eq!(DispatcherTimer::active_timers_count(), 1);
346
347        let background = timer.clone();
348        thread::spawn(move || background.set_tick(None)).join().unwrap();
349        assert!(timer.tick().is_none());
350
351        // The dispatcher timer is stopped by the job posted to its thread.
352        dispatcher.run_jobs(None);
353        assert_eq!(DispatcherTimer::active_timers_count(), 0);
354    }
355}