ferroui_base/rendering/
default_render_timer.rs1use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
2
3use super::{IRenderTimer, RenderTimerTick};
4use crate::reactive::IDisposable;
5
6pub type RenderTimerSubscription = Arc<dyn IDisposable + Send + Sync>;
10
11pub trait DefaultRenderTimerImpl: Send + Sync {
13 fn runs_in_background(&self) -> bool {
15 true
16 }
17
18 fn start_core(&self, timer: &DefaultRenderTimer, tick: RenderTimerTick) -> RenderTimerSubscription;
23}
24
25pub struct DefaultRenderTimer {
30 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 #[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 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 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 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 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 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 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 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 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 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}