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 #[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 #[inline]
462 #[must_use]
463 pub const fn tick(&self) -> TickId {
464 self.tick
465 }
466
467 #[inline]
470 #[must_use]
471 pub const fn tick_dt(&self) -> Duration {
472 Duration::from_nanos(self.tick_nanos)
473 }
474
475 #[inline]
477 #[must_use]
478 pub fn tick_dt_secs(&self) -> f64 {
479 self.tick_nanos as f64 * 1e-9
480 }
481
482 #[inline]
486 #[must_use]
487 pub const fn render_dt(&self) -> Duration {
488 self.render_dt
489 }
490
491 #[inline]
493 #[must_use]
494 pub fn render_dt_secs(&self) -> f32 {
495 self.render_dt.as_secs_f32()
496 }
497
498 #[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 #[inline]
514 #[must_use]
515 pub const fn dropped_ticks(&self) -> u64 {
516 self.dropped_ticks
517 }
518
519 #[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 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 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 #[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; 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 #[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 #[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 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 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 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 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 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 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 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 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 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 #[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 #[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 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 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}