use crate::ecs::SimTiming;
use std::sync::OnceLock;
use std::time::Instant;
pub(crate) fn monotonic_micros() -> u64 {
static EPOCH: OnceLock<Instant> = OnceLock::new();
EPOCH.get_or_init(Instant::now).elapsed().as_micros() as u64
}
const MAX_TICKS_PER_FRAME: u32 = 5;
#[derive(Debug, Default)]
pub(crate) struct SimClock {
last: Option<Instant>,
accumulator: f32,
}
impl SimClock {
pub(crate) fn advance(&mut self, now: Instant, paused: bool) -> SimTiming {
let elapsed = self
.last
.map(|t| now.duration_since(t).as_secs_f32())
.unwrap_or(0.0);
self.last = Some(now);
self.advance_by(elapsed, paused)
}
fn advance_by(&mut self, elapsed: f32, paused: bool) -> SimTiming {
let tick_dt = SimTiming::TICK_DT;
if !paused {
self.accumulator += elapsed.max(0.0);
}
let cap = MAX_TICKS_PER_FRAME as f32 * tick_dt;
self.accumulator = self.accumulator.min(cap);
let ticks = if paused {
0
} else {
(self.accumulator / tick_dt) as u32
};
self.accumulator -= ticks as f32 * tick_dt;
SimTiming {
ticks,
tick_dt,
alpha: (self.accumulator / tick_dt).clamp(0.0, 1.0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f32 = SimTiming::TICK_DT;
#[test]
fn monotonic_micros_never_goes_backwards() {
let first = monotonic_micros();
assert!(monotonic_micros() >= first);
}
#[test]
fn accumulates_whole_ticks_and_keeps_the_remainder() {
let mut clock = SimClock::default();
let t = clock.advance_by(DT * 2.5, false);
assert_eq!(t.ticks, 2);
assert!((t.alpha - 0.5).abs() < 1.0e-4, "alpha = {}", t.alpha);
let t = clock.advance_by(DT * 0.6, false);
assert_eq!(t.ticks, 1);
assert!((t.alpha - 0.1).abs() < 1.0e-3, "alpha = {}", t.alpha);
}
#[test]
fn short_frames_emit_zero_ticks_with_growing_alpha() {
let mut clock = SimClock::default();
let a = clock.advance_by(DT * 0.4, false);
assert_eq!(a.ticks, 0);
let b = clock.advance_by(DT * 0.4, false);
assert_eq!(b.ticks, 0);
assert!(b.alpha > a.alpha, "alpha advances between un-ticked frames");
let c = clock.advance_by(DT * 0.4, false);
assert_eq!(c.ticks, 1, "the third short frame crosses a tick");
}
#[test]
fn a_hitch_is_clamped_to_the_tick_cap() {
let mut clock = SimClock::default();
let t = clock.advance_by(2.0, false);
assert_eq!(t.ticks, MAX_TICKS_PER_FRAME);
assert!(t.alpha < 1.0e-4, "the overflow past the cap is dropped");
}
#[test]
fn pause_emits_no_ticks_and_holds_the_accumulator() {
let mut clock = SimClock::default();
clock.advance_by(DT * 0.5, false);
let frozen = clock.advance_by(DT * 20.0, true);
assert_eq!(frozen.ticks, 0);
assert!(
(frozen.alpha - 0.5).abs() < 1.0e-4,
"the paused blend holds at the pre-pause remainder"
);
let resumed = clock.advance_by(DT, false);
assert_eq!(resumed.ticks, 1);
}
#[test]
fn first_frame_has_no_elapsed_time() {
let mut clock = SimClock::default();
let t = clock.advance(Instant::now(), false);
assert_eq!(t.ticks, 0);
}
}