Skip to main content

ferroui_base/rendering/
thread_proxy_render_timer.rs

1// The timer owns a thread; threads do not exist on bare WebAssembly.
2#![cfg(not(target_family = "wasm"))]
3
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Mutex, OnceLock, PoisonError, Weak};
6use std::thread;
7use std::time::Instant;
8
9use super::auto_reset_event::AutoResetEvent;
10use super::{IRenderTimer, RenderTimerTick};
11
12/// The state shared with the timer thread.
13struct Shared {
14    tick: Mutex<Option<RenderTimerTick>>,
15    /// Started together with the thread.
16    stopwatch: OnceLock<Instant>,
17    auto_reset_event: AutoResetEvent,
18    /// Set when the timer is dropped, to let the thread exit.
19    released: AtomicBool,
20}
21
22impl Shared {
23    fn render_timer_thread_func(&self) {
24        loop {
25            self.auto_reset_event.wait_one();
26            if self.released.load(Ordering::SeqCst) {
27                return;
28            }
29
30            let tick = self.tick.lock().unwrap_or_else(PoisonError::into_inner).clone();
31            if let Some(tick) = tick {
32                tick(self.stopwatch.get().map(Instant::elapsed).unwrap_or_default());
33            }
34        }
35    }
36}
37
38/// A render timer that moves the ticks of another timer to a dedicated
39/// thread.
40pub struct ThreadProxyRenderTimer {
41    inner: Arc<dyn IRenderTimer>,
42    shared: Arc<Shared>,
43    max_stack_size: usize,
44    /// Guards `registered` and every change of the tick and of `active`.
45    lock: Mutex<bool>,
46    active: AtomicBool,
47    inner_tick: RenderTimerTick,
48}
49
50impl ThreadProxyRenderTimer {
51    /// The stack size of the timer thread used by [`new`](Self::new).
52    pub const DEFAULT_MAX_STACK_SIZE: usize = 1024 * 1024;
53
54    pub fn new(inner: Arc<dyn IRenderTimer>) -> Arc<ThreadProxyRenderTimer> {
55        Self::with_max_stack_size(inner, Self::DEFAULT_MAX_STACK_SIZE)
56    }
57
58    pub fn with_max_stack_size(inner: Arc<dyn IRenderTimer>, max_stack_size: usize) -> Arc<ThreadProxyRenderTimer> {
59        Arc::new_cyclic(|weak_self: &Weak<ThreadProxyRenderTimer>| {
60            let this = weak_self.clone();
61            ThreadProxyRenderTimer {
62                inner,
63                shared: Arc::new(Shared {
64                    tick: Mutex::new(None),
65                    stopwatch: OnceLock::new(),
66                    auto_reset_event: AutoResetEvent::new(false),
67                    released: AtomicBool::new(false),
68                }),
69                max_stack_size,
70                lock: Mutex::new(false),
71                active: AtomicBool::new(false),
72                inner_tick: Arc::new(move |_| {
73                    if let Some(this) = this.upgrade() {
74                        this.inner_tick();
75                    }
76                }),
77            }
78        })
79    }
80
81    fn ensure_started(&self, registered: &mut bool) {
82        if !*registered {
83            *registered = true;
84            let _ = self.shared.stopwatch.set(Instant::now());
85            let shared = self.shared.clone();
86            thread::Builder::new()
87                .name("RenderTimerLoop".to_string())
88                .stack_size(self.max_stack_size)
89                .spawn(move || shared.render_timer_thread_func())
90                .expect("failed to start the render timer thread");
91        }
92    }
93
94    fn inner_tick(&self) {
95        {
96            let _guard = self.lock.lock().unwrap_or_else(PoisonError::into_inner);
97            if !self.active.load(Ordering::SeqCst) {
98                self.inner.set_tick(None);
99                return;
100            }
101        }
102        self.shared.auto_reset_event.set();
103    }
104}
105
106impl IRenderTimer for ThreadProxyRenderTimer {
107    fn tick(&self) -> Option<RenderTimerTick> {
108        self.shared.tick.lock().unwrap_or_else(PoisonError::into_inner).clone()
109    }
110
111    fn set_tick(&self, value: Option<RenderTimerTick>) {
112        let mut registered = self.lock.lock().unwrap_or_else(PoisonError::into_inner);
113        match value {
114            Some(value) => {
115                *self.shared.tick.lock().unwrap_or_else(PoisonError::into_inner) = Some(value);
116                self.active.store(true, Ordering::SeqCst);
117                self.ensure_started(&mut registered);
118                self.inner.set_tick(Some(self.inner_tick.clone()));
119            }
120            None => {
121                // Don't clear the inner timer's tick here — may be on the wrong thread.
122                // inner_tick will detect active=false and clear it on the correct thread.
123                self.active.store(false, Ordering::SeqCst);
124                *self.shared.tick.lock().unwrap_or_else(PoisonError::into_inner) = None;
125            }
126        }
127    }
128
129    fn runs_in_background(&self) -> bool {
130        true
131    }
132}
133
134impl Drop for ThreadProxyRenderTimer {
135    fn drop(&mut self) {
136        self.shared.released.store(true, Ordering::SeqCst);
137        self.shared.auto_reset_event.set();
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use std::sync::atomic::AtomicI32;
145    use std::sync::mpsc;
146    use std::thread::ThreadId;
147    use std::time::Duration;
148
149    #[derive(Default)]
150    struct ManualRenderTimer {
151        tick: Mutex<Option<RenderTimerTick>>,
152        clear_count: AtomicI32,
153    }
154
155    impl ManualRenderTimer {
156        fn trigger_tick(&self) {
157            let tick = self.tick.lock().unwrap().clone();
158            if let Some(tick) = tick {
159                tick(Duration::ZERO);
160            }
161        }
162    }
163
164    impl IRenderTimer for ManualRenderTimer {
165        fn tick(&self) -> Option<RenderTimerTick> {
166            self.tick.lock().unwrap().clone()
167        }
168
169        fn set_tick(&self, value: Option<RenderTimerTick>) {
170            if value.is_none() {
171                self.clear_count.fetch_add(1, Ordering::SeqCst);
172            }
173            *self.tick.lock().unwrap() = value;
174        }
175
176        fn runs_in_background(&self) -> bool {
177            false
178        }
179    }
180
181    #[test]
182    fn forwards_inner_ticks_to_its_own_thread() {
183        let inner = Arc::new(ManualRenderTimer::default());
184        let proxy = ThreadProxyRenderTimer::new(inner.clone());
185        assert!(proxy.runs_in_background());
186        assert!(proxy.tick().is_none());
187        assert!(inner.tick().is_none());
188
189        let (sender, receiver) = mpsc::channel::<(ThreadId, Duration)>();
190        let sender = Mutex::new(sender);
191        proxy.set_tick(Some(Arc::new(move |time| {
192            let _ = sender.lock().unwrap().send((thread::current().id(), time));
193        })));
194        assert!(proxy.tick().is_some());
195        assert!(inner.tick().is_some());
196
197        inner.trigger_tick();
198        let (first_thread, first_time) = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
199        assert_ne!(first_thread, thread::current().id());
200
201        inner.trigger_tick();
202        let (second_thread, second_time) = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
203        assert_eq!(first_thread, second_thread);
204        assert!(second_time >= first_time);
205
206        // Stopping does not touch the inner timer...
207        proxy.set_tick(None);
208        assert!(proxy.tick().is_none());
209        assert!(inner.tick().is_some());
210        assert_eq!(inner.clear_count.load(Ordering::SeqCst), 0);
211
212        // ...its next tick does, on the inner timer's own thread.
213        inner.trigger_tick();
214        assert!(inner.tick().is_none());
215        assert_eq!(inner.clear_count.load(Ordering::SeqCst), 1);
216        assert!(matches!(
217            receiver.recv_timeout(Duration::from_millis(50)),
218            Err(mpsc::RecvTimeoutError::Disconnected | mpsc::RecvTimeoutError::Timeout)
219        ));
220
221        // Restarting subscribes to the inner timer again.
222        let (sender, receiver) = mpsc::channel::<ThreadId>();
223        let sender = Mutex::new(sender);
224        proxy.set_tick(Some(Arc::new(move |_| {
225            let _ = sender.lock().unwrap().send(thread::current().id());
226        })));
227        inner.trigger_tick();
228        assert_eq!(receiver.recv_timeout(Duration::from_secs(10)).unwrap(), first_thread);
229    }
230}