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    /// The id of the most recently simulated tick.
444    #[inline]
445    #[must_use]
446    pub const fn tick(&self) -> TickId {
447        self.tick
448    }
449
450    /// The fixed simulation step. Every tick advances the world by exactly
451    /// this much, whatever the frame rate.
452    #[inline]
453    #[must_use]
454    pub const fn tick_dt(&self) -> Duration {
455        Duration::from_nanos(self.tick_nanos)
456    }
457
458    /// [`FrameClock::tick_dt`] in seconds, for integrators.
459    #[inline]
460    #[must_use]
461    pub fn tick_dt_secs(&self) -> f64 {
462        self.tick_nanos as f64 * 1e-9
463    }
464
465    /// Wall-clock time covered by the last [`FrameClock::update`] — the
466    /// variable render delta, for camera smoothing, UI animation and anything
467    /// else that is not simulation.
468    #[inline]
469    #[must_use]
470    pub const fn render_dt(&self) -> Duration {
471        self.render_dt
472    }
473
474    /// [`FrameClock::render_dt`] in seconds.
475    #[inline]
476    #[must_use]
477    pub fn render_dt_secs(&self) -> f32 {
478        self.render_dt.as_secs_f32()
479    }
480
481    /// How far the render frame sits between the last simulated tick and the
482    /// next one, in `[0, 1)`.
483    ///
484    /// Interpolated rendering wants `lerp(previous_state, current_state,
485    /// alpha)`. Call it *after* draining the tick loop; called before, the
486    /// accumulator may still hold whole ticks, in which case this saturates
487    /// just below 1.0 rather than reporting a fraction greater than one.
488    #[inline]
489    #[must_use]
490    pub fn alpha(&self) -> f32 {
491        let alpha = self.accumulator_nanos as f32 / self.tick_nanos as f32;
492        if alpha >= 1.0 { ALPHA_MAX } else { alpha }
493    }
494
495    /// Ticks discarded by the catch-up cap since the clock was created.
496    #[inline]
497    #[must_use]
498    pub const fn dropped_ticks(&self) -> u64 {
499        self.dropped_ticks
500    }
501
502    /// Number of [`FrameClock::update`] calls whose timestamp went backwards.
503    /// Non-zero means the time source is broken.
504    #[inline]
505    #[must_use]
506    pub const fn backwards_updates(&self) -> u64 {
507        self.backwards_updates
508    }
509}
510
511impl fmt::Debug for FrameClock {
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        f.debug_struct("FrameClock")
514            .field("tick", &self.tick)
515            .field("tick_dt", &self.tick_dt())
516            .field("pending_ticks", &self.pending_ticks())
517            .field("render_dt", &self.render_dt)
518            .field("dropped_ticks", &self.dropped_ticks)
519            .finish()
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526
527    fn tick_count(clock: &mut FrameClock) -> u32 {
528        let mut ticks = 0;
529        while clock.consume_tick() {
530            ticks += 1;
531        }
532        ticks
533    }
534
535    #[test]
536    fn tick_ids_are_ordered_and_saturating() {
537        assert_eq!(TickId::ZERO.get(), 0);
538        assert_eq!(TickId::ZERO.next(), TickId::from_raw(1));
539        assert!(TickId::from_raw(2) > TickId::from_raw(1));
540        assert_eq!(TickId::from_raw(9).since(TickId::from_raw(4)), 5);
541        assert_eq!(TickId::from_raw(4).since(TickId::from_raw(9)), 0);
542        assert_eq!(TickId::from_raw(u64::MAX).next().get(), u64::MAX);
543        assert_eq!(TickId::from_raw(7).to_string(), "tick 7");
544    }
545
546    #[test]
547    fn manual_time_drives_the_clock() {
548        let mut time = ManualTime::new();
549        let mut clock = FrameClock::new(60);
550        assert_eq!(clock.tick(), TickId::ZERO);
551
552        // First update establishes the baseline; no time has passed yet.
553        clock.update(time.elapsed());
554        assert_eq!(tick_count(&mut clock), 0);
555        assert_eq!(clock.render_dt(), Duration::ZERO);
556
557        time.advance_secs(1.0);
558        clock.update(time.elapsed());
559        assert_eq!(clock.render_dt(), Duration::from_secs(1));
560        // One second of catch-up is far past the cap: 60 ticks' worth of time
561        // arrived, 8 run, 52 are dropped.
562        assert_eq!(tick_count(&mut clock), DEFAULT_MAX_CATCH_UP_TICKS);
563        assert_eq!(clock.dropped_ticks(), 52);
564    }
565
566    #[test]
567    fn steady_frame_rate_produces_exactly_one_tick_per_frame() {
568        let mut time = ManualTime::new();
569        let mut clock = FrameClock::new(60);
570        clock.update(time.elapsed());
571
572        let mut ticks = 0;
573        for _ in 0..600 {
574            time.advance(clock.tick_dt());
575            clock.update(time.elapsed());
576            ticks += tick_count(&mut clock);
577        }
578        assert_eq!(ticks, 600);
579        assert_eq!(clock.tick(), TickId::from_raw(600));
580        assert_eq!(clock.dropped_ticks(), 0);
581    }
582
583    /// A render frame rate that is not a multiple of the tick rate must still
584    /// average out to the right number of ticks.
585    #[test]
586    fn tick_count_matches_elapsed_time_at_an_awkward_frame_rate() {
587        let mut time = ManualTime::new();
588        let mut clock = FrameClock::new(60);
589        clock.update(time.elapsed());
590
591        let frame = Duration::from_nanos(1_000_000_000 / 144);
592        let frames = 1440u32; // exactly 10 seconds at 144 Hz
593        let mut ticks = 0;
594        for _ in 0..frames {
595            time.advance(frame);
596            clock.update(time.elapsed());
597            ticks += tick_count(&mut clock);
598        }
599        let expected = (frame * frames).as_nanos() / clock.tick_dt().as_nanos();
600        assert_eq!(u128::from(ticks), expected);
601        assert_eq!(clock.dropped_ticks(), 0);
602    }
603
604    /// Half the tick rate: two frames per tick, never more.
605    #[test]
606    fn slow_frames_never_queue_more_than_the_elapsed_time() {
607        let mut time = ManualTime::new();
608        let mut clock = FrameClock::new(60);
609        clock.update(time.elapsed());
610        for _ in 0..10 {
611            time.advance(clock.tick_dt() * 2);
612            clock.update(time.elapsed());
613            assert_eq!(tick_count(&mut clock), 2);
614        }
615    }
616
617    #[test]
618    fn alpha_stays_in_range_and_tracks_the_partial_tick() {
619        let mut time = ManualTime::new();
620        let mut clock = FrameClock::new(60);
621        clock.update(time.elapsed());
622        assert_eq!(clock.alpha(), 0.0);
623
624        // Quarter of a tick: no tick runs, alpha reports the fraction.
625        time.advance(clock.tick_dt() / 4);
626        clock.update(time.elapsed());
627        assert_eq!(tick_count(&mut clock), 0);
628        assert!((clock.alpha() - 0.25).abs() < 1e-6, "{}", clock.alpha());
629
630        // Alpha before draining the tick loop saturates below 1.0.
631        time.advance(clock.tick_dt() * 3);
632        clock.update(time.elapsed());
633        assert!(clock.alpha() < 1.0);
634        assert!(clock.alpha() >= 0.0);
635        assert_eq!(clock.alpha(), ALPHA_MAX);
636        tick_count(&mut clock);
637        assert!((clock.alpha() - 0.25).abs() < 1e-6);
638
639        // Fuzz the invariant across a jittery frame sequence.
640        let mut seed = 0x2545_F491_4F6C_DD1Du64;
641        for _ in 0..5_000 {
642            seed ^= seed << 13;
643            seed ^= seed >> 7;
644            seed ^= seed << 17;
645            time.advance(Duration::from_nanos(seed % 40_000_000));
646            clock.update(time.elapsed());
647            let before = clock.alpha();
648            assert!(
649                (0.0..1.0).contains(&before),
650                "alpha {before} out of range before draining"
651            );
652            tick_count(&mut clock);
653            let after = clock.alpha();
654            assert!(
655                (0.0..1.0).contains(&after),
656                "alpha {after} out of range after draining"
657            );
658        }
659    }
660
661    #[test]
662    fn spiral_of_death_cap_engages_and_reports() {
663        let mut time = ManualTime::new();
664        // An exact 100 Hz period keeps the arithmetic in the test obvious.
665        let mut clock = FrameClock::with_period(Duration::from_millis(10));
666        clock.set_max_catch_up_ticks(4);
667        clock.update(time.elapsed());
668
669        // A ten-second stall (1000 ticks' worth) yields 4 ticks, not 1000.
670        time.advance_secs(10.0);
671        clock.update(time.elapsed());
672        assert_eq!(tick_count(&mut clock), 4);
673        assert_eq!(clock.dropped_ticks(), 996);
674        assert_eq!(clock.tick(), TickId::from_raw(4));
675        assert!(clock.alpha() < 1.0);
676
677        // Dropping keeps the sub-tick remainder rather than snapping it away.
678        time.advance(Duration::from_millis(25));
679        clock.update(time.elapsed());
680        assert_eq!(tick_count(&mut clock), 2);
681        assert!((clock.alpha() - 0.5).abs() < 1e-6);
682
683        // And the clock keeps working normally afterwards.
684        assert_eq!(clock.dropped_ticks(), 996);
685    }
686
687    #[test]
688    fn non_monotonic_timestamps_are_absorbed() {
689        let mut clock = FrameClock::new(60);
690        clock.update(Duration::from_secs(10));
691        clock.update(Duration::from_secs(5));
692        assert_eq!(clock.backwards_updates(), 1);
693        assert_eq!(tick_count(&mut clock), 0);
694        assert_eq!(clock.render_dt(), Duration::ZERO);
695        // Recovers from the (bogus) new baseline.
696        clock.update(Duration::from_secs(5) + Duration::from_millis(20));
697        assert_eq!(tick_count(&mut clock), 1);
698    }
699
700    #[test]
701    fn tick_rate_and_period_agree() {
702        let by_rate = FrameClock::new(60);
703        let by_period = FrameClock::with_period(Duration::from_nanos(16_666_666));
704        assert_eq!(by_rate.tick_dt(), by_period.tick_dt());
705        assert!((by_rate.tick_dt_secs() - 1.0 / 60.0).abs() < 1e-6);
706
707        let physics = FrameClock::new(240);
708        assert_eq!(physics.tick_dt(), Duration::from_nanos(4_166_666));
709        assert!(format!("{physics:?}").contains("FrameClock"));
710    }
711
712    #[test]
713    fn manual_time_can_be_set_absolutely() {
714        let mut time = ManualTime::new();
715        assert_eq!(time.elapsed(), Duration::ZERO);
716        time.set(Duration::from_secs(90));
717        time.advance(Duration::from_millis(500));
718        assert_eq!(time.elapsed(), Duration::from_millis(90_500));
719
720        let mut clock = FrameClock::with_period(Duration::from_millis(100));
721        clock.update(time.elapsed());
722        time.advance_secs(0.35);
723        clock.update(time.elapsed());
724        assert_eq!(clock.pending_ticks(), 3);
725        assert!((clock.render_dt_secs() - 0.35).abs() < 1e-6);
726        assert_eq!(tick_count(&mut clock), 3);
727        assert_eq!(clock.pending_ticks(), 0);
728    }
729
730    #[test]
731    fn monotonic_time_moves_forward() {
732        let time = MonotonicTime::new();
733        let first = time.elapsed();
734        let second = time.elapsed();
735        assert!(second >= first);
736        // The clock accepts a real source with no ceremony.
737        let mut clock = FrameClock::new(60);
738        clock.update(time.elapsed());
739        assert!(clock.pending_ticks() < 2);
740    }
741
742    #[test]
743    #[should_panic(expected = "tick rate must be positive")]
744    fn zero_tick_rate_is_rejected() {
745        let _ = FrameClock::new(0);
746    }
747
748    /// The other end of the rate range, which used to fail as a period.
749    ///
750    /// One tick per nanosecond is the last rate with a period to round to; the
751    /// next one up truncates to zero nanoseconds, and the clock that came back
752    /// blamed `with_period` for a `Duration` the caller had not written.
753    #[test]
754    #[should_panic(expected = "tick rate must be at most 1 GHz")]
755    fn a_tick_rate_finer_than_a_nanosecond_is_rejected() {
756        let _ = FrameClock::new(1_000_000_001);
757    }
758
759    /// …and exactly 1 GHz still builds, so the bound above is the right one.
760    #[test]
761    fn one_tick_per_nanosecond_is_the_fastest_clock() {
762        let clock = FrameClock::new(1_000_000_000);
763        assert_eq!(clock.tick_dt(), Duration::from_nanos(1));
764    }
765
766    #[test]
767    fn event_times_share_the_time_sources_epoch() {
768        let mut time = ManualTime::new();
769        time.advance_secs(1.5);
770
771        // The load-bearing property: a timestamp taken from the same source the
772        // frame clock is driven by is directly comparable with it.
773        let pressed = EventTime::from_duration(time.elapsed());
774        time.advance(Duration::from_millis(120));
775        let released = EventTime::from_duration(time.elapsed());
776
777        assert_eq!(
778            released.saturating_since(pressed),
779            Duration::from_millis(120)
780        );
781        // Out-of-order delivery saturates instead of panicking.
782        assert_eq!(pressed.saturating_since(released), Duration::ZERO);
783
784        assert_eq!(EventTime::from_millis(1500), pressed);
785        assert_eq!(EventTime::from_micros(1_500_000), pressed);
786        assert_eq!(EventTime::from(Duration::from_millis(1500)), pressed);
787        assert!((pressed.as_secs_f64() - 1.5).abs() < 1e-9);
788        assert_eq!(pressed.as_duration(), Duration::from_millis(1500));
789        assert!(EventTime::ZERO < pressed);
790        assert_eq!(EventTime::default(), EventTime::ZERO);
791        assert_eq!(
792            EventTime::ZERO.saturating_add(Duration::from_secs(2)),
793            EventTime::from_millis(2000)
794        );
795        assert_eq!(pressed.to_string(), "t+1.500000s");
796    }
797}