ferroui_base/rendering/
thread_proxy_render_timer.rs1#![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
12struct Shared {
14 tick: Mutex<Option<RenderTimerTick>>,
15 stopwatch: OnceLock<Instant>,
17 auto_reset_event: AutoResetEvent,
18 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
38pub struct ThreadProxyRenderTimer {
41 inner: Arc<dyn IRenderTimer>,
42 shared: Arc<Shared>,
43 max_stack_size: usize,
44 lock: Mutex<bool>,
46 active: AtomicBool,
47 inner_tick: RenderTimerTick,
48}
49
50impl ThreadProxyRenderTimer {
51 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 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 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 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 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}