1use core::fmt;
34use core::time::Duration;
35use std::time::Instant;
36
37#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
43pub struct TickId(u64);
44
45impl TickId {
46 pub const ZERO: Self = Self(0);
48
49 #[inline]
51 #[must_use]
52 pub const fn from_raw(raw: u64) -> Self {
53 Self(raw)
54 }
55
56 #[inline]
58 #[must_use]
59 pub const fn get(self) -> u64 {
60 self.0
61 }
62
63 #[inline]
68 #[must_use]
69 pub const fn next(self) -> Self {
70 Self(self.0.saturating_add(1))
71 }
72
73 #[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
87pub trait TimeSource: fmt::Debug {
94 fn elapsed(&self) -> Duration;
96}
97
98#[derive(Clone, Copy, Debug)]
100pub struct MonotonicTime {
101 start: Instant,
102}
103
104impl MonotonicTime {
105 #[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
132pub struct ManualTime {
133 now: Duration,
134}
135
136impl ManualTime {
137 #[inline]
139 #[must_use]
140 pub const fn new() -> Self {
141 Self {
142 now: Duration::ZERO,
143 }
144 }
145
146 #[inline]
148 pub const fn advance(&mut self, delta: Duration) {
149 self.now = self.now.saturating_add(delta);
150 }
151
152 #[inline]
158 pub fn advance_secs(&mut self, seconds: f64) {
159 self.advance(Duration::from_secs_f64(seconds));
160 }
161
162 #[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#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
218pub struct EventTime(Duration);
219
220impl EventTime {
221 pub const ZERO: Self = Self(Duration::ZERO);
223
224 #[inline]
227 #[must_use]
228 pub const fn from_duration(since_epoch: Duration) -> Self {
229 Self(since_epoch)
230 }
231
232 #[inline]
235 #[must_use]
236 pub const fn from_micros(micros: u64) -> Self {
237 Self(Duration::from_micros(micros))
238 }
239
240 #[inline]
242 #[must_use]
243 pub const fn from_millis(millis: u64) -> Self {
244 Self(Duration::from_millis(millis))
245 }
246
247 #[inline]
249 #[must_use]
250 pub const fn as_duration(self) -> Duration {
251 self.0
252 }
253
254 #[inline]
256 #[must_use]
257 pub fn as_secs_f64(self) -> f64 {
258 self.0.as_secs_f64()
259 }
260
261 #[inline]
268 #[must_use]
269 pub fn saturating_since(self, earlier: Self) -> Duration {
270 self.0.saturating_sub(earlier.0)
271 }
272
273 #[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
294pub const DEFAULT_MAX_CATCH_UP_TICKS: u32 = 8;
297
298const ALPHA_MAX: f32 = 1.0 - f32::EPSILON / 2.0;
302
303pub 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 #[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 Self::with_period(Duration::from_nanos(u64::from(1_000_000_000u32 / tick_hz)))
340 }
341
342 #[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 #[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 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 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 #[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 #[inline]
438 #[must_use]
439 pub const fn pending_ticks(&self) -> u32 {
440 (self.accumulator_nanos / self.tick_nanos) as u32
441 }
442
443 #[inline]
445 #[must_use]
446 pub const fn tick(&self) -> TickId {
447 self.tick
448 }
449
450 #[inline]
453 #[must_use]
454 pub const fn tick_dt(&self) -> Duration {
455 Duration::from_nanos(self.tick_nanos)
456 }
457
458 #[inline]
460 #[must_use]
461 pub fn tick_dt_secs(&self) -> f64 {
462 self.tick_nanos as f64 * 1e-9
463 }
464
465 #[inline]
469 #[must_use]
470 pub const fn render_dt(&self) -> Duration {
471 self.render_dt
472 }
473
474 #[inline]
476 #[must_use]
477 pub fn render_dt_secs(&self) -> f32 {
478 self.render_dt.as_secs_f32()
479 }
480
481 #[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 #[inline]
497 #[must_use]
498 pub const fn dropped_ticks(&self) -> u64 {
499 self.dropped_ticks
500 }
501
502 #[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 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 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 #[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; 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 #[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 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 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 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 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 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 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 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 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 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 #[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 #[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 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 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}