use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use super::{IRenderTimer, RenderTimerTick};
use crate::reactive::IDisposable;
pub type RenderTimerSubscription = Arc<dyn IDisposable + Send + Sync>;
pub trait DefaultRenderTimerImpl: Send + Sync {
fn runs_in_background(&self) -> bool {
true
}
fn start_core(&self, timer: &DefaultRenderTimer, tick: RenderTimerTick) -> RenderTimerSubscription;
}
pub struct DefaultRenderTimer {
tick: Arc<Mutex<Option<RenderTimerTick>>>,
subscription: Mutex<Option<RenderTimerSubscription>>,
frames_per_second: i32,
overrides: Box<dyn DefaultRenderTimerImpl>,
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
impl DefaultRenderTimer {
#[cfg(not(target_family = "wasm"))]
pub fn new(frames_per_second: i32) -> Self {
Self::with_impl(frames_per_second, Box::new(standard_timer::StandardTimerImpl))
}
pub fn with_impl(frames_per_second: i32, overrides: Box<dyn DefaultRenderTimerImpl>) -> Self {
Self { tick: Arc::new(Mutex::new(None)), subscription: Mutex::new(None), frames_per_second, overrides }
}
pub fn frames_per_second(&self) -> i32 {
self.frames_per_second
}
fn internal_tick(&self) -> RenderTimerTick {
let slot = self.tick.clone();
Arc::new(move |tick_count| {
let tick = lock(&slot).clone();
if let Some(tick) = tick {
tick(tick_count);
}
})
}
}
impl IRenderTimer for DefaultRenderTimer {
fn tick(&self) -> Option<RenderTimerTick> {
lock(&self.tick).clone()
}
fn set_tick(&self, value: Option<RenderTimerTick>) {
let mut subscription = lock(&self.subscription);
match value {
Some(value) => {
*lock(&self.tick) = Some(value);
if subscription.is_none() {
*subscription = Some(self.overrides.start_core(self, self.internal_tick()));
}
}
None => {
if let Some(subscription) = subscription.take() {
subscription.dispose();
}
*lock(&self.tick) = None;
}
}
}
fn runs_in_background(&self) -> bool {
self.overrides.runs_in_background()
}
}
#[cfg(not(target_family = "wasm"))]
mod standard_timer {
use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError};
use std::thread;
use std::time::{Duration, Instant};
use super::{DefaultRenderTimer, DefaultRenderTimerImpl, RenderTimerSubscription};
use crate::reactive::IDisposable;
use crate::rendering::RenderTimerTick;
fn tick_count() -> Duration {
static START: OnceLock<Instant> = OnceLock::new();
Duration::from_millis(START.get_or_init(Instant::now).elapsed().as_millis() as u64)
}
struct PeriodicTimer {
stopped: Mutex<bool>,
wakeup: Condvar,
}
impl IDisposable for PeriodicTimer {
fn dispose(&self) {
*self.stopped.lock().unwrap_or_else(PoisonError::into_inner) = true;
self.wakeup.notify_all();
}
}
pub(super) struct StandardTimerImpl;
impl DefaultRenderTimerImpl for StandardTimerImpl {
fn start_core(&self, timer: &DefaultRenderTimer, tick: RenderTimerTick) -> RenderTimerSubscription {
let interval = match Duration::try_from_secs_f64(1.0 / f64::from(timer.frames_per_second())) {
Ok(interval) => interval,
Err(_) => panic!("frames_per_second must be a positive number."),
};
let state = Arc::new(PeriodicTimer { stopped: Mutex::new(false), wakeup: Condvar::new() });
let thread_state = state.clone();
tick_count();
thread::Builder::new()
.name("RenderTimer".to_string())
.spawn(move || {
let mut due = Instant::now() + interval;
loop {
{
let mut stopped = thread_state.stopped.lock().unwrap_or_else(PoisonError::into_inner);
loop {
if *stopped {
return;
}
let now = Instant::now();
if now >= due {
break;
}
stopped = thread_state
.wakeup
.wait_timeout(stopped, due - now)
.unwrap_or_else(PoisonError::into_inner)
.0;
}
}
tick(tick_count());
due += interval;
let now = Instant::now();
if due < now {
due = now;
}
}
})
.expect("failed to start the render timer thread");
state
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::Duration;
#[derive(Default)]
struct Recorded {
starts: AtomicI32,
disposals: AtomicI32,
core_tick: Mutex<Option<RenderTimerTick>>,
}
struct RecordingImpl(Arc<Recorded>);
struct RecordingSubscription(Arc<Recorded>);
impl IDisposable for RecordingSubscription {
fn dispose(&self) {
self.0.disposals.fetch_add(1, Ordering::SeqCst);
}
}
impl DefaultRenderTimerImpl for RecordingImpl {
fn runs_in_background(&self) -> bool {
false
}
fn start_core(&self, timer: &DefaultRenderTimer, tick: RenderTimerTick) -> RenderTimerSubscription {
assert_eq!(timer.frames_per_second(), 30);
self.0.starts.fetch_add(1, Ordering::SeqCst);
*lock(&self.0.core_tick) = Some(tick);
Arc::new(RecordingSubscription(self.0.clone()))
}
}
#[test]
fn setting_tick_starts_once_and_clearing_it_disposes_the_subscription() {
let recorded = Arc::new(Recorded::default());
let timer = DefaultRenderTimer::with_impl(30, Box::new(RecordingImpl(recorded.clone())));
assert!(!timer.runs_in_background());
assert!(timer.tick().is_none());
let first = Arc::new(AtomicI32::new(0));
let second = Arc::new(AtomicI32::new(0));
let f = first.clone();
timer.set_tick(Some(Arc::new(move |time| {
assert_eq!(time, Duration::from_millis(7));
f.fetch_add(1, Ordering::SeqCst);
})));
assert_eq!(recorded.starts.load(Ordering::SeqCst), 1);
assert!(timer.tick().is_some());
let core_tick = lock(&recorded.core_tick).clone().unwrap();
core_tick(Duration::from_millis(7));
assert_eq!(first.load(Ordering::SeqCst), 1);
let s = second.clone();
timer.set_tick(Some(Arc::new(move |_| {
s.fetch_add(1, Ordering::SeqCst);
})));
assert_eq!(recorded.starts.load(Ordering::SeqCst), 1);
core_tick(Duration::from_millis(7));
assert_eq!((first.load(Ordering::SeqCst), second.load(Ordering::SeqCst)), (1, 1));
timer.set_tick(None);
assert_eq!(recorded.disposals.load(Ordering::SeqCst), 1);
assert!(timer.tick().is_none());
core_tick(Duration::from_millis(7));
assert_eq!(second.load(Ordering::SeqCst), 1);
timer.set_tick(None);
assert_eq!(recorded.disposals.load(Ordering::SeqCst), 1);
timer.set_tick(Some(Arc::new(|_| {})));
assert_eq!(recorded.starts.load(Ordering::SeqCst), 2);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn standard_timer_ticks_in_the_background_until_stopped() {
let timer = DefaultRenderTimer::new(500);
assert!(timer.runs_in_background());
assert_eq!(timer.frames_per_second(), 500);
let (sender, receiver) = std::sync::mpsc::channel();
let sender = Mutex::new(sender);
timer.set_tick(Some(Arc::new(move |time| {
let _ = lock(&sender).send(time);
})));
let first = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
let second = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
assert!(second >= first);
timer.set_tick(None);
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
match receiver.recv_timeout(Duration::from_millis(50)) {
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
_ => assert!(std::time::Instant::now() < deadline, "the timer thread did not stop"),
}
}
}
}