1use std::time::{Duration, Instant};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum TickTrigger {
25 Timer,
26 Input,
27 HostFrame,
28 Damage,
29}
30
31impl TickTrigger {
32 fn pulls_forward(self) -> bool {
34 !matches!(self, TickTrigger::Timer)
35 }
36
37 fn refills(self) -> bool {
42 matches!(self, TickTrigger::Input)
43 }
44}
45
46const TIMER_TICK_MIN_FRACTION: f64 = 0.9;
48const INPUT_TICK_MIN_FRACTION: f64 = 0.5;
51const INPUT_BORROW_REFILL: f64 = 1.0 / 32.0;
55const TIMER_GRACE_AFTER_PULL: f64 = 0.25;
60const WINDOW_EARLY_FRACTION: f64 = 0.25;
66const EXTRA_MIN_FRACTION: f64 = 0.5;
69const EXTRA_CAP: f64 = 2.0;
71const EXTRA_REFILL: f64 = 1.0 / 32.0;
76const STILL_PERIODS: f64 = 1.5;
79const PACED_INPUT_MIN_FRACTION: f64 = 0.75;
83const PACED_INPUT_MAX_FRACTION: f64 = 1.25;
84
85#[derive(Debug, Default)]
115pub struct FramePace {
116 pub last_tick: Option<Instant>,
118 last_paced: bool,
121 borrow_budget: Option<(Duration, Instant)>,
124 phase_budget: Option<Duration>,
127 deferred_until: Option<Instant>,
130 last_damage: Option<Instant>,
132 rephase: bool,
134 damage_spacing: Option<Duration>,
135 window_end: Option<Instant>,
137 windowed: bool,
138 extra: Option<(f64, Instant, Duration)>,
141}
142
143impl FramePace {
144 pub fn windowed() -> Self {
163 Self {
164 windowed: true,
165 ..Self::default()
166 }
167 }
168
169 fn window_closed_by(&self, period: Duration, now: Instant) -> Instant {
172 match self.window_end {
173 None => now,
174 Some(end) => {
175 let mut close = end + period;
176 while close < now {
177 close += period;
178 }
179 close
180 }
181 }
182 }
183
184 fn window_tick(&mut self, trigger: TickTrigger, period: Duration, now: Instant, sent: bool) {
196 self.rebase_extra(period, now);
197 let scheduled = self.next_due(period, now);
198 if trigger == TickTrigger::Timer {
199 self.window_end = Some(if now <= scheduled {
200 scheduled
201 } else {
202 scheduled
203 + period * ((now - scheduled).as_nanos() / period.as_nanos().max(1)) as u32
204 });
205 } else if sent && now < self.window_pull(period, now) {
206 let left = (self.extra_frames(now) - 1.0).max(0.0);
207 self.extra = Some((left, now, period));
208 } else if sent {
209 self.window_end = Some(
210 if self.window_end.is_some() && now > scheduled && now < scheduled + period {
211 scheduled
212 } else {
213 self.window_closed_by(period, now)
214 },
215 );
216 }
217 if sent {
218 if trigger == TickTrigger::Damage {
219 self.damage_paced(period, now);
220 }
221 self.last_tick = Some(now);
222 self.deferred_until = None;
223 }
224 }
225
226 pub fn due(&self, trigger: TickTrigger, period: Duration, now: Instant) -> bool {
230 if self.windowed {
231 return if trigger.pulls_forward() {
232 now >= self.pull_at(trigger, period, now)
233 } else {
234 now + period.mul_f64(1.0 - TIMER_TICK_MIN_FRACTION) >= self.next_due(period, now)
235 };
236 }
237 let Some(last) = self.last_tick else {
238 return true;
239 };
240 let elapsed = now.saturating_duration_since(last);
241 if !trigger.pulls_forward() {
242 return elapsed >= self.timer_wait(period);
243 }
244 elapsed >= period
245 || (elapsed >= period.mul_f64(INPUT_TICK_MIN_FRACTION)
246 && self.budget(trigger, period, now) >= period - elapsed)
247 }
248
249 pub fn input_paced(interval: Option<Duration>, period: Duration) -> bool {
251 interval.is_some_and(|i| {
252 i >= period.mul_f64(PACED_INPUT_MIN_FRACTION)
253 && i <= period.mul_f64(PACED_INPUT_MAX_FRACTION)
254 })
255 }
256
257 pub fn ticked(
265 &mut self,
266 trigger: TickTrigger,
267 period: Duration,
268 now: Instant,
269 input_paced: bool,
270 ) {
271 if self.windowed {
272 self.window_tick(trigger, period, now, true);
273 return;
274 }
275 if trigger.pulls_forward()
276 && let Some(last) = self.last_tick
277 {
278 let elapsed = now.saturating_duration_since(last);
279 let budget = self.budget(trigger, period, now);
280 let left = if elapsed < period {
281 budget.saturating_sub(period - elapsed)
282 } else {
283 (budget + (elapsed - period)).min(period.mul_f64(INPUT_TICK_MIN_FRACTION))
284 };
285 if trigger.refills() {
286 self.borrow_budget = Some((left, now));
287 } else if std::mem::take(&mut self.rephase) {
288 self.phase_budget = Some(Duration::ZERO);
289 } else {
290 self.phase_budget = Some(left);
291 }
292 }
293 let mut at = now;
294 if trigger == TickTrigger::Timer && self.last_tick.is_some() {
295 let scheduled = self.next_due(period, now);
296 let off = if now > scheduled {
297 now - scheduled
298 } else {
299 scheduled - now
300 };
301 if off < period / 2 {
302 at = scheduled;
303 }
304 }
305 let damage_paced = trigger == TickTrigger::Damage && self.damage_paced(period, now);
306 self.last_paced = (trigger == TickTrigger::Input && input_paced) || damage_paced;
307 self.last_tick = Some(at);
308 self.deferred_until = None;
309 }
310
311 fn damage_paced(&mut self, period: Duration, now: Instant) -> bool {
314 if let Some(prev) = self.last_damage {
315 let dt = now.saturating_duration_since(prev).min(period * 4);
316 self.damage_spacing = Some(
317 self.damage_spacing
318 .map_or(dt, |avg| avg.mul_f64(0.875) + dt.mul_f64(0.125)),
319 );
320 }
321 self.last_damage = Some(now);
322 Self::input_paced(self.damage_spacing, period)
323 }
324
325 fn timer_wait(&self, period: Duration) -> Duration {
327 if self.last_paced {
328 period.mul_f64(1.0 + TIMER_GRACE_AFTER_PULL)
329 } else {
330 period.mul_f64(TIMER_TICK_MIN_FRACTION)
331 }
332 }
333
334 pub fn still(&self, period: Duration, now: Instant) -> bool {
338 self.last_damage
339 .is_none_or(|at| now.saturating_duration_since(at) >= period.mul_f64(STILL_PERIODS))
340 }
341
342 pub fn stood_still(&mut self) {
346 self.phase_budget = None;
347 self.rephase = true;
348 }
349
350 pub fn record_tick(
360 &mut self,
361 trigger: TickTrigger,
362 period: Duration,
363 now: Instant,
364 sent: bool,
365 ) {
366 if self.windowed {
367 self.window_tick(trigger, period, now, sent);
368 return;
369 }
370 if !sent {
371 self.defer(now + period);
372 return;
373 }
374 self.ticked(trigger, period, now, false);
375 if trigger == TickTrigger::Timer && self.still(period, now) {
376 self.stood_still();
377 }
378 }
379
380 pub fn defer(&mut self, until: Instant) {
384 self.deferred_until = Some(until);
385 }
386
387 pub fn since_last_tick(&self, now: Instant) -> Option<Duration> {
389 self.last_tick
390 .map(|last| now.saturating_duration_since(last))
391 }
392
393 fn budget(&self, trigger: TickTrigger, period: Duration, now: Instant) -> Duration {
395 let cap = period.mul_f64(INPUT_TICK_MIN_FRACTION);
396 if !trigger.refills() {
397 return self.phase_budget.unwrap_or(cap);
398 }
399 match self.borrow_budget {
400 None => cap,
401 Some((left, at)) => (left
402 + now
403 .saturating_duration_since(at)
404 .mul_f64(INPUT_BORROW_REFILL))
405 .min(cap),
406 }
407 }
408
409 pub fn pull_at(&self, trigger: TickTrigger, period: Duration, now: Instant) -> Instant {
413 if self.windowed {
414 let window = self.window_pull(period, now);
415 return match self.last_tick {
416 Some(last) if self.extra_frames(now) >= 1.0 => {
417 window.min(last + period.mul_f64(EXTRA_MIN_FRACTION))
418 }
419 _ => window,
420 };
421 }
422 self.last_tick.map_or(now, |last| {
423 last + period.saturating_sub(self.budget(trigger, period, now))
424 })
425 }
426
427 fn window_pull(&self, period: Duration, now: Instant) -> Instant {
430 self.window_end
431 .map_or(now, |end| end - period.mul_f64(WINDOW_EARLY_FRACTION))
432 }
433
434 fn rebase_extra(&mut self, period: Duration, now: Instant) {
440 if let Some((left, at, old_period)) = self.extra
441 && old_period != period
442 {
443 let left = Self::extra_refill(left, at, old_period, now);
444 self.extra = Some((left, now, period));
445 }
446 }
447
448 fn extra_refill(left: f64, at: Instant, period: Duration, now: Instant) -> f64 {
449 let periods = now.saturating_duration_since(at).as_secs_f64() / period.as_secs_f64();
450 (left + periods * EXTRA_REFILL).min(EXTRA_CAP)
451 }
452
453 fn extra_frames(&self, now: Instant) -> f64 {
455 self.extra.map_or(EXTRA_CAP, |(left, at, period)| {
456 Self::extra_refill(left, at, period, now)
457 })
458 }
459
460 pub fn next_due(&self, period: Duration, now: Instant) -> Instant {
463 if self.windowed {
464 let due = self.window_end.map_or(now, |end| end + period);
467 return self.deferred_until.map_or(due, |held| held.max(due));
468 }
469 let due = self.last_tick.map_or(now, |last| {
470 last + if self.last_paced {
471 period.mul_f64(1.0 + TIMER_GRACE_AFTER_PULL)
472 } else {
473 period
474 }
475 });
476 self.deferred_until.map_or(due, |held| held.max(due))
477 }
478
479 pub fn window_opened(&self, period: Duration, now: Instant) -> Option<Instant> {
482 let end = self.window_end?;
483 let p = period.as_nanos().max(1);
484 Some(if now >= end {
485 end + period * ((now - end).as_nanos() / p) as u32
486 } else {
487 end - period * (end - now).as_nanos().div_ceil(p) as u32
488 })
489 }
490
491 pub fn next_wake(&self, period: Duration, now: Instant, change_pending: bool) -> Instant {
500 let due = self.next_due(period, now);
501 let pull = self.pull_at(TickTrigger::Damage, period, now);
502 let wake = if change_pending || pull > now {
503 due.min(pull)
504 } else {
505 due
506 };
507 match self.window_opened(period, now) {
508 Some(opened) if self.deferred_until.is_none() => wake.min(opened + period),
509 _ => wake,
510 }
511 }
512}
513
514#[cfg(test)]
515mod pacing_tests {
516 use super::*;
517
518 const PERIOD: Duration = Duration::from_millis(20);
519
520 fn at(base: Instant, ms: u64) -> Instant {
521 base + Duration::from_millis(ms)
522 }
523
524 #[test]
528 fn a_windowed_change_is_taken_as_it_lands_and_the_rate_holds() {
529 let base = Instant::now();
530 let mut pace = FramePace::windowed();
531 pace.record_tick(TickTrigger::Timer, PERIOD, base, true);
532 let mut frames = 1u32;
533 let mut t = base;
534 let end = base + Duration::from_secs(4);
535 let mut i = 0u64;
536 while t < end {
537 let change = t + PERIOD * 4 + Duration::from_micros(300 + (i * 3700) % 19_500);
539 i += 1;
540 loop {
541 let due = pace.next_due(PERIOD, t);
542 if due >= change {
543 break;
544 }
545 t = due;
546 pace.record_tick(TickTrigger::Timer, PERIOD, t, true);
547 frames += 1;
548 }
549 let at = pace
550 .pull_at(TickTrigger::Damage, PERIOD, change)
551 .max(change);
552 assert_eq!(at, change, "change {i} waited {:?}", at - change);
553 pace.record_tick(TickTrigger::Damage, PERIOD, at, true);
554 frames += 1;
555 t = at;
556 }
557 let windows = (t - base).as_secs_f64() / PERIOD.as_secs_f64();
558 assert!(
559 f64::from(frames) <= windows.ceil() + 1.0,
560 "{frames} frames in {windows:.1} windows"
561 );
562 }
563
564 #[test]
568 fn a_second_change_in_a_window_spends_the_reserve_then_waits_for_the_next() {
569 let base = Instant::now();
570 let mut pace = FramePace::windowed();
571 pace.record_tick(TickTrigger::Timer, PERIOD, base, true);
572 pace.record_tick(TickTrigger::Damage, PERIOD, at(base, 3), true);
573 assert_eq!(
574 pace.pull_at(TickTrigger::Damage, PERIOD, at(base, 5)),
575 at(base, 13)
576 );
577 assert_eq!(
578 pace.next_due(PERIOD, at(base, 5)),
579 at(base, 40),
580 "its window's timer frame is skipped"
581 );
582 pace.record_tick(TickTrigger::Damage, PERIOD, at(base, 13), true);
583 assert_eq!(
584 pace.next_due(PERIOD, at(base, 14)),
585 at(base, 40),
586 "the extra frame leaves the windows"
587 );
588 assert_eq!(
589 pace.pull_at(TickTrigger::Damage, PERIOD, at(base, 14)),
590 at(base, 15)
591 );
592 pace.record_tick(TickTrigger::Damage, PERIOD, at(base, 15), true);
593 pace.record_tick(TickTrigger::Damage, PERIOD, at(base, 25), true);
594 assert_eq!(
595 pace.pull_at(TickTrigger::Damage, PERIOD, at(base, 26)),
596 at(base, 35),
597 "the reserve is spent"
598 );
599 }
600
601 fn spend_windowed_reserve(period: Duration, base: Instant) -> FramePace {
604 let mut pace = FramePace::windowed();
605 pace.record_tick(TickTrigger::Timer, period, base, true);
606 for fraction in [0.1, 0.6, 0.75, 1.25] {
607 let now = base + period.mul_f64(fraction);
608 assert!(pace.due(TickTrigger::Damage, period, now));
609 pace.record_tick(TickTrigger::Damage, period, now, true);
610 }
611 pace
612 }
613
614 #[test]
615 fn extra_reserve_does_not_refill_old_time_at_a_faster_cadence() {
616 let base = Instant::now();
617 let old_period = Duration::from_millis(100);
618 let new_period = Duration::from_millis(10);
619 let mut pace = spend_windowed_reserve(old_period, base);
620
621 pace.record_tick(TickTrigger::Timer, new_period, at(base, 1_000), true);
624 pace.record_tick(TickTrigger::Damage, new_period, at(base, 1_001), true);
625 assert_eq!(
626 pace.pull_at(TickTrigger::Damage, new_period, at(base, 1_006)),
627 base + Duration::from_micros(1_007_500),
628 "the next change waits for its window rather than getting an unearned extra"
629 );
630 }
631
632 #[test]
633 fn extra_reserve_rebases_from_a_faster_to_a_slower_cadence() {
634 let base = Instant::now();
635 let old_period = Duration::from_millis(10);
636 let new_period = Duration::from_millis(100);
637 let mut pace = spend_windowed_reserve(old_period, base);
638
639 pace.record_tick(TickTrigger::Timer, new_period, at(base, 550), true);
642 pace.record_tick(TickTrigger::Timer, new_period, at(base, 560), false);
643 let left = pace.extra_frames(at(base, 650));
644 assert!(
645 (left - 1.73125).abs() < 1e-9,
646 "reserve was {left}, expected 1.73125"
647 );
648 }
649
650 #[test]
653 fn a_source_at_the_cadence_keeps_its_windows_through_a_stray_change() {
654 let base = Instant::now();
655 let mut pace = FramePace::windowed();
656 pace.record_tick(TickTrigger::Timer, PERIOD, base, true);
657 for k in 0..20u64 {
658 let frame = at(base, 2 + 20 * k);
659 assert!(
660 pace.pull_at(TickTrigger::Damage, PERIOD, frame) <= frame,
661 "frame {k}"
662 );
663 pace.record_tick(TickTrigger::Damage, PERIOD, frame, true);
664 if k == 5 {
665 let stray = at(base, 2 + 20 * k + 12);
666 assert!(pace.pull_at(TickTrigger::Damage, PERIOD, stray) <= stray);
667 pace.record_tick(TickTrigger::Damage, PERIOD, stray, true);
668 }
669 }
670 }
671
672 #[test]
675 fn windowed_changes_faster_than_the_cadence_raise_its_rate_by_the_refill_at_most() {
676 let base = Instant::now();
677 let mut pace = FramePace::windowed();
678 pace.record_tick(TickTrigger::Timer, PERIOD, base, true);
679 let (mut frames, mut changes, mut pending) = (1u32, 0u32, false);
680 let mut next_change = base + PERIOD.mul_f64(0.7);
681 for step in 1..=20_000u32 {
683 let now = base + Duration::from_micros(100) * step;
684 if now >= next_change {
685 pending = true;
686 changes += 1;
687 next_change += PERIOD.mul_f64(0.7);
688 }
689 if pending && now >= pace.pull_at(TickTrigger::Damage, PERIOD, now) {
690 pace.record_tick(TickTrigger::Damage, PERIOD, now, true);
691 pending = false;
692 frames += 1;
693 } else if now >= pace.next_due(PERIOD, now) {
694 pace.record_tick(TickTrigger::Timer, PERIOD, now, true);
695 frames += 1;
696 }
697 }
698 let closed =
700 (pace.window_end.expect("windows") - base).as_secs_f64() / PERIOD.as_secs_f64();
701 let extra = EXTRA_CAP + closed * EXTRA_REFILL;
702 assert!(changes > 130, "{changes} changes");
703 assert!(
704 f64::from(frames) >= closed.round() + 1.0
705 && f64::from(frames) <= closed.round() + 1.0 + extra,
706 "{frames} frames over {closed:.1} windows"
707 );
708 }
709
710 #[test]
713 fn unsent_ticks_keep_the_windows() {
714 let base = Instant::now();
715 let mut pace = FramePace::windowed();
716 pace.record_tick(TickTrigger::Timer, PERIOD, base, true);
717 for k in 1..=10u64 {
718 pace.record_tick(TickTrigger::Timer, PERIOD, at(base, 20 * k), false);
719 }
720 assert_eq!(pace.next_due(PERIOD, at(base, 200)), at(base, 220));
721 assert!(pace.due(TickTrigger::Damage, PERIOD, at(base, 207)));
722 pace.record_tick(TickTrigger::Damage, PERIOD, at(base, 207), true);
723 assert_eq!(pace.next_due(PERIOD, at(base, 207)), at(base, 240));
724 }
725
726 #[test]
729 fn windowed_due_follows_the_windows() {
730 let base = Instant::now();
731 let mut pace = FramePace::windowed();
732 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
733 assert!(pace.due(TickTrigger::Damage, PERIOD, at(base, 3)));
735 assert!(pace.due(TickTrigger::Input, PERIOD, at(base, 3)));
736 assert!(!pace.due(TickTrigger::Timer, PERIOD, at(base, 3)));
737 pace.ticked(TickTrigger::Damage, PERIOD, at(base, 3), false);
739 assert!(!pace.due(TickTrigger::Damage, PERIOD, at(base, 12)));
740 assert_eq!(
741 pace.pull_at(TickTrigger::Damage, PERIOD, at(base, 12)),
742 at(base, 13)
743 );
744 assert!(pace.due(TickTrigger::Damage, PERIOD, at(base, 20)));
745 assert!(!pace.due(TickTrigger::Timer, PERIOD, at(base, 20)));
748 assert!(pace.due(TickTrigger::Timer, PERIOD, at(base, 39)));
749 }
750
751 #[test]
754 fn windowed_timer_keeps_a_deferral() {
755 let base = Instant::now();
756 let mut pace = FramePace::windowed();
757 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
758 pace.defer(at(base, 70));
759 assert_eq!(pace.next_due(PERIOD, at(base, 21)), at(base, 70));
760 assert!(!pace.due(TickTrigger::Timer, PERIOD, at(base, 40)));
761 pace.ticked(TickTrigger::HostFrame, PERIOD, at(base, 45), false);
762 assert_eq!(pace.next_due(PERIOD, at(base, 46)), at(base, 80));
763 }
764
765 fn frames_of_a_callback_client(draws: Duration) -> u32 {
770 let base = Instant::now();
771 let end = base + Duration::from_secs(2);
772 let mut pace = FramePace::windowed();
773 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
774 let mut frames = 1;
775 let late = Duration::from_micros(100);
776 let mut fires = pace.next_wake(PERIOD, base, false) + late;
777 let mut commit = Some(base + draws);
778 let mut pending = false;
779 loop {
780 let now = commit.map_or(fires, |c| c.min(fires));
781 if now >= end {
782 return frames;
783 }
784 let fired = now == fires;
785 if commit == Some(now) {
786 commit = None;
787 pending = true;
788 }
789 let trigger = if pending && pace.due(TickTrigger::Damage, PERIOD, now) {
790 Some(TickTrigger::Damage)
791 } else if fired && pace.due(TickTrigger::Timer, PERIOD, now) {
792 Some(TickTrigger::Timer)
793 } else {
794 None
795 };
796 if let Some(trigger) = trigger {
797 pace.ticked(trigger, PERIOD, now, false);
798 frames += 1;
799 pending = false;
800 commit = Some(now + draws);
801 }
802 if fired {
803 fires = pace.next_wake(PERIOD, now, pending).max(now) + late;
804 }
805 }
806 }
807
808 #[test]
814 fn a_client_drawing_on_frame_callbacks_keeps_the_cadence() {
815 let most = 102.0 + EXTRA_CAP + 100.0 * EXTRA_REFILL;
816 for draws_us in [100, 2_000, 10_000, 19_000] {
817 let frames = frames_of_a_callback_client(Duration::from_micros(draws_us));
818 assert!(
819 frames >= 99 && f64::from(frames) <= most,
820 "{frames} frames in two seconds at 50 fps, drawing {draws_us} us"
821 );
822 }
823 }
824
825 #[test]
828 fn a_window_opens_on_its_boundary() {
829 let base = Instant::now();
830 let mut pace = FramePace::windowed();
831 assert_eq!(pace.window_opened(PERIOD, base), None);
832 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
833 assert_eq!(pace.window_opened(PERIOD, at(base, 1)), Some(base));
834 pace.ticked(TickTrigger::Damage, PERIOD, at(base, 3), false);
835 assert_eq!(pace.window_opened(PERIOD, at(base, 4)), Some(base));
836 assert_eq!(pace.window_opened(PERIOD, at(base, 20)), Some(at(base, 20)));
837 assert_eq!(pace.window_opened(PERIOD, at(base, 45)), Some(at(base, 40)));
838 }
839
840 #[test]
843 fn the_timer_wakes_for_an_opening_window_and_not_an_open_one() {
844 let base = Instant::now();
845 let mut pace = FramePace::windowed();
846 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
847 assert_eq!(pace.next_wake(PERIOD, at(base, 1), false), at(base, 20));
848 assert!(pace.next_wake(PERIOD, at(base, 1), true) <= at(base, 1));
849 pace.ticked(TickTrigger::Damage, PERIOD, at(base, 3), false);
850 assert_eq!(pace.next_wake(PERIOD, at(base, 4), false), at(base, 13));
851 pace.defer(at(base, 70));
852 assert_eq!(pace.next_wake(PERIOD, at(base, 4), false), at(base, 13));
853 assert_eq!(pace.next_wake(PERIOD, at(base, 21), false), at(base, 70));
854 }
855
856 #[test]
859 fn a_wake_between_the_pull_and_the_boundary_still_wakes_at_the_boundary() {
860 let base = Instant::now();
861 let mut pace = FramePace::windowed();
862 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
863 pace.ticked(TickTrigger::Damage, PERIOD, at(base, 3), false);
864 assert_eq!(pace.next_wake(PERIOD, at(base, 4), false), at(base, 13));
865 assert_eq!(pace.next_wake(PERIOD, at(base, 14), false), at(base, 20));
866 assert_eq!(pace.next_wake(PERIOD, at(base, 19), false), at(base, 20));
867 }
868
869 #[test]
874 fn late_timer_ticks_keep_the_rate() {
875 let base = Instant::now();
876 for max_late in [0.0, 0.3, 0.6, 0.9] {
877 let mut pace = FramePace::windowed();
878 pace.record_tick(TickTrigger::Timer, PERIOD, base, true);
879 let (mut seed, mut frames) = (12345u64, 1);
880 loop {
881 seed = seed
882 .wrapping_mul(6364136223846793005)
883 .wrapping_add(1442695040888963407);
884 let late = (seed >> 33) as f64 / (1u64 << 31) as f64 * max_late;
885 let at = pace.next_due(PERIOD, base) + PERIOD.mul_f64(late);
886 if at > base + PERIOD * 1000 {
887 break;
888 }
889 pace.record_tick(TickTrigger::Timer, PERIOD, at, true);
890 frames += 1;
891 }
892 assert!(
893 (1000..=1001).contains(&frames),
894 "{frames} frames at up to {max_late} late"
895 );
896 }
897 }
898
899 #[test]
904 fn a_change_taken_late_keeps_the_rate() {
905 let base = Instant::now();
906 let end = base + PERIOD * 1000;
907 let mut pace = FramePace::windowed();
908 pace.record_tick(TickTrigger::Timer, PERIOD, base, true);
909 let (mut t, mut frames, mut taken, mut k) = (base, 1, base, 0u32);
910 let mut change = base;
911 while t < end {
912 let due = pace.next_due(PERIOD, t);
914 let start = t.max(pace.pull_at(TickTrigger::Damage, PERIOD, t).min(due));
915 while change <= taken {
916 change += PERIOD.mul_f64(1.3);
917 }
918 let (now, trigger) = if change <= due.max(start) {
919 (start.max(change), TickTrigger::Damage)
920 } else {
921 (due.max(t), TickTrigger::Timer)
922 };
923 if now > end {
924 break;
925 }
926 taken = now;
927 pace.record_tick(trigger, PERIOD, now, true);
928 frames += 1;
929 k += 1;
930 t = now + PERIOD.mul_f64(if k % 6 == 0 { 1.25 } else { 0.77 });
931 }
932 assert!(
933 (999..=1001).contains(&frames),
934 "{frames} frames in 1000 periods"
935 );
936 }
937
938 #[test]
942 fn the_earliest_pull_is_the_instant_the_gate_opens() {
943 let base = Instant::now();
944 let mut pace = FramePace::default();
945 assert_eq!(
946 pace.pull_at(TickTrigger::Damage, PERIOD, base),
947 base,
948 "a fresh capture waits for nothing"
949 );
950 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
951 let earliest = pace.pull_at(TickTrigger::Damage, PERIOD, base);
952 assert_eq!(earliest, at(base, 10), "a whole budget buys half a period");
953 assert!(pace.due(TickTrigger::Damage, PERIOD, earliest));
954 assert!(!pace.due(
955 TickTrigger::Damage,
956 PERIOD,
957 earliest - Duration::from_millis(1)
958 ));
959 pace.ticked(TickTrigger::Damage, PERIOD, earliest, false);
960 assert_eq!(
961 pace.pull_at(TickTrigger::Damage, PERIOD, earliest),
962 at(base, 30),
963 "a spent budget waits the period out"
964 );
965 }
966
967 #[test]
968 fn a_fresh_capture_is_due_at_once() {
969 let pace = FramePace::default();
970 let now = Instant::now();
971 assert!(pace.due(TickTrigger::Timer, PERIOD, now));
972 assert!(pace.due(TickTrigger::Input, PERIOD, now));
973 assert_eq!(pace.next_due(PERIOD, now), now);
974 }
975
976 #[test]
977 fn the_timer_ticks_at_its_period_with_jitter_slack() {
978 let base = Instant::now();
979 let mut pace = FramePace::default();
980 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
981 assert!(!pace.due(TickTrigger::Timer, PERIOD, at(base, 17)));
982 assert!(pace.due(TickTrigger::Timer, PERIOD, at(base, 18)));
983 assert_eq!(pace.next_due(PERIOD, at(base, 5)), at(base, 20));
984 }
985
986 #[test]
987 fn input_pulls_a_frame_forward_from_a_budget() {
988 let base = Instant::now();
989 let mut pace = FramePace::default();
990 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
991 assert!(
992 !pace.due(TickTrigger::Input, PERIOD, at(base, 9)),
993 "under half a period waits for the timer"
994 );
995 assert!(pace.due(TickTrigger::Input, PERIOD, at(base, 10)));
996 pace.ticked(TickTrigger::Input, PERIOD, at(base, 10), false);
997 assert_eq!(
998 pace.next_due(PERIOD, at(base, 10)),
999 at(base, 30),
1000 "a pull off the cadence leaves the timer its period"
1001 );
1002 assert!(pace.due(TickTrigger::Timer, PERIOD, at(base, 30)));
1003 assert!(
1004 !pace.due(TickTrigger::Input, PERIOD, at(base, 25)),
1005 "the budget is spent for a second pull"
1006 );
1007 assert!(
1008 pace.due(TickTrigger::Input, PERIOD, at(base, 30)),
1009 "a whole period on, input takes the due frame"
1010 );
1011 pace.ticked(TickTrigger::Input, PERIOD, at(base, 30), false);
1012 pace.ticked(TickTrigger::Timer, PERIOD, at(base, 120), false);
1014 assert!(
1015 !pace.due(TickTrigger::Input, PERIOD, at(base, 130)),
1016 "a whole pull needs the whole budget back"
1017 );
1018 assert!(
1019 pace.due(TickTrigger::Input, PERIOD, at(base, 137)),
1020 "a small pull fits what refilled"
1021 );
1022 pace.ticked(TickTrigger::Input, PERIOD, at(base, 137), false);
1023 pace.ticked(TickTrigger::Timer, PERIOD, at(base, 500), false);
1024 assert!(
1025 pace.due(TickTrigger::Input, PERIOD, at(base, 510)),
1026 "the budget is whole again"
1027 );
1028 }
1029
1030 #[test]
1031 fn input_a_hair_slower_than_the_cadence_renders_every_move() {
1032 let base = Instant::now();
1033 let mut pace = FramePace::default();
1034 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
1035 let spacing = Duration::from_micros(20_400);
1039 assert!(FramePace::input_paced(Some(spacing), PERIOD));
1040 let mut t = 16.0;
1041 pace.ticked(TickTrigger::Input, PERIOD, at(base, 16), true);
1042 for _ in 0..50 {
1043 t += 20.4;
1044 let now = base + Duration::from_micros((t * 1000.0) as u64);
1045 assert!(
1046 pace.next_due(PERIOD, now) > now,
1047 "no timer frame slipped in ahead of the move"
1048 );
1049 assert!(
1050 pace.due(TickTrigger::Input, PERIOD, now),
1051 "the move renders its own frame"
1052 );
1053 pace.ticked(TickTrigger::Input, PERIOD, now, true);
1054 }
1055 let stopped = base + Duration::from_micros((t * 1000.0) as u64);
1056 assert_eq!(
1057 pace.next_due(PERIOD, stopped),
1058 stopped + Duration::from_millis(25),
1059 "once the moves stop, the timer resumes after the grace"
1060 );
1061 }
1062
1063 #[test]
1064 fn a_pull_that_arrives_late_pays_the_budget_back() {
1065 let base = Instant::now();
1066 let mut pace = FramePace::default();
1067 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
1068 pace.ticked(TickTrigger::Input, PERIOD, at(base, 10), true);
1069 assert!(
1070 !pace.due(TickTrigger::Input, PERIOD, at(base, 25)),
1071 "the budget is spent"
1072 );
1073 pace.ticked(TickTrigger::Input, PERIOD, at(base, 40), true);
1074 assert!(
1075 pace.due(TickTrigger::Input, PERIOD, at(base, 50)),
1076 "a pull 10 ms late earned a 10 ms pull back"
1077 );
1078 }
1079
1080 #[test]
1081 fn only_input_at_the_cadence_holds_the_timer_back() {
1082 let base = Instant::now();
1083 let mut pace = FramePace::default();
1084 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
1085 assert!(
1086 !FramePace::input_paced(Some(Duration::from_millis(7)), PERIOD),
1087 "a 143 Hz client is not paced at 50 Hz"
1088 );
1089 assert!(
1090 !FramePace::input_paced(Some(Duration::from_millis(40)), PERIOD),
1091 "nor is one at half the rate"
1092 );
1093 assert!(!FramePace::input_paced(None, PERIOD), "nor no input at all");
1094 pace.ticked(TickTrigger::Input, PERIOD, at(base, 12), false);
1095 assert_eq!(
1096 pace.next_due(PERIOD, at(base, 12)),
1097 at(base, 32),
1098 "an unpaced pull leaves the timer its period"
1099 );
1100 pace.ticked(TickTrigger::Input, PERIOD, at(base, 32), true);
1101 assert_eq!(
1102 pace.next_due(PERIOD, at(base, 32)),
1103 at(base, 57),
1104 "a paced one adds the grace"
1105 );
1106 pace.ticked(TickTrigger::HostFrame, PERIOD, at(base, 57), true);
1107 assert_eq!(
1108 pace.next_due(PERIOD, at(base, 57)),
1109 at(base, 77),
1110 "a host frame never does"
1111 );
1112 }
1113
1114 #[test]
1115 fn damage_at_the_cadence_renders_every_frame_through_vblank_jitter() {
1116 let base = Instant::now();
1117 let mut pace = FramePace::default();
1118 let steps = [21, 19, 20, 21, 21, 19, 20, 19, 21, 20];
1122 let mut t = 0u64;
1123 pace.ticked(TickTrigger::Damage, PERIOD, at(base, t), false);
1124 for (i, step) in steps.iter().cycle().take(60).enumerate() {
1125 t += step;
1126 let now = at(base, t);
1127 if i >= 2 {
1128 assert!(
1129 pace.next_due(PERIOD, now) > now,
1130 "no timer frame slipped in ahead of frame {i}"
1131 );
1132 }
1133 assert!(
1134 pace.due(TickTrigger::Damage, PERIOD, now),
1135 "frame {i} renders as it lands"
1136 );
1137 pace.ticked(TickTrigger::Damage, PERIOD, now, false);
1138 }
1139 assert_eq!(
1140 pace.next_due(PERIOD, at(base, t)),
1141 at(base, t + 25),
1142 "once it stops, the timer resumes after the grace"
1143 );
1144 }
1145
1146 #[test]
1147 fn damage_off_the_cadence_leaves_the_timer_its_period() {
1148 let base = Instant::now();
1149 let mut pace = FramePace::default();
1150 for i in 0..40 {
1152 pace.ticked(TickTrigger::Damage, PERIOD, at(base, 8 * i), false);
1153 }
1154 assert_eq!(pace.next_due(PERIOD, at(base, 312)), at(base, 332));
1155 }
1156
1157 #[test]
1158 fn a_host_frame_pulls_forward_like_input() {
1159 let base = Instant::now();
1160 let mut pace = FramePace::default();
1161 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
1162 assert!(
1163 !pace.due(TickTrigger::HostFrame, PERIOD, at(base, 9)),
1164 "under half a period waits for the timer"
1165 );
1166 assert!(pace.due(TickTrigger::HostFrame, PERIOD, at(base, 10)));
1167 pace.ticked(TickTrigger::HostFrame, PERIOD, at(base, 10), false);
1168 assert_eq!(
1169 pace.next_due(PERIOD, at(base, 10)),
1170 at(base, 30),
1171 "the cadence follows the host's frames"
1172 );
1173 assert!(
1174 !pace.due(TickTrigger::Timer, PERIOD, at(base, 20)),
1175 "the old phase's tick is skipped"
1176 );
1177 assert!(
1178 !pace.due(TickTrigger::HostFrame, PERIOD, at(base, 25)),
1179 "the budget is spent for a second pull"
1180 );
1181 assert!(
1182 pace.due(TickTrigger::HostFrame, PERIOD, at(base, 30)),
1183 "a whole period on, the frame is due"
1184 );
1185 }
1186
1187 #[test]
1188 fn a_deferred_tick_holds_the_timer_without_counting_a_frame() {
1189 let base = Instant::now();
1190 let mut pace = FramePace::default();
1191 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
1192 pace.defer(at(base, 30));
1193 assert_eq!(
1194 pace.next_due(PERIOD, at(base, 20)),
1195 at(base, 30),
1196 "the timer waits where it was held to"
1197 );
1198 assert!(
1199 pace.due(TickTrigger::HostFrame, PERIOD, at(base, 21)),
1200 "a frame a period on is still due at once"
1201 );
1202 pace.ticked(TickTrigger::HostFrame, PERIOD, at(base, 21), false);
1203 assert_eq!(
1204 pace.next_due(PERIOD, at(base, 21)),
1205 at(base, 41),
1206 "a rendered frame lifts the hold"
1207 );
1208 }
1209
1210 #[test]
1211 fn a_timer_that_wakes_late_keeps_its_rate() {
1212 let base = Instant::now();
1213 let mut pace = FramePace::default();
1214 let end = base + Duration::from_secs(2);
1215 let (mut frames, mut t) = (0, base);
1216 loop {
1217 t = pace.next_due(PERIOD, t) + Duration::from_micros(700);
1218 if t >= end {
1219 break;
1220 }
1221 assert!(pace.due(TickTrigger::Timer, PERIOD, t));
1222 pace.ticked(TickTrigger::Timer, PERIOD, t, false);
1223 frames += 1;
1224 }
1225 assert!(
1226 (99..=100).contains(&frames),
1227 "{frames} frames in two seconds at 50 fps"
1228 );
1229 }
1230
1231 #[test]
1232 fn a_stalled_timer_starts_its_cadence_again_rather_than_catch_up() {
1233 let base = Instant::now();
1234 let mut pace = FramePace::default();
1235 pace.ticked(TickTrigger::Timer, PERIOD, base, false);
1236 pace.ticked(TickTrigger::Timer, PERIOD, at(base, 75), false);
1237 assert_eq!(
1238 pace.next_due(PERIOD, at(base, 75)),
1239 at(base, 95),
1240 "a tick a period and more late counts from when it ran"
1241 );
1242 }
1243
1244 #[test]
1247 fn damage_faster_than_the_period_holds_the_rate() {
1248 let base = Instant::now();
1249 let mut pace = FramePace::default();
1250 let end = base + Duration::from_secs(2);
1251 let (mut frames, mut t) = (0, base);
1252 loop {
1253 let due = pace.next_due(PERIOD, t);
1254 t = pace.pull_at(TickTrigger::Damage, PERIOD, t).min(due).max(t);
1255 if t >= end {
1256 break;
1257 }
1258 pace.ticked(TickTrigger::Damage, PERIOD, t, false);
1259 frames += 1;
1260 t += Duration::from_micros(100);
1261 }
1262 assert!(
1263 (100..=101).contains(&frames),
1264 "{frames} frames in two seconds at 50 fps"
1265 );
1266 }
1267
1268 #[test]
1275 fn changes_between_still_periods_pull_as_a_fresh_capture_does() {
1276 for sent in [true, false] {
1277 changes_between_still_periods_pull(sent);
1278 }
1279 }
1280
1281 fn changes_between_still_periods_pull(sent: bool) {
1282 let base = Instant::now();
1283 let mut pace = FramePace::default();
1284 pace.record_tick(TickTrigger::Timer, PERIOD, base, true);
1286 let mut t = base;
1287 for i in 0..40u32 {
1288 let change = t + PERIOD * 16 + Duration::from_micros(500 + u64::from(i) * 470 % 19_000);
1289 let mut tick = t;
1290 loop {
1291 let due = pace.next_due(PERIOD, t);
1292 if due >= change {
1293 break;
1294 }
1295 t = due;
1296 tick = t;
1297 pace.record_tick(TickTrigger::Timer, PERIOD, t, sent);
1298 }
1299 let at = pace
1300 .pull_at(TickTrigger::Damage, PERIOD, change)
1301 .max(change);
1302 assert!(
1303 at <= change.max(tick + PERIOD / 2),
1304 "change {i} (still ticks sent: {sent}) waited {:?}",
1305 at - change
1306 );
1307 pace.record_tick(TickTrigger::Damage, PERIOD, at, true);
1308 let left = if sent || i == 0 {
1312 Duration::ZERO
1313 } else {
1314 PERIOD / 2
1315 };
1316 assert_eq!(
1317 pace.pull_at(TickTrigger::Damage, PERIOD, at),
1318 at + PERIOD - left,
1319 "the change after change {i} (still ticks sent: {sent}) borrows {left:?}"
1320 );
1321 t = at;
1322 }
1323 }
1324
1325 #[test]
1328 fn the_screen_is_still_a_period_and_a_half_after_its_last_change() {
1329 let base = Instant::now();
1330 let mut pace = FramePace::default();
1331 assert!(pace.still(PERIOD, base), "a capture with no change yet");
1332 pace.ticked(TickTrigger::Damage, PERIOD, base, false);
1333 assert!(
1334 !pace.still(PERIOD, at(base, 25)),
1335 "the graced timer's tick after a change's frame"
1336 );
1337 assert!(!pace.still(PERIOD, at(base, 29)));
1338 assert!(pace.still(PERIOD, at(base, 30)), "a period and a half on");
1339 }
1340
1341 #[test]
1342 fn input_faster_than_the_period_raises_the_rate_by_the_refill_at_most() {
1343 let base = Instant::now();
1344 let mut pace = FramePace::default();
1345 let end = base + Duration::from_secs(2);
1346 let (mut frames, mut next_input, mut next_timer) = (0, base, base);
1347 loop {
1348 let t = next_input.min(next_timer);
1349 if t >= end {
1350 break;
1351 }
1352 if t == next_timer {
1353 if pace.due(TickTrigger::Timer, PERIOD, t) {
1354 pace.ticked(TickTrigger::Timer, PERIOD, t, false);
1355 frames += 1;
1356 }
1357 next_timer = pace.next_due(PERIOD, t);
1358 }
1359 if t == next_input {
1360 if pace.due(TickTrigger::Input, PERIOD, t) {
1361 pace.ticked(TickTrigger::Input, PERIOD, t, false);
1362 frames += 1;
1363 next_timer = pace.next_due(PERIOD, t);
1364 }
1365 next_input = t + Duration::from_millis(7);
1366 }
1367 }
1368 assert!(
1369 (100..=104).contains(&frames),
1370 "{frames} frames in two seconds at 50 fps"
1371 );
1372 }
1373}