Skip to main content

ferroui_base/rendering/
render_loop.rs

1use std::any::Any;
2use std::panic::{catch_unwind, AssertUnwindSafe};
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
5use std::time::Duration;
6
7use super::{IRenderLoop, IRenderLoopTask, IRenderTimer, RenderTimerTick};
8use crate::logging::{LogArea, LogEventLevel, Logger};
9use crate::threading::Dispatcher;
10
11/// Provides factory methods for creating [`IRenderLoop`] instances.
12pub struct RenderLoop;
13
14impl RenderLoop {
15    /// Creates an [`IRenderLoop`] from an [`IRenderTimer`].
16    pub fn from_timer(timer: Arc<dyn IRenderTimer>) -> Arc<dyn IRenderLoop> {
17        DefaultRenderLoop::new(timer)
18    }
19}
20
21/// The state guarded by the timer lock.
22#[derive(Default)]
23struct TimerState {
24    running: bool,
25    wakeup_pending: bool,
26}
27
28/// Default implementation of the application render loop.
29///
30/// The render loop is responsible for advancing the animation timer and
31/// updating the scene graph for visible windows. It owns the sleep/wake
32/// state machine: setting the timer's tick to a callback to start the timer
33/// and to `None` to stop it, under a lock so that timer implementations
34/// never see concurrent changes.
35pub struct DefaultRenderLoop {
36    items: Mutex<Vec<Arc<dyn IRenderLoopTask>>>,
37    /// Only used by the tick that owns `in_tick`.
38    items_copy: Mutex<Vec<Arc<dyn IRenderLoopTask>>>,
39    tick: RenderTimerTick,
40    timer: Arc<dyn IRenderTimer>,
41    timer_lock: Mutex<TimerState>,
42    in_tick: AtomicBool,
43    has_items: AtomicBool,
44}
45
46fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
47    mutex.lock().unwrap_or_else(PoisonError::into_inner)
48}
49
50fn same_task(a: &Arc<dyn IRenderLoopTask>, b: &Arc<dyn IRenderLoopTask>) -> bool {
51    // Reference equality of the objects, whatever vtable the handles carry.
52    std::ptr::eq(Arc::as_ptr(a) as *const (), Arc::as_ptr(b) as *const ())
53}
54
55fn panic_message(payload: &(dyn Any + Send)) -> &str {
56    if let Some(message) = payload.downcast_ref::<&'static str>() {
57        message
58    } else if let Some(message) = payload.downcast_ref::<String>() {
59        message
60    } else {
61        "unknown error"
62    }
63}
64
65/// Clears the in-tick flag when the tick ends, however it ends.
66struct InTickGuard<'a>(&'a AtomicBool);
67
68impl Drop for InTickGuard<'_> {
69    fn drop(&mut self) {
70        self.0.store(false, Ordering::SeqCst);
71    }
72}
73
74impl DefaultRenderLoop {
75    /// Initializes a new instance of the [`DefaultRenderLoop`] class.
76    pub fn new(timer: Arc<dyn IRenderTimer>) -> Arc<DefaultRenderLoop> {
77        Arc::new_cyclic(|weak_self: &Weak<DefaultRenderLoop>| {
78            let this = weak_self.clone();
79            DefaultRenderLoop {
80                items: Mutex::new(Vec::new()),
81                items_copy: Mutex::new(Vec::new()),
82                tick: Arc::new(move |time| {
83                    if let Some(this) = this.upgrade() {
84                        this.timer_tick(time);
85                    }
86                }),
87                timer,
88                timer_lock: Mutex::new(TimerState::default()),
89                in_tick: AtomicBool::new(false),
90                has_items: AtomicBool::new(false),
91            }
92        })
93    }
94
95    /// The timer that drives the loop.
96    pub fn timer(&self) -> &Arc<dyn IRenderTimer> {
97        &self.timer
98    }
99
100    fn timer_tick(&self, _time: Duration) {
101        if self.in_tick.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
102            let _in_tick = InTickGuard(&self.in_tick);
103
104            if let Err(error) = catch_unwind(AssertUnwindSafe(|| self.timer_tick_core())) {
105                if let Some(logger) = Logger::try_get(LogEventLevel::Error, LogArea::VISUAL) {
106                    logger.log_with_values(
107                        Some(self as &dyn Any),
108                        "Exception in render loop: {Error}",
109                        &[&panic_message(&*error)],
110                    );
111                }
112            }
113        }
114    }
115
116    fn timer_tick_core(&self) {
117        // Consume any pending wakeup — this tick will process its work.
118        // Only wakeups arriving during task execution will keep the timer running.
119        // Also drop late ticks that arrive after the timer was stopped.
120        {
121            let mut state = lock(&self.timer_lock);
122            if !state.running {
123                return;
124            }
125            state.wakeup_pending = false;
126        }
127
128        let mut items_copy = lock(&self.items_copy);
129        {
130            let items = lock(&self.items);
131            items_copy.clear();
132            items_copy.extend(items.iter().cloned());
133        }
134
135        let mut wants_next_tick = false;
136        for item in items_copy.iter() {
137            wants_next_tick |= item.render();
138        }
139
140        items_copy.clear();
141        drop(items_copy);
142
143        if !wants_next_tick {
144            let mut state = lock(&self.timer_lock);
145            if !state.running {
146                // Already stopped by remove()
147            } else if state.wakeup_pending {
148                state.wakeup_pending = false;
149            } else {
150                state.running = false;
151                self.timer.set_tick(None);
152            }
153        }
154    }
155}
156
157impl IRenderLoop for DefaultRenderLoop {
158    fn add(&self, i: Arc<dyn IRenderLoopTask>) {
159        Dispatcher::ui_thread().verify_access();
160
161        let should_start = {
162            let mut items = lock(&self.items);
163            items.push(i);
164            items.len() == 1
165        };
166
167        if should_start {
168            self.has_items.store(true, Ordering::SeqCst);
169            self.wakeup();
170        }
171    }
172
173    fn remove(&self, i: &Arc<dyn IRenderLoopTask>) {
174        Dispatcher::ui_thread().verify_access();
175
176        let should_stop = {
177            let mut items = lock(&self.items);
178            if let Some(index) = items.iter().position(|item| same_task(item, i)) {
179                items.remove(index);
180            }
181            items.is_empty()
182        };
183
184        if should_stop {
185            self.has_items.store(false, Ordering::SeqCst);
186            let mut state = lock(&self.timer_lock);
187            if state.running {
188                state.running = false;
189                state.wakeup_pending = false;
190                self.timer.set_tick(None);
191            }
192        }
193    }
194
195    fn runs_in_background(&self) -> bool {
196        self.timer.runs_in_background()
197    }
198
199    fn wakeup(&self) {
200        let mut state = lock(&self.timer_lock);
201        if self.has_items.load(Ordering::SeqCst) && !state.running {
202            state.running = true;
203            self.timer.set_tick(Some(self.tick.clone()));
204        } else {
205            state.wakeup_pending = true;
206        }
207    }
208
209    fn request_frame_out_of_turn(&self) {
210        // The timer knows how to tick early, if it can; the tick itself goes
211        // through `timer_tick` like any other.
212        self.timer.request_tick_out_of_turn();
213    }
214}
215
216impl Drop for DefaultRenderLoop {
217    fn drop(&mut self) {
218        // The tick callback only holds a weak reference to the loop, so a
219        // running timer does not keep the loop alive; stop it instead of
220        // leaving it ticking into nothing.
221        let state = self.timer_lock.get_mut().unwrap_or_else(PoisonError::into_inner);
222        if state.running {
223            state.running = false;
224            self.timer.set_tick(None);
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use std::sync::atomic::AtomicI32;
233
234    /// A timer that ticks when told to.
235    #[derive(Default)]
236    struct ManualRenderTimer {
237        tick: Mutex<Option<RenderTimerTick>>,
238        set_count: AtomicI32,
239        clear_count: AtomicI32,
240        out_of_turn_count: AtomicI32,
241        runs_in_background: bool,
242    }
243
244    impl ManualRenderTimer {
245        fn trigger_tick(&self) {
246            let tick = lock(&self.tick).clone();
247            if let Some(tick) = tick {
248                tick(Duration::ZERO);
249            }
250        }
251
252        fn is_running(&self) -> bool {
253            lock(&self.tick).is_some()
254        }
255    }
256
257    impl IRenderTimer for ManualRenderTimer {
258        fn tick(&self) -> Option<RenderTimerTick> {
259            lock(&self.tick).clone()
260        }
261
262        fn set_tick(&self, value: Option<RenderTimerTick>) {
263            match value {
264                Some(_) => self.set_count.fetch_add(1, Ordering::SeqCst),
265                None => self.clear_count.fetch_add(1, Ordering::SeqCst),
266            };
267            *lock(&self.tick) = value;
268        }
269
270        fn runs_in_background(&self) -> bool {
271            self.runs_in_background
272        }
273
274        fn request_tick_out_of_turn(&self) {
275            self.out_of_turn_count.fetch_add(1, Ordering::SeqCst);
276        }
277    }
278
279    type OnRender = Box<dyn Fn() + Send + Sync>;
280
281    #[derive(Default)]
282    struct Task {
283        renders: AtomicI32,
284        wants_next_tick: AtomicBool,
285        on_render: Mutex<Option<OnRender>>,
286    }
287
288    impl Task {
289        fn new(wants_next_tick: bool) -> Arc<Task> {
290            Arc::new(Task { wants_next_tick: AtomicBool::new(wants_next_tick), ..Task::default() })
291        }
292
293        fn renders(&self) -> i32 {
294            self.renders.load(Ordering::SeqCst)
295        }
296    }
297
298    impl IRenderLoopTask for Task {
299        fn render(&self) -> bool {
300            self.renders.fetch_add(1, Ordering::SeqCst);
301            if let Some(on_render) = &*lock(&self.on_render) {
302                on_render();
303            }
304            self.wants_next_tick.load(Ordering::SeqCst)
305        }
306    }
307
308    fn setup() -> (crate::threading::UnitTestDispatcherScope, Arc<ManualRenderTimer>, Arc<DefaultRenderLoop>) {
309        let scope = Dispatcher::unit_test_scope();
310        let timer = Arc::new(ManualRenderTimer::default());
311        let render_loop = DefaultRenderLoop::new(timer.clone());
312        (scope, timer, render_loop)
313    }
314
315    #[test]
316    fn from_timer_reports_where_the_timer_runs() {
317        let background = Arc::new(ManualRenderTimer { runs_in_background: true, ..Default::default() });
318        assert!(RenderLoop::from_timer(background).runs_in_background());
319        assert!(!RenderLoop::from_timer(Arc::new(ManualRenderTimer::default())).runs_in_background());
320    }
321
322    #[test]
323    fn a_frame_out_of_turn_is_asked_of_the_timer_and_not_rendered_by_the_caller() {
324        // Not from upstream.
325        let (_scope, timer, render_loop) = setup();
326        let task = Task::new(true);
327        render_loop.add(task.clone());
328
329        render_loop.request_frame_out_of_turn();
330        assert_eq!(timer.out_of_turn_count.load(Ordering::SeqCst), 1);
331        assert_eq!(task.renders(), 0);
332    }
333
334    #[test]
335    fn timer_is_not_started_without_tasks() {
336        let (_scope, timer, render_loop) = setup();
337        assert!(!timer.is_running());
338        render_loop.wakeup();
339        assert!(!timer.is_running());
340        assert_eq!(timer.set_count.load(Ordering::SeqCst), 0);
341    }
342
343    #[test]
344    fn first_task_starts_the_timer_and_last_removal_stops_it() {
345        let (_scope, timer, render_loop) = setup();
346        let first = Task::new(true);
347        let second = Task::new(true);
348        let first_handle: Arc<dyn IRenderLoopTask> = first.clone();
349        let second_handle: Arc<dyn IRenderLoopTask> = second.clone();
350
351        render_loop.add(first_handle.clone());
352        assert!(timer.is_running());
353        render_loop.add(second_handle.clone());
354        assert_eq!(timer.set_count.load(Ordering::SeqCst), 1);
355
356        timer.trigger_tick();
357        assert_eq!((first.renders(), second.renders()), (1, 1));
358
359        render_loop.remove(&first_handle);
360        assert!(timer.is_running());
361        timer.trigger_tick();
362        assert_eq!((first.renders(), second.renders()), (1, 2));
363
364        // Removing a task that is not registered changes nothing.
365        render_loop.remove(&first_handle);
366        assert!(timer.is_running());
367
368        render_loop.remove(&second_handle);
369        assert!(!timer.is_running());
370        assert_eq!(timer.clear_count.load(Ordering::SeqCst), 1);
371
372        // Adding again restarts the timer.
373        render_loop.add(first_handle);
374        assert!(timer.is_running());
375        assert_eq!(timer.set_count.load(Ordering::SeqCst), 2);
376    }
377
378    #[test]
379    fn timer_keeps_running_while_a_task_wants_the_next_tick() {
380        let (_scope, timer, render_loop) = setup();
381        let idle = Task::new(false);
382        let busy = Task::new(true);
383        render_loop.add(idle.clone());
384        render_loop.add(busy.clone());
385
386        timer.trigger_tick();
387        timer.trigger_tick();
388        assert!(timer.is_running());
389        assert_eq!((idle.renders(), busy.renders()), (2, 2));
390
391        busy.wants_next_tick.store(false, Ordering::SeqCst);
392        timer.trigger_tick();
393        assert!(!timer.is_running());
394        assert_eq!((idle.renders(), busy.renders()), (3, 3));
395    }
396
397    #[test]
398    fn timer_stops_when_no_task_wants_the_next_tick_and_wakeup_restarts_it() {
399        let (_scope, timer, render_loop) = setup();
400        let task = Task::new(false);
401        render_loop.add(task.clone());
402        assert!(timer.is_running());
403
404        timer.trigger_tick();
405        assert_eq!(task.renders(), 1);
406        assert!(!timer.is_running());
407        assert_eq!(timer.clear_count.load(Ordering::SeqCst), 1);
408
409        render_loop.wakeup();
410        assert!(timer.is_running());
411        assert_eq!(timer.set_count.load(Ordering::SeqCst), 2);
412        timer.trigger_tick();
413        assert_eq!(task.renders(), 2);
414        assert!(!timer.is_running());
415    }
416
417    #[test]
418    fn wakeup_before_a_tick_is_consumed_by_that_tick() {
419        let (_scope, timer, render_loop) = setup();
420        let task = Task::new(false);
421        render_loop.add(task.clone());
422
423        // The timer is already running, so this only records a pending wakeup.
424        render_loop.wakeup();
425        timer.trigger_tick();
426        assert!(!timer.is_running());
427    }
428
429    #[test]
430    fn wakeup_during_a_tick_keeps_the_timer_running_for_one_more_tick() {
431        let (_scope, timer, render_loop) = setup();
432        let task = Task::new(false);
433        let weak_loop = Arc::downgrade(&render_loop);
434        let wake = Arc::new(AtomicBool::new(true));
435        let w = wake.clone();
436        *lock(&task.on_render) = Some(Box::new(move || {
437            if w.load(Ordering::SeqCst) {
438                weak_loop.upgrade().unwrap().wakeup();
439            }
440        }));
441        render_loop.add(task.clone());
442
443        timer.trigger_tick();
444        assert!(timer.is_running());
445        // The timer was never restarted, it just was not stopped.
446        assert_eq!(timer.set_count.load(Ordering::SeqCst), 1);
447
448        wake.store(false, Ordering::SeqCst);
449        timer.trigger_tick();
450        assert!(!timer.is_running());
451        assert_eq!(task.renders(), 2);
452    }
453
454    #[test]
455    fn late_ticks_after_the_timer_was_stopped_are_dropped() {
456        let (_scope, timer, render_loop) = setup();
457        let task = Task::new(true);
458        let handle: Arc<dyn IRenderLoopTask> = task.clone();
459        render_loop.add(handle.clone());
460        let late_tick = timer.tick().unwrap();
461        render_loop.remove(&handle);
462        assert!(!timer.is_running());
463
464        late_tick(Duration::ZERO);
465        assert_eq!(task.renders(), 0);
466    }
467
468    #[test]
469    fn tick_is_not_reentrant() {
470        let (_scope, timer, render_loop) = setup();
471        let task = Task::new(true);
472        let nested_timer = timer.clone();
473        *lock(&task.on_render) = Some(Box::new(move || nested_timer.trigger_tick()));
474        render_loop.add(task.clone());
475
476        timer.trigger_tick();
477        assert_eq!(task.renders(), 1);
478        timer.trigger_tick();
479        assert_eq!(task.renders(), 2);
480    }
481
482    #[test]
483    fn tick_is_skipped_while_another_thread_is_ticking() {
484        let (_scope, timer, render_loop) = setup();
485        let task = Task::new(true);
486        let (entered_tx, entered_rx) = std::sync::mpsc::channel::<()>();
487        let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
488        let entered_tx = Mutex::new(entered_tx);
489        let release_rx = Mutex::new(release_rx);
490        let block = Arc::new(AtomicBool::new(true));
491        let b = block.clone();
492        *lock(&task.on_render) = Some(Box::new(move || {
493            if b.swap(false, Ordering::SeqCst) {
494                lock(&entered_tx).send(()).unwrap();
495                lock(&release_rx).recv().unwrap();
496            }
497        }));
498        render_loop.add(task.clone());
499
500        let background_timer = timer.clone();
501        let background = std::thread::spawn(move || background_timer.trigger_tick());
502        entered_rx.recv().unwrap();
503
504        // The other thread is inside the tick: this one is dropped.
505        timer.trigger_tick();
506        assert_eq!(task.renders(), 1);
507
508        release_tx.send(()).unwrap();
509        background.join().unwrap();
510        timer.trigger_tick();
511        assert_eq!(task.renders(), 2);
512    }
513
514    #[test]
515    fn tasks_can_be_removed_while_rendering() {
516        let (_scope, timer, render_loop) = setup();
517        let first = Task::new(true);
518        let second = Task::new(true);
519        let second_handle: Arc<dyn IRenderLoopTask> = second.clone();
520        let weak_loop = Arc::downgrade(&render_loop);
521        let to_remove = second_handle.clone();
522        *lock(&first.on_render) = Some(Box::new(move || weak_loop.upgrade().unwrap().remove(&to_remove)));
523        render_loop.add(first.clone());
524        render_loop.add(second_handle);
525
526        // The tick works on a copy of the list, so the removed task is still
527        // rendered by the tick that removes it.
528        timer.trigger_tick();
529        assert_eq!((first.renders(), second.renders()), (1, 1));
530        timer.trigger_tick();
531        assert_eq!((first.renders(), second.renders()), (2, 1));
532    }
533
534    #[test]
535    fn a_panicking_task_does_not_break_the_loop() {
536        let (_scope, timer, render_loop) = setup();
537        let task = Task::new(true);
538        let fail = Arc::new(AtomicBool::new(true));
539        let f = fail.clone();
540        *lock(&task.on_render) = Some(Box::new(move || {
541            if f.swap(false, Ordering::SeqCst) {
542                panic!("render failed");
543            }
544        }));
545        render_loop.add(task.clone());
546
547        timer.trigger_tick();
548        assert_eq!(task.renders(), 1);
549        assert!(timer.is_running());
550
551        timer.trigger_tick();
552        assert_eq!(task.renders(), 2);
553    }
554
555    #[test]
556    fn dropping_a_running_loop_stops_the_timer() {
557        let (_scope, timer, render_loop) = setup();
558        render_loop.add(Task::new(true));
559        assert!(timer.is_running());
560        drop(render_loop);
561        assert!(!timer.is_running());
562    }
563}