use core::cell::Cell;
use core::time::Duration;
use std::time::Instant;
use crate::platform::types::{FRAME_INTERVAL_MS, IDLE_FRAME_INTERVAL_MS};
#[cfg(not(alloc_frugal))]
mod inner {
use core::sync::atomic::{AtomicU64, Ordering};
use core::time::Duration;
use std::sync::{Condvar, Mutex, OnceLock};
struct WakeState {
generation: AtomicU64,
mutex: Mutex<()>,
ready: Condvar,
}
static WAKE: OnceLock<WakeState> = OnceLock::new();
fn state() -> &'static WakeState {
WAKE.get_or_init(|| WakeState {
generation: AtomicU64::new(0),
mutex: Mutex::new(()),
ready: Condvar::new(),
})
}
pub(crate) fn wake() {
let state = state();
state.generation.fetch_add(1, Ordering::SeqCst);
let _guard = state.mutex.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
state.ready.notify_all();
}
#[allow(dead_code)]
pub(crate) fn wait_timeout(timeout: Duration) -> bool {
let state = state();
let observed = state.generation.load(Ordering::SeqCst);
let guard = state.mutex.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if state.generation.load(Ordering::SeqCst) != observed {
return true;
}
let (_guard, result) = state
.ready
.wait_timeout(guard, timeout)
.unwrap_or_else(|poisoned| poisoned.into_inner());
!result.timed_out()
}
pub(crate) fn generation() -> u64 {
state().generation.load(Ordering::SeqCst)
}
pub(crate) fn wait_after(observed: u64, timeout: Duration) {
let state = state();
let guard = state.mutex.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if state.generation.load(Ordering::SeqCst) != observed {
return;
}
let _ = state.ready.wait_timeout(guard, timeout);
}
}
#[cfg(alloc_frugal)]
mod inner {
use core::time::Duration;
pub(crate) fn wake() {}
pub(crate) fn wait_timeout(timeout: Duration) -> bool {
std::thread::sleep(timeout);
false
}
}
pub(crate) use inner::wake;
#[cfg(not(alloc_frugal))]
pub(crate) use inner::{generation, wait_after};
thread_local! {
static PACING: Cell<Pacing> = const { Cell::new(Pacing { next_deadline: None }) };
#[allow(dead_code)]
static VSYNC_PACING: Cell<VsyncPacing> =
const { Cell::new(VsyncPacing { last_presented: None }) };
}
#[derive(Debug, Clone, Copy, Default)]
struct Pacing {
next_deadline: Option<Instant>,
}
impl Pacing {
fn plan(&self, now: Instant, interval: Duration) -> (Instant, Duration) {
let deadline = match self.next_deadline {
Some(deadline) => deadline,
None => now.checked_add(interval).unwrap_or(now),
};
(deadline, deadline.saturating_duration_since(now))
}
fn settle(&mut self, deadline: Instant, ran_at: Instant, behind: bool, interval: Duration) {
self.next_deadline = Some(if behind {
ran_at.checked_add(interval).unwrap_or(ran_at)
} else {
deadline.checked_add(interval).unwrap_or(ran_at)
});
}
fn resume(&mut self, deadline: Instant) {
self.next_deadline = Some(deadline);
}
fn idle(&mut self) {
self.next_deadline = None;
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, Default)]
struct VsyncPacing {
last_presented: Option<Instant>,
}
#[allow(dead_code)]
impl VsyncPacing {
fn wait_for(&self, now: Instant, deadline: Instant) -> Duration {
deadline.saturating_duration_since(now)
}
fn observe(&mut self, presented: Instant) -> u32 {
let delta = match self.last_presented {
Some(previous) => presented.saturating_duration_since(previous),
None => Duration::from_millis(FRAME_INTERVAL_MS),
};
self.last_presented = Some(presented);
u32::try_from(delta.as_millis()).unwrap_or(u32::MAX)
}
fn idle(&mut self) {
self.last_presented = None;
}
}
#[allow(dead_code)] pub(crate) fn presented_delta_ms(deadline: Option<Instant>) -> u32 {
VSYNC_PACING.with(|cell| {
let mut pacing = cell.get();
let delta = match deadline {
Some(deadline) => pacing.observe(deadline.max(Instant::now())),
None => {
pacing.idle();
FRAME_INTERVAL_MS as u32
}
};
cell.set(pacing);
delta
})
}
fn load_pacing() -> Pacing {
PACING.with(|pacing| pacing.get())
}
fn store_pacing(pacing: Pacing) {
PACING.with(|cell| cell.set(pacing));
}
pub(crate) fn pause_between_frames(needs_another_frame: bool) {
pause_between_frames_with(needs_another_frame, block_for);
}
pub(crate) fn pause_between_frames_with<F>(needs_another_frame: bool, block: F)
where
F: FnOnce(Duration) -> bool,
{
let interval = Duration::from_millis(FRAME_INTERVAL_MS);
let mut pacing = load_pacing();
if !needs_another_frame {
pacing.idle();
store_pacing(pacing);
let _ = block(Duration::from_millis(IDLE_FRAME_INTERVAL_MS));
return;
}
let now = Instant::now();
let (deadline, wait) = pacing.plan(now, interval);
if wait.is_zero() {
let _ = block(Duration::ZERO);
pacing.settle(deadline, now, true, interval);
store_pacing(pacing);
return;
}
if block(wait) {
pacing.resume(deadline);
store_pacing(pacing);
return;
}
pacing.settle(deadline, Instant::now(), false, interval);
store_pacing(pacing);
}
#[allow(dead_code)]
pub(crate) fn pause_between_frames_with_vsync<D, F>(
needs_another_frame: bool,
next_deadline: D,
block: F,
) -> Option<u32>
where
D: FnOnce() -> Option<Instant>,
F: FnOnce(Duration) -> bool,
{
if !needs_another_frame {
VSYNC_PACING.with(|cell| cell.set(VsyncPacing::default()));
let _ = block(Duration::from_millis(IDLE_FRAME_INTERVAL_MS));
return None;
}
let Some(deadline) = next_deadline() else {
VSYNC_PACING.with(|cell| cell.set(VsyncPacing::default()));
pause_between_frames_with(true, block);
return None;
};
let now = Instant::now();
let mut pacing = VSYNC_PACING.with(Cell::get);
let wait = pacing.wait_for(now, deadline);
if wait.is_zero() {
let _ = block(Duration::ZERO);
let delta = pacing.observe(now);
VSYNC_PACING.with(|cell| cell.set(pacing));
return Some(delta);
}
if block(wait) {
let delta = pacing.observe(Instant::now());
VSYNC_PACING.with(|cell| cell.set(pacing));
return Some(delta);
}
let delta = pacing.observe(deadline);
VSYNC_PACING.with(|cell| cell.set(pacing));
Some(delta)
}
#[cfg(all(windows, not(alloc_frugal)))]
fn block_for(timeout: Duration) -> bool {
let millis = timeout_millis_ceil(timeout);
let wait_result = unsafe {
winapi::um::winuser::MsgWaitForMultipleObjectsEx(
0,
core::ptr::null(),
millis,
winapi::um::winuser::QS_ALLINPUT,
winapi::um::winuser::MWMO_INPUTAVAILABLE,
)
};
wait_result != winapi::shared::winerror::WAIT_TIMEOUT
}
#[cfg(not(all(windows, not(alloc_frugal))))]
fn block_for(timeout: Duration) -> bool {
if timeout.is_zero() {
return false;
}
inner::wait_timeout(timeout)
}
#[cfg(all(windows, not(alloc_frugal)))]
fn timeout_millis_ceil(timeout: Duration) -> u32 {
let millis = timeout.as_micros().div_ceil(1_000);
u32::try_from(millis).unwrap_or(u32::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::Vec;
fn interval() -> Duration {
Duration::from_millis(FRAME_INTERVAL_MS)
}
fn at(base: Instant, millis: u64) -> Instant {
base + Duration::from_millis(millis)
}
fn simulate(
frames: usize,
mut work: impl FnMut(usize) -> u64,
) -> (Vec<Duration>, Vec<Instant>) {
let base = Instant::now();
let mut now = base;
let mut pacing = Pacing::default();
let mut waits = Vec::with_capacity(frames);
let mut starts = Vec::with_capacity(frames + 1);
starts.push(now);
for index in 0..frames {
now += Duration::from_millis(work(index));
let (deadline, wait) = pacing.plan(now, interval());
waits.push(wait);
if !wait.is_zero() {
now = deadline;
}
pacing.settle(deadline, now, wait.is_zero(), interval());
starts.push(now);
}
(waits, starts)
}
#[test]
fn the_frame_interval_is_unchanged() {
assert_eq!(FRAME_INTERVAL_MS, 16);
}
#[test]
fn a_steady_loop_waits_exactly_one_interval_per_frame() {
let (waits, starts) = simulate(8, |_| 4);
for pair in starts.windows(2).skip(1) {
assert_eq!(pair[1] - pair[0], interval(), "frame starts drifted off the grid");
}
assert_eq!(waits[0], interval(), "the first frame must get a full interval");
for wait in &waits[1..] {
assert_eq!(*wait, interval() - Duration::from_millis(4));
}
}
#[test]
fn a_late_frame_does_not_accumulate_drift() {
let (waits, starts) = simulate(12, |index| if index == 4 { 40 } else { 4 });
assert_eq!(waits.iter().filter(|wait| wait.is_zero()).count(), 1);
let catch_up = waits.iter().position(|wait| wait.is_zero()).expect("no catch-up") + 1;
for pair in starts[catch_up..].windows(2) {
assert_eq!(pair[1] - pair[0], interval(), "the overrun leaked into later frames");
}
}
#[test]
fn a_late_frame_is_caught_up_exactly_once() {
let (waits, _) = simulate(40, |index| if index == 10 { 40 } else { 2 });
assert_eq!(waits.iter().filter(|wait| wait.is_zero()).count(), 1);
}
#[test]
fn a_steady_run_never_spins_on_zero_waits() {
let (waits, _) = simulate(240, |_| 3);
assert!(waits.iter().all(|wait| !wait.is_zero()), "the loop spun instead of blocking");
for wait in &waits[1..] {
assert_eq!(*wait, interval() - Duration::from_millis(3));
}
}
#[test]
fn an_early_wake_returns_immediately_and_keeps_the_deadline() {
let base = Instant::now();
let mut pacing = Pacing::default();
let (deadline, wait) = pacing.plan(base, interval());
assert_eq!(wait, interval());
pacing.resume(deadline);
let woke_at = at(base, 4);
let (deadline_again, remaining) = pacing.plan(woke_at, interval());
assert_eq!(deadline_again, deadline, "an early wake moved the deadline");
assert_eq!(remaining, interval() - Duration::from_millis(4));
}
#[test]
fn an_idle_pause_clears_the_grid() {
let base = Instant::now();
let mut pacing = Pacing::default();
let (deadline, _) = pacing.plan(base, interval());
pacing.settle(deadline, at(base, FRAME_INTERVAL_MS), false, interval());
assert!(pacing.next_deadline.is_some());
pacing.idle();
let (fresh, wait) = pacing.plan(base, interval());
assert_eq!(fresh, at(base, FRAME_INTERVAL_MS));
assert_eq!(wait, interval());
}
#[test]
fn the_blocking_primitive_gets_the_remaining_deadline() {
store_pacing(Pacing::default());
let requested = Cell::new(Duration::ZERO);
pause_between_frames_with(true, |timeout| {
requested.set(timeout);
true
});
assert_eq!(requested.get(), interval(), "the first owed frame must wait one interval");
let remaining = Cell::new(Duration::ZERO);
pause_between_frames_with(true, |timeout| {
remaining.set(timeout);
true
});
assert!(remaining.get() > Duration::ZERO);
assert!(remaining.get() <= interval());
}
#[test]
fn an_idle_pause_waits_only_for_the_backstop_and_rebases() {
store_pacing(Pacing::default());
pause_between_frames_with(true, |_| true);
let idle = Cell::new(Duration::ZERO);
pause_between_frames_with(false, |timeout| {
idle.set(timeout);
false
});
assert_eq!(idle.get(), Duration::from_millis(IDLE_FRAME_INTERVAL_MS));
let fresh = Cell::new(Duration::ZERO);
pause_between_frames_with(true, |timeout| {
fresh.set(timeout);
false
});
assert_eq!(fresh.get(), interval(), "the idle pause did not rebase the grid");
}
#[test]
fn a_backend_without_vsync_keeps_the_fixed_grid() {
store_pacing(Pacing::default());
let requested = Cell::new(Duration::ZERO);
pause_between_frames_with(true, |timeout| {
requested.set(timeout);
false
});
assert_eq!(requested.get(), interval());
}
#[test]
fn a_mock_vsync_deadline_produces_the_expected_wait() {
let base = Instant::now();
let mut pacing = VsyncPacing::default();
let first = at(base, FRAME_INTERVAL_MS);
assert_eq!(pacing.wait_for(base, first), interval());
assert_eq!(pacing.observe(first), FRAME_INTERVAL_MS as u32);
let after_work = at(first, 4);
let second = at(first, FRAME_INTERVAL_MS);
assert_eq!(pacing.wait_for(after_work, second), interval() - Duration::from_millis(4));
assert_eq!(pacing.observe(second), FRAME_INTERVAL_MS as u32);
}
#[test]
fn a_mock_vsync_deadline_paces_the_blocking_wait() {
store_pacing(Pacing::default());
VSYNC_PACING.with(|cell| cell.set(VsyncPacing::default()));
let mock = Duration::from_millis(200);
let deadline = Instant::now() + mock;
let requested = Cell::new(Duration::ZERO);
let delta = pause_between_frames_with_vsync(
true,
|| Some(deadline),
|timeout| {
requested.set(timeout);
false
},
);
assert!(delta.is_some(), "a live deadline must report a measured delta");
assert!(requested.get() > interval(), "the fixed grid overrode the mock deadline");
assert!(requested.get() <= mock, "the wait ran past the mock deadline");
}
#[test]
fn early_presented_frames_do_not_accumulate_drift() {
let base = Instant::now();
let mut pacing = VsyncPacing::default();
let mut deadline = at(base, 12);
let mut now = at(base, 3);
let mut waited = Duration::ZERO;
assert_eq!(pacing.wait_for(now, deadline), Duration::from_millis(9));
waited += Duration::from_millis(9);
assert_eq!(pacing.observe(deadline), FRAME_INTERVAL_MS as u32);
now = deadline;
for _ in 1..5 {
deadline += Duration::from_millis(12);
let wait = pacing.wait_for(now, deadline);
assert_eq!(wait, Duration::from_millis(12));
waited += wait;
assert_eq!(pacing.observe(deadline), 12);
now = deadline;
}
assert_eq!(waited, at(base, 60).saturating_duration_since(at(base, 3)));
}
#[test]
fn late_presented_frames_do_not_accumulate_drift() {
let base = Instant::now();
let mut pacing = VsyncPacing::default();
let first = at(base, FRAME_INTERVAL_MS);
assert_eq!(pacing.wait_for(base, first), interval());
assert_eq!(pacing.observe(first), FRAME_INTERVAL_MS as u32);
let late_start = at(base, 34);
let missed = at(base, 32);
assert_eq!(pacing.wait_for(late_start, missed), Duration::ZERO);
assert_eq!(pacing.observe(late_start), 18);
let next = at(base, 48);
assert_eq!(pacing.wait_for(late_start, next), Duration::from_millis(14));
assert_eq!(pacing.observe(next), 14);
let after = at(base, 52);
let following = at(base, 64);
assert_eq!(pacing.wait_for(after, following), Duration::from_millis(12));
assert_eq!(pacing.observe(following), FRAME_INTERVAL_MS as u32);
}
#[test]
fn a_missing_vsync_deadline_falls_back_to_the_fixed_grid() {
store_pacing(Pacing::default());
VSYNC_PACING.with(|cell| cell.set(VsyncPacing::default()));
let requested = Cell::new(Duration::ZERO);
let delta = pause_between_frames_with_vsync(
true,
|| None,
|timeout| {
requested.set(timeout);
false
},
);
assert_eq!(delta, None, "a backend without a deadline must not report a vsync delta");
assert_eq!(requested.get(), interval(), "the fixed grid must still pace the frame");
}
#[test]
fn an_early_vsync_wake_measures_elapsed_time_without_shifting_the_grid() {
store_pacing(Pacing::default());
VSYNC_PACING.with(|cell| cell.set(VsyncPacing::default()));
let deadline = Instant::now() + interval();
let woke = pause_between_frames_with_vsync(true, || Some(deadline), |_| true)
.expect("an early wake must still report a measured delta");
assert!(woke <= FRAME_INTERVAL_MS as u32, "the delta exceeded one interval");
let remaining = Cell::new(Duration::ZERO);
let _ = pause_between_frames_with_vsync(
true,
|| Some(deadline),
|timeout| {
remaining.set(timeout);
false
},
);
assert!(remaining.get() > Duration::ZERO);
assert!(remaining.get() <= interval());
}
#[test]
fn presented_delta_reports_the_measured_period() {
VSYNC_PACING.with(|cell| cell.set(VsyncPacing::default()));
let base = Instant::now();
let step = Duration::from_secs(2);
let first = base + step;
let second = first + step;
let third = second + step;
assert_eq!(presented_delta_ms(Some(first)), FRAME_INTERVAL_MS as u32);
assert_eq!(presented_delta_ms(Some(second)), 2_000);
assert_eq!(presented_delta_ms(None), FRAME_INTERVAL_MS as u32);
assert_eq!(presented_delta_ms(Some(third)), FRAME_INTERVAL_MS as u32);
}
#[test]
fn a_frame_that_is_already_due_gets_a_zero_timeout() {
store_pacing(Pacing { next_deadline: Some(Instant::now()) });
let timeout = Cell::new(interval());
pause_between_frames_with(true, |requested| {
timeout.set(requested);
false
});
assert_eq!(timeout.get(), Duration::ZERO, "a frame that is already due must not wait");
let next = Cell::new(Duration::ZERO);
pause_between_frames_with(true, |requested| {
next.set(requested);
false
});
assert!(next.get() > Duration::ZERO);
assert!(next.get() <= interval());
}
}