Skip to main content

crcbl_core/
time.rs

1//! The frame clock: a fixed-timestep accumulator for the server tick plus the
2//! variable render delta.
3//!
4//! The loop shape this exists to enforce:
5//!
6//! ```
7//! use crcbl_core::time::{FrameClock, MonotonicTime, TimeSource};
8//!
9//! let time = MonotonicTime::new();
10//! let mut clock = FrameClock::new(60);
11//!
12//! # for _ in 0..1 {
13//! clock.update(time.elapsed());        // once per frame
14//! while clock.consume_tick() {
15//!     // server_tick(clock.tick(), clock.tick_dt());  — always the same dt
16//! }
17//! // render(clock.alpha());            — interpolate between the last two ticks
18//! # }
19//! ```
20//!
21//! Simulation advances in exact, identical steps regardless of frame rate;
22//! rendering runs as fast as it can and interpolates. The split is here in
23//! stage 1 because it is nearly impossible to retrofit into a loop that grew
24//! around `dt = last_frame_time`.
25//!
26//! # Determinism
27//!
28//! [`FrameClock::update`] takes the current time as an argument. It never reads
29//! a clock itself, because a hidden `Instant::now()` is a nondeterminism source
30//! and `docs/notes/process.md` requires every one of them to be injected.
31//! [`MonotonicTime`] is the real driver; [`ManualTime`] is the test one.
32
33use core::fmt;
34use core::time::Duration;
35use std::time::Instant;
36
37/// Identifier of a server tick.
38///
39/// A distinct type rather than a bare `u64` because the network protocol,
40/// snapshots and (later) rollback all key off it, and confusing a tick id with
41/// a frame counter or an index is exactly the bug that costs a day.
42#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
43pub struct TickId(u64);
44
45impl TickId {
46    /// The tick before any simulation has run.
47    pub const ZERO: Self = Self(0);
48
49    /// Wraps a raw tick number.
50    #[inline]
51    #[must_use]
52    pub const fn from_raw(raw: u64) -> Self {
53        Self(raw)
54    }
55
56    /// The raw tick number.
57    #[inline]
58    #[must_use]
59    pub const fn get(self) -> u64 {
60        self.0
61    }
62
63    /// The next tick.
64    ///
65    /// Saturates rather than wrapping; at 60 Hz, `u64::MAX` ticks is ~9.7
66    /// billion years, so this is a formality.
67    #[inline]
68    #[must_use]
69    pub const fn next(self) -> Self {
70        Self(self.0.saturating_add(1))
71    }
72
73    /// Ticks elapsed from `earlier` to `self`, saturating at zero.
74    #[inline]
75    #[must_use]
76    pub const fn since(self, earlier: Self) -> u64 {
77        self.0.saturating_sub(earlier.0)
78    }
79}
80
81impl fmt::Display for TickId {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(f, "tick {}", self.0)
84    }
85}
86
87/// A source of monotonically increasing timestamps, measured from an arbitrary
88/// start.
89///
90/// Implemented by [`MonotonicTime`] (real) and [`ManualTime`] (tests). The
91/// clock itself takes timestamps as arguments, so this trait exists to make
92/// "where does the time come from" an explicit choice at the call site.
93pub trait TimeSource: fmt::Debug {
94    /// Time elapsed since this source was created.
95    fn elapsed(&self) -> Duration;
96}
97
98/// The real time source: [`Instant`] deltas from process start.
99#[derive(Clone, Copy, Debug)]
100pub struct MonotonicTime {
101    start: Instant,
102}
103
104impl MonotonicTime {
105    /// Starts the clock now.
106    #[must_use]
107    pub fn new() -> Self {
108        Self {
109            start: Instant::now(),
110        }
111    }
112}
113
114impl Default for MonotonicTime {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120impl TimeSource for MonotonicTime {
121    #[inline]
122    fn elapsed(&self) -> Duration {
123        self.start.elapsed()
124    }
125}
126
127/// A hand-driven time source for tests, replays and headless runs.
128///
129/// Nothing in the engine can tell it apart from [`MonotonicTime`], which is the
130/// point: a deterministic test drives the same code the game does.
131#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
132pub struct ManualTime {
133    now: Duration,
134}
135
136impl ManualTime {
137    /// A source sitting at zero.
138    #[inline]
139    #[must_use]
140    pub const fn new() -> Self {
141        Self {
142            now: Duration::ZERO,
143        }
144    }
145
146    /// Moves time forward.
147    #[inline]
148    pub const fn advance(&mut self, delta: Duration) {
149        self.now = self.now.saturating_add(delta);
150    }
151
152    /// Moves time forward by `seconds`.
153    ///
154    /// # Panics
155    ///
156    /// If `seconds` is negative or not finite.
157    #[inline]
158    pub fn advance_secs(&mut self, seconds: f64) {
159        self.advance(Duration::from_secs_f64(seconds));
160    }
161
162    /// Jumps to an absolute timestamp.
163    #[inline]
164    pub const fn set(&mut self, now: Duration) {
165        self.now = now;
166    }
167}
168
169impl TimeSource for ManualTime {
170    #[inline]
171    fn elapsed(&self) -> Duration {
172        self.now
173    }
174}
175
176/// When an input event happened, on the engine's monotonic clock.
177///
178/// # The epoch is the contract
179///
180/// A timestamp whose origin is unspecified is worth nothing: you cannot
181/// subtract two of them across subsystems, and you cannot compare one to
182/// "now". So this type states its epoch and makes matching it a backend
183/// obligation:
184///
185/// > An `EventTime` is a duration measured from the same origin as the
186/// > [`TimeSource`] driving [`FrameClock::update`] — that is, from
187/// > [`MonotonicTime::new`] (process start) in a real run, and from
188/// > [`ManualTime`]'s zero in a deterministic one.
189///
190/// That makes `event.time` and `clock.render_dt()` live on one timeline, which
191/// is what the P2 input pipeline needs to age an input against the tick it
192/// belongs to, and what topic 26's client prediction needs to compute
193/// input-to-photon latency.
194///
195/// # What backends have to do about it
196///
197/// No window system hands us this directly, so rebasing is part of writing a
198/// backend, not an optimization:
199///
200/// * **Wayland** stamps events with `CLOCK_MONOTONIC` milliseconds
201///   (microseconds under `wp_presentation` and the high-resolution input
202///   protocols). Subtract the clock reading taken when the shell was created.
203/// * **X11** stamps events with the X *server's* time: a 32-bit millisecond
204///   counter with an unspecified origin that wraps every 49.7 days. The backend
205///   estimates the offset once at connection time and must detect the wrap; a
206///   backend that forwards the raw value produces an input pipeline that
207///   silently breaks after a month and a half of uptime.
208/// * **Win32** has `GetMessageTime` (a `GetTickCount` sample, also wrapping) and
209///   raw input's `QPC` timestamps.
210/// * **The browser** gives `event.timeStamp`, already a
211///   `performance.now()`-relative double.
212///
213/// When a backend genuinely has no timestamp for an event — a synthesized
214/// focus-loss key release, for instance — it stamps the event with the *current*
215/// time rather than [`EventTime::ZERO`], because a zero timestamp reads as "this
216/// happened at process start" to every consumer downstream.
217#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
218pub struct EventTime(Duration);
219
220impl EventTime {
221    /// The clock's origin.
222    pub const ZERO: Self = Self(Duration::ZERO);
223
224    /// Wraps a duration measured from the epoch described in the [type
225    /// docs](Self).
226    #[inline]
227    #[must_use]
228    pub const fn from_duration(since_epoch: Duration) -> Self {
229        Self(since_epoch)
230    }
231
232    /// Microseconds since the epoch — the unit Wayland's high-resolution input
233    /// protocols and libinput both use.
234    #[inline]
235    #[must_use]
236    pub const fn from_micros(micros: u64) -> Self {
237        Self(Duration::from_micros(micros))
238    }
239
240    /// Milliseconds since the epoch — the unit core Wayland and X11 use.
241    #[inline]
242    #[must_use]
243    pub const fn from_millis(millis: u64) -> Self {
244        Self(Duration::from_millis(millis))
245    }
246
247    /// Time since the epoch.
248    #[inline]
249    #[must_use]
250    pub const fn as_duration(self) -> Duration {
251        self.0
252    }
253
254    /// Time since the epoch in seconds.
255    #[inline]
256    #[must_use]
257    pub fn as_secs_f64(self) -> f64 {
258        self.0.as_secs_f64()
259    }
260
261    /// How long after `earlier` this timestamp is, saturating at zero.
262    ///
263    /// Saturating rather than panicking because event queues are not perfectly
264    /// ordered in practice — a compositor can deliver a relative-motion event
265    /// stamped fractionally before the absolute-motion event it accompanies —
266    /// and an input pipeline must not die of it.
267    #[inline]
268    #[must_use]
269    pub fn saturating_since(self, earlier: Self) -> Duration {
270        self.0.saturating_sub(earlier.0)
271    }
272
273    /// This timestamp moved forward by `delta`.
274    #[inline]
275    #[must_use]
276    pub const fn saturating_add(self, delta: Duration) -> Self {
277        Self(self.0.saturating_add(delta))
278    }
279}
280
281impl From<Duration> for EventTime {
282    #[inline]
283    fn from(since_epoch: Duration) -> Self {
284        Self(since_epoch)
285    }
286}
287
288impl fmt::Display for EventTime {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        write!(f, "t+{:.6}s", self.0.as_secs_f64())
291    }
292}
293
294/// Default ticks the simulation may catch up in a single frame — see
295/// [`FrameClock::set_max_catch_up_ticks`].
296pub const DEFAULT_MAX_CATCH_UP_TICKS: u32 = 8;
297
298/// Largest `f32` strictly below 1.0. [`FrameClock::alpha`] never returns 1.0
299/// itself: alpha is a *fraction of the way to the next tick*, and 1.0 would
300/// mean "at a tick that has not been simulated yet".
301const ALPHA_MAX: f32 = 1.0 - f32::EPSILON / 2.0;
302
303/// Fixed-timestep accumulator plus variable render delta.
304///
305/// See the [module docs](self) for the loop shape.
306pub struct FrameClock {
307    tick_nanos: u64,
308    accumulator_nanos: u64,
309    max_catch_up_ticks: u32,
310    last_update: Option<Duration>,
311    render_dt: Duration,
312    tick: TickId,
313    dropped_ticks: u64,
314    backwards_updates: u64,
315}
316
317impl FrameClock {
318    /// A clock running the simulation at `tick_hz`.
319    ///
320    /// # Panics
321    ///
322    /// If `tick_hz` is zero, or over one tick per nanosecond — the period below
323    /// is truncating integer nanoseconds, so a rate past 1 GHz rounds to a
324    /// period of zero and there is no clock to build. It used to reach
325    /// [`with_period`](Self::with_period) and fail there, blaming a period the
326    /// caller never passed.
327    #[must_use]
328    pub fn new(tick_hz: u32) -> Self {
329        assert!(tick_hz > 0, "tick rate must be positive");
330        assert!(
331            tick_hz <= 1_000_000_000,
332            "tick rate must be at most 1 GHz, not {tick_hz} Hz"
333        );
334        // Truncating division leaves the step slightly short of 1/hz (16.666666
335        // ms instead of 16.666667 ms at 60 Hz). The *simulation* is unaffected
336        // — its second is 60 steps by definition — and wall-clock drift is
337        // ~40 ns/s, which the accumulator absorbs. Use `with_period` if an
338        // exact period matters.
339        Self::with_period(Duration::from_nanos(u64::from(1_000_000_000u32 / tick_hz)))
340    }
341
342    /// A clock with an explicit tick period.
343    ///
344    /// # Panics
345    ///
346    /// If `period` is zero.
347    #[must_use]
348    pub fn with_period(period: Duration) -> Self {
349        let tick_nanos = u64::try_from(period.as_nanos()).expect("tick period must fit in u64 ns");
350        assert!(tick_nanos > 0, "tick period must be positive");
351        Self {
352            tick_nanos,
353            accumulator_nanos: 0,
354            max_catch_up_ticks: DEFAULT_MAX_CATCH_UP_TICKS,
355            last_update: None,
356            render_dt: Duration::ZERO,
357            tick: TickId::ZERO,
358            dropped_ticks: 0,
359            backwards_updates: 0,
360        }
361    }
362
363    /// Caps how many ticks a single [`FrameClock::update`] may queue.
364    ///
365    /// This is the spiral-of-death guard. If a frame takes longer than the
366    /// ticks it queues take to simulate, the accumulator grows, which queues
367    /// more ticks, which take longer still — the simulation never catches up
368    /// and the game locks solid. Past the cap, surplus wall-clock time is
369    /// **discarded**: those ticks never run, so simulation time falls
370    /// permanently behind wall time (a hitch is a hitch — nothing rewinds it),
371    /// but the frame rate recovers instead of collapsing. The count of skipped
372    /// ticks is available from [`FrameClock::dropped_ticks`] and is worth
373    /// logging; in a multiplayer build it is the client's cue that it has
374    /// desynced from the server's tick timeline.
375    ///
376    /// The default, [`DEFAULT_MAX_CATCH_UP_TICKS`], allows 8 ticks — 133 ms of
377    /// catch-up at 60 Hz, enough to absorb an asset-load or shader-compile
378    /// hitch without letting a slow machine dig itself into a hole.
379    ///
380    /// # Panics
381    ///
382    /// If `ticks` is zero — a clock that can never tick is never what you want.
383    #[inline]
384    pub const fn set_max_catch_up_ticks(&mut self, ticks: u32) {
385        assert!(ticks > 0, "max catch-up ticks must be positive");
386        self.max_catch_up_ticks = ticks;
387    }
388
389    /// Feeds the clock the current timestamp. Call once per frame, before the
390    /// tick loop.
391    ///
392    /// Timestamps must come from a [`TimeSource`]. A timestamp that moves
393    /// backwards (a source that is not actually monotonic) contributes zero
394    /// time and bumps [`FrameClock::backwards_updates`] rather than panicking
395    /// or wrapping.
396    pub fn update(&mut self, now: Duration) {
397        let previous = self.last_update.unwrap_or(now);
398        self.last_update = Some(now);
399        if now < previous {
400            self.backwards_updates += 1;
401            self.render_dt = Duration::ZERO;
402            return;
403        }
404        let delta = now - previous;
405        self.render_dt = delta;
406
407        let delta_nanos = u64::try_from(delta.as_nanos()).unwrap_or(u64::MAX);
408        self.accumulator_nanos = self.accumulator_nanos.saturating_add(delta_nanos);
409
410        // Drop whole ticks only, and only the surplus above the cap: the
411        // sub-tick remainder survives, so `alpha` stays continuous across the
412        // hitch instead of snapping to zero.
413        let pending = self.accumulator_nanos / self.tick_nanos;
414        let cap = u64::from(self.max_catch_up_ticks);
415        if pending > cap {
416            let dropped = pending - cap;
417            self.dropped_ticks += dropped;
418            self.accumulator_nanos -= dropped * self.tick_nanos;
419        }
420    }
421
422    /// Takes one pending tick, if the accumulator holds a whole one.
423    ///
424    /// Drive it as `while clock.consume_tick() { server_tick(); }`.
425    #[inline]
426    pub const fn consume_tick(&mut self) -> bool {
427        if self.accumulator_nanos >= self.tick_nanos {
428            self.accumulator_nanos -= self.tick_nanos;
429            self.tick = self.tick.next();
430            true
431        } else {
432            false
433        }
434    }
435
436    /// How many ticks [`FrameClock::consume_tick`] would still yield.
437    #[inline]
438    #[must_use]
439    pub const fn pending_ticks(&self) -> u32 {
440        (self.accumulator_nanos / self.tick_nanos) as u32
441    }
442
443    /// When the next tick comes due, on the timeline
444    /// [`FrameClock::update`] is fed: the last update, plus whatever the
445    /// accumulator still lacks of a whole tick. A tick already pending is due
446    /// at the last update. `None` before the first update, which has no
447    /// timeline to answer on.
448    ///
449    /// What a loop that has nothing to render may sleep until: no tick is due
450    /// before this, so a wait that ends here keeps the simulation on schedule.
451    #[must_use]
452    pub const fn next_tick_due(&self) -> Option<Duration> {
453        let Some(last) = self.last_update else {
454            return None;
455        };
456        let short = self.tick_nanos.saturating_sub(self.accumulator_nanos);
457        Some(last.saturating_add(Duration::from_nanos(short)))
458    }
459
460    /// The id of the most recently simulated tick.
461    #[inline]
462    #[must_use]
463    pub const fn tick(&self) -> TickId {
464        self.tick
465    }
466
467    /// The fixed simulation step. Every tick advances the world by exactly
468    /// this much, whatever the frame rate.
469    #[inline]
470    #[must_use]
471    pub const fn tick_dt(&self) -> Duration {
472        Duration::from_nanos(self.tick_nanos)
473    }
474
475    /// [`FrameClock::tick_dt`] in seconds, for integrators.
476    #[inline]
477    #[must_use]
478    pub fn tick_dt_secs(&self) -> f64 {
479        self.tick_nanos as f64 * 1e-9
480    }
481
482    /// Wall-clock time covered by the last [`FrameClock::update`] — the
483    /// variable render delta, for camera smoothing, UI animation and anything
484    /// else that is not simulation.
485    #[inline]
486    #[must_use]
487    pub const fn render_dt(&self) -> Duration {
488        self.render_dt
489    }
490
491    /// [`FrameClock::render_dt`] in seconds.
492    #[inline]
493    #[must_use]
494    pub fn render_dt_secs(&self) -> f32 {
495        self.render_dt.as_secs_f32()
496    }
497
498    /// How far the render frame sits between the last simulated tick and the
499    /// next one, in `[0, 1)`.
500    ///
501    /// Interpolated rendering wants `lerp(previous_state, current_state,
502    /// alpha)`. Call it *after* draining the tick loop; called before, the
503    /// accumulator may still hold whole ticks, in which case this saturates
504    /// just below 1.0 rather than reporting a fraction greater than one.
505    #[inline]
506    #[must_use]
507    pub fn alpha(&self) -> f32 {
508        let alpha = self.accumulator_nanos as f32 / self.tick_nanos as f32;
509        if alpha >= 1.0 { ALPHA_MAX } else { alpha }
510    }
511
512    /// Ticks discarded by the catch-up cap since the clock was created.
513    #[inline]
514    #[must_use]
515    pub const fn dropped_ticks(&self) -> u64 {
516        self.dropped_ticks
517    }
518
519    /// Number of [`FrameClock::update`] calls whose timestamp went backwards.
520    /// Non-zero means the time source is broken.
521    #[inline]
522    #[must_use]
523    pub const fn backwards_updates(&self) -> u64 {
524        self.backwards_updates
525    }
526}
527
528impl fmt::Debug for FrameClock {
529    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
530        f.debug_struct("FrameClock")
531            .field("tick", &self.tick)
532            .field("tick_dt", &self.tick_dt())
533            .field("pending_ticks", &self.pending_ticks())
534            .field("render_dt", &self.render_dt)
535            .field("dropped_ticks", &self.dropped_ticks)
536            .finish()
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    fn tick_count(clock: &mut FrameClock) -> u32 {
545        let mut ticks = 0;
546        while clock.consume_tick() {
547            ticks += 1;
548        }
549        ticks
550    }
551
552    #[test]
553    fn tick_ids_are_ordered_and_saturating() {
554        assert_eq!(TickId::ZERO.get(), 0);
555        assert_eq!(TickId::ZERO.next(), TickId::from_raw(1));
556        assert!(TickId::from_raw(2) > TickId::from_raw(1));
557        assert_eq!(TickId::from_raw(9).since(TickId::from_raw(4)), 5);
558        assert_eq!(TickId::from_raw(4).since(TickId::from_raw(9)), 0);
559        assert_eq!(TickId::from_raw(u64::MAX).next().get(), u64::MAX);
560        assert_eq!(TickId::from_raw(7).to_string(), "tick 7");
561    }
562
563    #[test]
564    fn manual_time_drives_the_clock() {
565        let mut time = ManualTime::new();
566        let mut clock = FrameClock::new(60);
567        assert_eq!(clock.tick(), TickId::ZERO);
568
569        // First update establishes the baseline; no time has passed yet.
570        clock.update(time.elapsed());
571        assert_eq!(tick_count(&mut clock), 0);
572        assert_eq!(clock.render_dt(), Duration::ZERO);
573
574        time.advance_secs(1.0);
575        clock.update(time.elapsed());
576        assert_eq!(clock.render_dt(), Duration::from_secs(1));
577        // One second of catch-up is far past the cap: 60 ticks' worth of time
578        // arrived, 8 run, 52 are dropped.
579        assert_eq!(tick_count(&mut clock), DEFAULT_MAX_CATCH_UP_TICKS);
580        assert_eq!(clock.dropped_ticks(), 52);
581    }
582
583    #[test]
584    fn steady_frame_rate_produces_exactly_one_tick_per_frame() {
585        let mut time = ManualTime::new();
586        let mut clock = FrameClock::new(60);
587        clock.update(time.elapsed());
588
589        let mut ticks = 0;
590        for _ in 0..600 {
591            time.advance(clock.tick_dt());
592            clock.update(time.elapsed());
593            ticks += tick_count(&mut clock);
594        }
595        assert_eq!(ticks, 600);
596        assert_eq!(clock.tick(), TickId::from_raw(600));
597        assert_eq!(clock.dropped_ticks(), 0);
598    }
599
600    /// A render frame rate that is not a multiple of the tick rate must still
601    /// average out to the right number of ticks.
602    #[test]
603    fn tick_count_matches_elapsed_time_at_an_awkward_frame_rate() {
604        let mut time = ManualTime::new();
605        let mut clock = FrameClock::new(60);
606        clock.update(time.elapsed());
607
608        let frame = Duration::from_nanos(1_000_000_000 / 144);
609        let frames = 1440u32; // exactly 10 seconds at 144 Hz
610        let mut ticks = 0;
611        for _ in 0..frames {
612            time.advance(frame);
613            clock.update(time.elapsed());
614            ticks += tick_count(&mut clock);
615        }
616        let expected = (frame * frames).as_nanos() / clock.tick_dt().as_nanos();
617        assert_eq!(u128::from(ticks), expected);
618        assert_eq!(clock.dropped_ticks(), 0);
619    }
620
621    /// Half the tick rate: two frames per tick, never more.
622    #[test]
623    fn slow_frames_never_queue_more_than_the_elapsed_time() {
624        let mut time = ManualTime::new();
625        let mut clock = FrameClock::new(60);
626        clock.update(time.elapsed());
627        for _ in 0..10 {
628            time.advance(clock.tick_dt() * 2);
629            clock.update(time.elapsed());
630            assert_eq!(tick_count(&mut clock), 2);
631        }
632    }
633
634    /// The next tick is due a whole tick after an update that left nothing
635    /// over, the remainder after one that left part of a tick, and at the
636    /// update itself while a whole tick is still pending.
637    #[test]
638    fn the_next_tick_is_due_when_the_accumulator_fills() {
639        let mut time = ManualTime::new();
640        let mut clock = FrameClock::new(60);
641        assert_eq!(clock.next_tick_due(), None, "no update, no timeline");
642        clock.update(time.elapsed());
643        assert_eq!(clock.next_tick_due(), Some(clock.tick_dt()));
644
645        time.advance(clock.tick_dt() / 4);
646        clock.update(time.elapsed());
647        assert_eq!(
648            clock.next_tick_due(),
649            Some(clock.tick_dt()),
650            "a quarter tick in, the deadline has not moved"
651        );
652
653        time.advance(clock.tick_dt());
654        clock.update(time.elapsed());
655        assert_eq!(
656            clock.next_tick_due(),
657            Some(time.elapsed()),
658            "a whole tick is pending, so it is due now"
659        );
660        assert_eq!(tick_count(&mut clock), 1);
661        assert_eq!(
662            clock.next_tick_due(),
663            Some(clock.tick_dt() * 2),
664            "and once it ran, the one after is due on the same grid"
665        );
666    }
667
668    #[test]
669    fn alpha_stays_in_range_and_tracks_the_partial_tick() {
670        let mut time = ManualTime::new();
671        let mut clock = FrameClock::new(60);
672        clock.update(time.elapsed());
673        assert_eq!(clock.alpha(), 0.0);
674
675        // Quarter of a tick: no tick runs, alpha reports the fraction.
676        time.advance(clock.tick_dt() / 4);
677        clock.update(time.elapsed());
678        assert_eq!(tick_count(&mut clock), 0);
679        assert!((clock.alpha() - 0.25).abs() < 1e-6, "{}", clock.alpha());
680
681        // Alpha before draining the tick loop saturates below 1.0.
682        time.advance(clock.tick_dt() * 3);
683        clock.update(time.elapsed());
684        assert!(clock.alpha() < 1.0);
685        assert!(clock.alpha() >= 0.0);
686        assert_eq!(clock.alpha(), ALPHA_MAX);
687        tick_count(&mut clock);
688        assert!((clock.alpha() - 0.25).abs() < 1e-6);
689
690        // Fuzz the invariant across a jittery frame sequence.
691        let mut seed = 0x2545_F491_4F6C_DD1Du64;
692        for _ in 0..5_000 {
693            seed ^= seed << 13;
694            seed ^= seed >> 7;
695            seed ^= seed << 17;
696            time.advance(Duration::from_nanos(seed % 40_000_000));
697            clock.update(time.elapsed());
698            let before = clock.alpha();
699            assert!(
700                (0.0..1.0).contains(&before),
701                "alpha {before} out of range before draining"
702            );
703            tick_count(&mut clock);
704            let after = clock.alpha();
705            assert!(
706                (0.0..1.0).contains(&after),
707                "alpha {after} out of range after draining"
708            );
709        }
710    }
711
712    #[test]
713    fn spiral_of_death_cap_engages_and_reports() {
714        let mut time = ManualTime::new();
715        // An exact 100 Hz period keeps the arithmetic in the test obvious.
716        let mut clock = FrameClock::with_period(Duration::from_millis(10));
717        clock.set_max_catch_up_ticks(4);
718        clock.update(time.elapsed());
719
720        // A ten-second stall (1000 ticks' worth) yields 4 ticks, not 1000.
721        time.advance_secs(10.0);
722        clock.update(time.elapsed());
723        assert_eq!(tick_count(&mut clock), 4);
724        assert_eq!(clock.dropped_ticks(), 996);
725        assert_eq!(clock.tick(), TickId::from_raw(4));
726        assert!(clock.alpha() < 1.0);
727
728        // Dropping keeps the sub-tick remainder rather than snapping it away.
729        time.advance(Duration::from_millis(25));
730        clock.update(time.elapsed());
731        assert_eq!(tick_count(&mut clock), 2);
732        assert!((clock.alpha() - 0.5).abs() < 1e-6);
733
734        // And the clock keeps working normally afterwards.
735        assert_eq!(clock.dropped_ticks(), 996);
736    }
737
738    #[test]
739    fn non_monotonic_timestamps_are_absorbed() {
740        let mut clock = FrameClock::new(60);
741        clock.update(Duration::from_secs(10));
742        clock.update(Duration::from_secs(5));
743        assert_eq!(clock.backwards_updates(), 1);
744        assert_eq!(tick_count(&mut clock), 0);
745        assert_eq!(clock.render_dt(), Duration::ZERO);
746        // Recovers from the (bogus) new baseline.
747        clock.update(Duration::from_secs(5) + Duration::from_millis(20));
748        assert_eq!(tick_count(&mut clock), 1);
749    }
750
751    #[test]
752    fn tick_rate_and_period_agree() {
753        let by_rate = FrameClock::new(60);
754        let by_period = FrameClock::with_period(Duration::from_nanos(16_666_666));
755        assert_eq!(by_rate.tick_dt(), by_period.tick_dt());
756        assert!((by_rate.tick_dt_secs() - 1.0 / 60.0).abs() < 1e-6);
757
758        let physics = FrameClock::new(240);
759        assert_eq!(physics.tick_dt(), Duration::from_nanos(4_166_666));
760        assert!(format!("{physics:?}").contains("FrameClock"));
761    }
762
763    #[test]
764    fn manual_time_can_be_set_absolutely() {
765        let mut time = ManualTime::new();
766        assert_eq!(time.elapsed(), Duration::ZERO);
767        time.set(Duration::from_secs(90));
768        time.advance(Duration::from_millis(500));
769        assert_eq!(time.elapsed(), Duration::from_millis(90_500));
770
771        let mut clock = FrameClock::with_period(Duration::from_millis(100));
772        clock.update(time.elapsed());
773        time.advance_secs(0.35);
774        clock.update(time.elapsed());
775        assert_eq!(clock.pending_ticks(), 3);
776        assert!((clock.render_dt_secs() - 0.35).abs() < 1e-6);
777        assert_eq!(tick_count(&mut clock), 3);
778        assert_eq!(clock.pending_ticks(), 0);
779    }
780
781    #[test]
782    fn monotonic_time_moves_forward() {
783        let time = MonotonicTime::new();
784        let first = time.elapsed();
785        let second = time.elapsed();
786        assert!(second >= first);
787        // The clock accepts a real source with no ceremony.
788        let mut clock = FrameClock::new(60);
789        clock.update(time.elapsed());
790        assert!(clock.pending_ticks() < 2);
791    }
792
793    #[test]
794    #[should_panic(expected = "tick rate must be positive")]
795    fn zero_tick_rate_is_rejected() {
796        let _ = FrameClock::new(0);
797    }
798
799    /// The other end of the rate range, which used to fail as a period.
800    ///
801    /// One tick per nanosecond is the last rate with a period to round to; the
802    /// next one up truncates to zero nanoseconds, and the clock that came back
803    /// blamed `with_period` for a `Duration` the caller had not written.
804    #[test]
805    #[should_panic(expected = "tick rate must be at most 1 GHz")]
806    fn a_tick_rate_finer_than_a_nanosecond_is_rejected() {
807        let _ = FrameClock::new(1_000_000_001);
808    }
809
810    /// …and exactly 1 GHz still builds, so the bound above is the right one.
811    #[test]
812    fn one_tick_per_nanosecond_is_the_fastest_clock() {
813        let clock = FrameClock::new(1_000_000_000);
814        assert_eq!(clock.tick_dt(), Duration::from_nanos(1));
815    }
816
817    #[test]
818    fn event_times_share_the_time_sources_epoch() {
819        let mut time = ManualTime::new();
820        time.advance_secs(1.5);
821
822        // The load-bearing property: a timestamp taken from the same source the
823        // frame clock is driven by is directly comparable with it.
824        let pressed = EventTime::from_duration(time.elapsed());
825        time.advance(Duration::from_millis(120));
826        let released = EventTime::from_duration(time.elapsed());
827
828        assert_eq!(
829            released.saturating_since(pressed),
830            Duration::from_millis(120)
831        );
832        // Out-of-order delivery saturates instead of panicking.
833        assert_eq!(pressed.saturating_since(released), Duration::ZERO);
834
835        assert_eq!(EventTime::from_millis(1500), pressed);
836        assert_eq!(EventTime::from_micros(1_500_000), pressed);
837        assert_eq!(EventTime::from(Duration::from_millis(1500)), pressed);
838        assert!((pressed.as_secs_f64() - 1.5).abs() < 1e-9);
839        assert_eq!(pressed.as_duration(), Duration::from_millis(1500));
840        assert!(EventTime::ZERO < pressed);
841        assert_eq!(EventTime::default(), EventTime::ZERO);
842        assert_eq!(
843            EventTime::ZERO.saturating_add(Duration::from_secs(2)),
844            EventTime::from_millis(2000)
845        );
846        assert_eq!(pressed.to_string(), "t+1.500000s");
847    }
848}