use core::fmt;
use core::time::Duration;
use std::time::Instant;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct TickId(u64);
impl TickId {
pub const ZERO: Self = Self(0);
#[inline]
#[must_use]
pub const fn from_raw(raw: u64) -> Self {
Self(raw)
}
#[inline]
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
#[inline]
#[must_use]
pub const fn next(self) -> Self {
Self(self.0.saturating_add(1))
}
#[inline]
#[must_use]
pub const fn since(self, earlier: Self) -> u64 {
self.0.saturating_sub(earlier.0)
}
}
impl fmt::Display for TickId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "tick {}", self.0)
}
}
pub trait TimeSource: fmt::Debug {
fn elapsed(&self) -> Duration;
}
#[derive(Clone, Copy, Debug)]
pub struct MonotonicTime {
start: Instant,
}
impl MonotonicTime {
#[must_use]
pub fn new() -> Self {
Self {
start: Instant::now(),
}
}
}
impl Default for MonotonicTime {
fn default() -> Self {
Self::new()
}
}
impl TimeSource for MonotonicTime {
#[inline]
fn elapsed(&self) -> Duration {
self.start.elapsed()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ManualTime {
now: Duration,
}
impl ManualTime {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
now: Duration::ZERO,
}
}
#[inline]
pub const fn advance(&mut self, delta: Duration) {
self.now = self.now.saturating_add(delta);
}
#[inline]
pub fn advance_secs(&mut self, seconds: f64) {
self.advance(Duration::from_secs_f64(seconds));
}
#[inline]
pub const fn set(&mut self, now: Duration) {
self.now = now;
}
}
impl TimeSource for ManualTime {
#[inline]
fn elapsed(&self) -> Duration {
self.now
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct EventTime(Duration);
impl EventTime {
pub const ZERO: Self = Self(Duration::ZERO);
#[inline]
#[must_use]
pub const fn from_duration(since_epoch: Duration) -> Self {
Self(since_epoch)
}
#[inline]
#[must_use]
pub const fn from_micros(micros: u64) -> Self {
Self(Duration::from_micros(micros))
}
#[inline]
#[must_use]
pub const fn from_millis(millis: u64) -> Self {
Self(Duration::from_millis(millis))
}
#[inline]
#[must_use]
pub const fn as_duration(self) -> Duration {
self.0
}
#[inline]
#[must_use]
pub fn as_secs_f64(self) -> f64 {
self.0.as_secs_f64()
}
#[inline]
#[must_use]
pub fn saturating_since(self, earlier: Self) -> Duration {
self.0.saturating_sub(earlier.0)
}
#[inline]
#[must_use]
pub const fn saturating_add(self, delta: Duration) -> Self {
Self(self.0.saturating_add(delta))
}
}
impl From<Duration> for EventTime {
#[inline]
fn from(since_epoch: Duration) -> Self {
Self(since_epoch)
}
}
impl fmt::Display for EventTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "t+{:.6}s", self.0.as_secs_f64())
}
}
pub const DEFAULT_MAX_CATCH_UP_TICKS: u32 = 8;
const ALPHA_MAX: f32 = 1.0 - f32::EPSILON / 2.0;
pub struct FrameClock {
tick_nanos: u64,
accumulator_nanos: u64,
max_catch_up_ticks: u32,
last_update: Option<Duration>,
render_dt: Duration,
tick: TickId,
dropped_ticks: u64,
backwards_updates: u64,
}
impl FrameClock {
#[must_use]
pub fn new(tick_hz: u32) -> Self {
assert!(tick_hz > 0, "tick rate must be positive");
assert!(
tick_hz <= 1_000_000_000,
"tick rate must be at most 1 GHz, not {tick_hz} Hz"
);
Self::with_period(Duration::from_nanos(u64::from(1_000_000_000u32 / tick_hz)))
}
#[must_use]
pub fn with_period(period: Duration) -> Self {
let tick_nanos = u64::try_from(period.as_nanos()).expect("tick period must fit in u64 ns");
assert!(tick_nanos > 0, "tick period must be positive");
Self {
tick_nanos,
accumulator_nanos: 0,
max_catch_up_ticks: DEFAULT_MAX_CATCH_UP_TICKS,
last_update: None,
render_dt: Duration::ZERO,
tick: TickId::ZERO,
dropped_ticks: 0,
backwards_updates: 0,
}
}
#[inline]
pub const fn set_max_catch_up_ticks(&mut self, ticks: u32) {
assert!(ticks > 0, "max catch-up ticks must be positive");
self.max_catch_up_ticks = ticks;
}
pub fn update(&mut self, now: Duration) {
let previous = self.last_update.unwrap_or(now);
self.last_update = Some(now);
if now < previous {
self.backwards_updates += 1;
self.render_dt = Duration::ZERO;
return;
}
let delta = now - previous;
self.render_dt = delta;
let delta_nanos = u64::try_from(delta.as_nanos()).unwrap_or(u64::MAX);
self.accumulator_nanos = self.accumulator_nanos.saturating_add(delta_nanos);
let pending = self.accumulator_nanos / self.tick_nanos;
let cap = u64::from(self.max_catch_up_ticks);
if pending > cap {
let dropped = pending - cap;
self.dropped_ticks += dropped;
self.accumulator_nanos -= dropped * self.tick_nanos;
}
}
#[inline]
pub const fn consume_tick(&mut self) -> bool {
if self.accumulator_nanos >= self.tick_nanos {
self.accumulator_nanos -= self.tick_nanos;
self.tick = self.tick.next();
true
} else {
false
}
}
#[inline]
#[must_use]
pub const fn pending_ticks(&self) -> u32 {
(self.accumulator_nanos / self.tick_nanos) as u32
}
#[must_use]
pub const fn next_tick_due(&self) -> Option<Duration> {
let Some(last) = self.last_update else {
return None;
};
let short = self.tick_nanos.saturating_sub(self.accumulator_nanos);
Some(last.saturating_add(Duration::from_nanos(short)))
}
#[inline]
#[must_use]
pub const fn tick(&self) -> TickId {
self.tick
}
#[inline]
#[must_use]
pub const fn tick_dt(&self) -> Duration {
Duration::from_nanos(self.tick_nanos)
}
#[inline]
#[must_use]
pub fn tick_dt_secs(&self) -> f64 {
self.tick_nanos as f64 * 1e-9
}
#[inline]
#[must_use]
pub const fn render_dt(&self) -> Duration {
self.render_dt
}
#[inline]
#[must_use]
pub fn render_dt_secs(&self) -> f32 {
self.render_dt.as_secs_f32()
}
#[inline]
#[must_use]
pub fn alpha(&self) -> f32 {
let alpha = self.accumulator_nanos as f32 / self.tick_nanos as f32;
if alpha >= 1.0 { ALPHA_MAX } else { alpha }
}
#[inline]
#[must_use]
pub const fn dropped_ticks(&self) -> u64 {
self.dropped_ticks
}
#[inline]
#[must_use]
pub const fn backwards_updates(&self) -> u64 {
self.backwards_updates
}
}
impl fmt::Debug for FrameClock {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FrameClock")
.field("tick", &self.tick)
.field("tick_dt", &self.tick_dt())
.field("pending_ticks", &self.pending_ticks())
.field("render_dt", &self.render_dt)
.field("dropped_ticks", &self.dropped_ticks)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tick_count(clock: &mut FrameClock) -> u32 {
let mut ticks = 0;
while clock.consume_tick() {
ticks += 1;
}
ticks
}
#[test]
fn tick_ids_are_ordered_and_saturating() {
assert_eq!(TickId::ZERO.get(), 0);
assert_eq!(TickId::ZERO.next(), TickId::from_raw(1));
assert!(TickId::from_raw(2) > TickId::from_raw(1));
assert_eq!(TickId::from_raw(9).since(TickId::from_raw(4)), 5);
assert_eq!(TickId::from_raw(4).since(TickId::from_raw(9)), 0);
assert_eq!(TickId::from_raw(u64::MAX).next().get(), u64::MAX);
assert_eq!(TickId::from_raw(7).to_string(), "tick 7");
}
#[test]
fn manual_time_drives_the_clock() {
let mut time = ManualTime::new();
let mut clock = FrameClock::new(60);
assert_eq!(clock.tick(), TickId::ZERO);
clock.update(time.elapsed());
assert_eq!(tick_count(&mut clock), 0);
assert_eq!(clock.render_dt(), Duration::ZERO);
time.advance_secs(1.0);
clock.update(time.elapsed());
assert_eq!(clock.render_dt(), Duration::from_secs(1));
assert_eq!(tick_count(&mut clock), DEFAULT_MAX_CATCH_UP_TICKS);
assert_eq!(clock.dropped_ticks(), 52);
}
#[test]
fn steady_frame_rate_produces_exactly_one_tick_per_frame() {
let mut time = ManualTime::new();
let mut clock = FrameClock::new(60);
clock.update(time.elapsed());
let mut ticks = 0;
for _ in 0..600 {
time.advance(clock.tick_dt());
clock.update(time.elapsed());
ticks += tick_count(&mut clock);
}
assert_eq!(ticks, 600);
assert_eq!(clock.tick(), TickId::from_raw(600));
assert_eq!(clock.dropped_ticks(), 0);
}
#[test]
fn tick_count_matches_elapsed_time_at_an_awkward_frame_rate() {
let mut time = ManualTime::new();
let mut clock = FrameClock::new(60);
clock.update(time.elapsed());
let frame = Duration::from_nanos(1_000_000_000 / 144);
let frames = 1440u32; let mut ticks = 0;
for _ in 0..frames {
time.advance(frame);
clock.update(time.elapsed());
ticks += tick_count(&mut clock);
}
let expected = (frame * frames).as_nanos() / clock.tick_dt().as_nanos();
assert_eq!(u128::from(ticks), expected);
assert_eq!(clock.dropped_ticks(), 0);
}
#[test]
fn slow_frames_never_queue_more_than_the_elapsed_time() {
let mut time = ManualTime::new();
let mut clock = FrameClock::new(60);
clock.update(time.elapsed());
for _ in 0..10 {
time.advance(clock.tick_dt() * 2);
clock.update(time.elapsed());
assert_eq!(tick_count(&mut clock), 2);
}
}
#[test]
fn the_next_tick_is_due_when_the_accumulator_fills() {
let mut time = ManualTime::new();
let mut clock = FrameClock::new(60);
assert_eq!(clock.next_tick_due(), None, "no update, no timeline");
clock.update(time.elapsed());
assert_eq!(clock.next_tick_due(), Some(clock.tick_dt()));
time.advance(clock.tick_dt() / 4);
clock.update(time.elapsed());
assert_eq!(
clock.next_tick_due(),
Some(clock.tick_dt()),
"a quarter tick in, the deadline has not moved"
);
time.advance(clock.tick_dt());
clock.update(time.elapsed());
assert_eq!(
clock.next_tick_due(),
Some(time.elapsed()),
"a whole tick is pending, so it is due now"
);
assert_eq!(tick_count(&mut clock), 1);
assert_eq!(
clock.next_tick_due(),
Some(clock.tick_dt() * 2),
"and once it ran, the one after is due on the same grid"
);
}
#[test]
fn alpha_stays_in_range_and_tracks_the_partial_tick() {
let mut time = ManualTime::new();
let mut clock = FrameClock::new(60);
clock.update(time.elapsed());
assert_eq!(clock.alpha(), 0.0);
time.advance(clock.tick_dt() / 4);
clock.update(time.elapsed());
assert_eq!(tick_count(&mut clock), 0);
assert!((clock.alpha() - 0.25).abs() < 1e-6, "{}", clock.alpha());
time.advance(clock.tick_dt() * 3);
clock.update(time.elapsed());
assert!(clock.alpha() < 1.0);
assert!(clock.alpha() >= 0.0);
assert_eq!(clock.alpha(), ALPHA_MAX);
tick_count(&mut clock);
assert!((clock.alpha() - 0.25).abs() < 1e-6);
let mut seed = 0x2545_F491_4F6C_DD1Du64;
for _ in 0..5_000 {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
time.advance(Duration::from_nanos(seed % 40_000_000));
clock.update(time.elapsed());
let before = clock.alpha();
assert!(
(0.0..1.0).contains(&before),
"alpha {before} out of range before draining"
);
tick_count(&mut clock);
let after = clock.alpha();
assert!(
(0.0..1.0).contains(&after),
"alpha {after} out of range after draining"
);
}
}
#[test]
fn spiral_of_death_cap_engages_and_reports() {
let mut time = ManualTime::new();
let mut clock = FrameClock::with_period(Duration::from_millis(10));
clock.set_max_catch_up_ticks(4);
clock.update(time.elapsed());
time.advance_secs(10.0);
clock.update(time.elapsed());
assert_eq!(tick_count(&mut clock), 4);
assert_eq!(clock.dropped_ticks(), 996);
assert_eq!(clock.tick(), TickId::from_raw(4));
assert!(clock.alpha() < 1.0);
time.advance(Duration::from_millis(25));
clock.update(time.elapsed());
assert_eq!(tick_count(&mut clock), 2);
assert!((clock.alpha() - 0.5).abs() < 1e-6);
assert_eq!(clock.dropped_ticks(), 996);
}
#[test]
fn non_monotonic_timestamps_are_absorbed() {
let mut clock = FrameClock::new(60);
clock.update(Duration::from_secs(10));
clock.update(Duration::from_secs(5));
assert_eq!(clock.backwards_updates(), 1);
assert_eq!(tick_count(&mut clock), 0);
assert_eq!(clock.render_dt(), Duration::ZERO);
clock.update(Duration::from_secs(5) + Duration::from_millis(20));
assert_eq!(tick_count(&mut clock), 1);
}
#[test]
fn tick_rate_and_period_agree() {
let by_rate = FrameClock::new(60);
let by_period = FrameClock::with_period(Duration::from_nanos(16_666_666));
assert_eq!(by_rate.tick_dt(), by_period.tick_dt());
assert!((by_rate.tick_dt_secs() - 1.0 / 60.0).abs() < 1e-6);
let physics = FrameClock::new(240);
assert_eq!(physics.tick_dt(), Duration::from_nanos(4_166_666));
assert!(format!("{physics:?}").contains("FrameClock"));
}
#[test]
fn manual_time_can_be_set_absolutely() {
let mut time = ManualTime::new();
assert_eq!(time.elapsed(), Duration::ZERO);
time.set(Duration::from_secs(90));
time.advance(Duration::from_millis(500));
assert_eq!(time.elapsed(), Duration::from_millis(90_500));
let mut clock = FrameClock::with_period(Duration::from_millis(100));
clock.update(time.elapsed());
time.advance_secs(0.35);
clock.update(time.elapsed());
assert_eq!(clock.pending_ticks(), 3);
assert!((clock.render_dt_secs() - 0.35).abs() < 1e-6);
assert_eq!(tick_count(&mut clock), 3);
assert_eq!(clock.pending_ticks(), 0);
}
#[test]
fn monotonic_time_moves_forward() {
let time = MonotonicTime::new();
let first = time.elapsed();
let second = time.elapsed();
assert!(second >= first);
let mut clock = FrameClock::new(60);
clock.update(time.elapsed());
assert!(clock.pending_ticks() < 2);
}
#[test]
#[should_panic(expected = "tick rate must be positive")]
fn zero_tick_rate_is_rejected() {
let _ = FrameClock::new(0);
}
#[test]
#[should_panic(expected = "tick rate must be at most 1 GHz")]
fn a_tick_rate_finer_than_a_nanosecond_is_rejected() {
let _ = FrameClock::new(1_000_000_001);
}
#[test]
fn one_tick_per_nanosecond_is_the_fastest_clock() {
let clock = FrameClock::new(1_000_000_000);
assert_eq!(clock.tick_dt(), Duration::from_nanos(1));
}
#[test]
fn event_times_share_the_time_sources_epoch() {
let mut time = ManualTime::new();
time.advance_secs(1.5);
let pressed = EventTime::from_duration(time.elapsed());
time.advance(Duration::from_millis(120));
let released = EventTime::from_duration(time.elapsed());
assert_eq!(
released.saturating_since(pressed),
Duration::from_millis(120)
);
assert_eq!(pressed.saturating_since(released), Duration::ZERO);
assert_eq!(EventTime::from_millis(1500), pressed);
assert_eq!(EventTime::from_micros(1_500_000), pressed);
assert_eq!(EventTime::from(Duration::from_millis(1500)), pressed);
assert!((pressed.as_secs_f64() - 1.5).abs() < 1e-9);
assert_eq!(pressed.as_duration(), Duration::from_millis(1500));
assert!(EventTime::ZERO < pressed);
assert_eq!(EventTime::default(), EventTime::ZERO);
assert_eq!(
EventTime::ZERO.saturating_add(Duration::from_secs(2)),
EventTime::from_millis(2000)
);
assert_eq!(pressed.to_string(), "t+1.500000s");
}
}