Skip to main content

pixelflux/
pace.rs

1//! The frame pacing every capture backend shares: when a display is due a frame, and how far
2//! a change that just happened may bring the next one forward.
3//!
4//! A capture renders on its frame timer, and something that actually happened -- input the
5//! compositor processed, a frame a host delivered, a region the X server reports damaged --
6//! may render one ahead of that timer. Input's pull spends from a budget that refills slowly,
7//! so input faster than the cadence raises the output rate by no more than the refill; a
8//! change the screen or a host reports spends only what its own late frames paid back, so it
9//! moves the cadence onto the frames it reports without raising the rate. Either way a change
10//! arriving on its own schedule is published as it lands rather than up to a period later.
11
12use std::time::{Duration, Instant};
13
14/// What asks a display for a frame: its frame timer, fresh input, a frame the host just
15/// delivered under host capture, or a region the X server reports damaged. All but the timer
16/// say something has actually changed, and so may pull the cadence's phase forward.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum TickTrigger {
19    Timer,
20    Input,
21    HostFrame,
22    Damage,
23}
24
25impl TickTrigger {
26    /// Whether this trigger may render ahead of the timer, moving the cadence to its phase.
27    fn pulls_forward(self) -> bool {
28        !matches!(self, TickTrigger::Timer)
29    }
30
31    /// Whether the time this trigger may pull a frame forward by refills with wall time: fresh
32    /// input's, whose latency the pull is for. A screen that changes faster than the cadence, a
33    /// video or an application rendering unsynced, would otherwise raise the output rate by the
34    /// refill for as long as it runs.
35    fn refills(self) -> bool {
36        matches!(self, TickTrigger::Input)
37    }
38}
39
40/// The share of a frame period the timer's tick may run early by, absorbing its wakeup jitter.
41const TIMER_TICK_MIN_FRACTION: f64 = 0.9;
42/// The share of a frame period that has to pass before input or a host frame may pull the
43/// next frame forward.
44const INPUT_TICK_MIN_FRACTION: f64 = 0.5;
45/// The share of wall time that accrues as time an input pull may bring frames forward by. A
46/// frame pulled forward by some time is one the cadence earns that much later, so the
47/// sustained rate can rise above the configured one by at most this share.
48const INPUT_BORROW_REFILL: f64 = 1.0 / 32.0;
49/// The share of a frame period the timer waits beyond a whole one after an input frame while
50/// the input paces itself at the cadence, so input arriving a little slower than the timer
51/// still renders its own frames instead of landing just behind a timer frame and waiting a
52/// period.
53const TIMER_GRACE_AFTER_PULL: f64 = 0.25;
54/// The input spacings, as shares of the frame period, that count as pacing at the cadence:
55/// a client at the cadence give or take its jitter. A faster client is borrowing frames and a
56/// slower one is content the timer paces, and the timer keeps its period behind either.
57const PACED_INPUT_MIN_FRACTION: f64 = 0.75;
58const PACED_INPUT_MAX_FRACTION: f64 = 1.25;
59
60/// A capture's frame pacing: the last frame it rendered, and how far a pull may still bring
61/// the next one forward.
62///
63/// The shared frame timer fires for the earliest due capture; each capture renders on it once
64/// its own period has (nearly) passed. Fresh input may also render a frame, and so may a frame
65/// the host compositor delivers under host capture or a region the X server reports damaged:
66/// a whole period after the last one the pull renders at once, and from half a period on it
67/// brings the frame forward, spending the time it comes early by from a budget that is paid
68/// back by a pull that arrives late and holds half a period at most. Input's budget also
69/// refills at `INPUT_BORROW_REFILL`; the others' does not (`TickTrigger::refills`). A pointer
70/// moving at the client's refresh rate is thus captured as it lands rather than up to a period
71/// later, and a host's or an application's frames are published as they arrive rather than on
72/// the next timer tick, while input at a rate the cadence cannot follow raises the output rate
73/// by no more than the refill, and frames reported faster than the cadence not at all.
74///
75/// After an input frame, while the input's own spacing sits at the cadence
76/// (`PACED_INPUT_MIN_FRACTION` to `PACED_INPUT_MAX_FRACTION` of a period), the timer stands
77/// back for `TIMER_GRACE_AFTER_PULL` beyond a period, so such input keeps rendering its own
78/// frames whether it runs a hair faster or slower than the timer, or jitters around it: a timer
79/// that resumed exactly a period after the frame would render just ahead of a move landing a
80/// little late, which then could not pull and waited a period. When the input stops, the timer
81/// renders after the grace and carries on from there. A faster client, a slower one, and a
82/// host's frames leave the timer its period as before.
83///
84/// Damage the X server reports at the cadence earns the same grace: it is an application
85/// presenting on the server's vblank, which runs on a clock of its own (a framebuffer server's
86/// fake vblank fires on a millisecond timer, so its frames land up to a millisecond either side
87/// of the period). Without the grace a timer tick taken just ahead of such a frame captures the
88/// one before it again, and the frame it missed is published late or never.
89#[derive(Debug, Default)]
90pub struct FramePace {
91    /// Last tick this capture actually rendered.
92    pub last_tick: Option<Instant>,
93    /// Whether that frame was input's, with the input pacing itself at the cadence, so the
94    /// timer stands back for the next move.
95    last_paced: bool,
96    /// Input's budget left when it was last spent, and when that was; a fresh capture holds the
97    /// cap.
98    borrow_budget: Option<(Duration, Instant)>,
99    /// The budget host frames and damage pull from, which refills only from their own late
100    /// frames; a fresh capture holds the cap.
101    phase_budget: Option<Duration>,
102    /// Where the timer was held off to by a tick that rendered nothing (host capture with no
103    /// fresh frame), so it neither spins nor counts against the next frame.
104    deferred_until: Option<Instant>,
105    /// The last damage frame, and the running average of the spacing between damage frames.
106    last_damage: Option<Instant>,
107    damage_spacing: Option<Duration>,
108}
109
110impl FramePace {
111    /// Whether a capture rendering every `period` is due a frame at `now`, given what asks.
112    pub fn due(&self, trigger: TickTrigger, period: Duration, now: Instant) -> bool {
113        let Some(last) = self.last_tick else {
114            return true;
115        };
116        let elapsed = now.saturating_duration_since(last);
117        if !trigger.pulls_forward() {
118            return elapsed >= self.timer_wait(period);
119        }
120        elapsed >= period
121            || (elapsed >= period.mul_f64(INPUT_TICK_MIN_FRACTION)
122                && self.budget(trigger, period, now) >= period - elapsed)
123    }
124
125    /// Whether input spaced `interval` apart paces itself at the cadence of `period`.
126    pub fn input_paced(interval: Option<Duration>, period: Duration) -> bool {
127        interval.is_some_and(|i| {
128            i >= period.mul_f64(PACED_INPUT_MIN_FRACTION)
129                && i <= period.mul_f64(PACED_INPUT_MAX_FRACTION)
130        })
131    }
132
133    /// Record the frame rendered at `now`. A pull ahead of the period spends the budget by
134    /// what it came early; one behind the period pays back what it came late. A timer tick
135    /// within half a period of when it was due counts as on time, so a timer that wakes a
136    /// little late, as one does under load, keeps the rate instead of losing what it was late
137    /// by every frame. `input_paced` says whether the input driving the capture paces itself
138    /// at the cadence (`Self::input_paced`), which is what lets the timer stand back after an
139    /// input frame.
140    pub fn ticked(
141        &mut self,
142        trigger: TickTrigger,
143        period: Duration,
144        now: Instant,
145        input_paced: bool,
146    ) {
147        if trigger.pulls_forward()
148            && let Some(last) = self.last_tick
149        {
150            let elapsed = now.saturating_duration_since(last);
151            let budget = self.budget(trigger, period, now);
152            let left = if elapsed < period {
153                budget.saturating_sub(period - elapsed)
154            } else {
155                (budget + (elapsed - period)).min(period.mul_f64(INPUT_TICK_MIN_FRACTION))
156            };
157            if trigger.refills() {
158                self.borrow_budget = Some((left, now));
159            } else {
160                self.phase_budget = Some(left);
161            }
162        }
163        let mut at = now;
164        if trigger == TickTrigger::Timer && self.last_tick.is_some() {
165            let scheduled = self.next_due(period, now);
166            let off = if now > scheduled {
167                now - scheduled
168            } else {
169                scheduled - now
170            };
171            if off < period / 2 {
172                at = scheduled;
173            }
174        }
175        let damage_paced = trigger == TickTrigger::Damage && self.damage_paced(period, now);
176        self.last_paced = (trigger == TickTrigger::Input && input_paced) || damage_paced;
177        self.last_tick = Some(at);
178        self.deferred_until = None;
179    }
180
181    /// Fold a damage frame at `now` into the spacing of damage frames, and say whether that
182    /// spacing sits at the cadence of `period`.
183    fn damage_paced(&mut self, period: Duration, now: Instant) -> bool {
184        if let Some(prev) = self.last_damage {
185            let dt = now.saturating_duration_since(prev).min(period * 4);
186            self.damage_spacing = Some(
187                self.damage_spacing
188                    .map_or(dt, |avg| avg.mul_f64(0.875) + dt.mul_f64(0.125)),
189            );
190        }
191        self.last_damage = Some(now);
192        Self::input_paced(self.damage_spacing, period)
193    }
194
195    /// How long after the last frame the timer's own tick renders again.
196    fn timer_wait(&self, period: Duration) -> Duration {
197        if self.last_paced {
198            period.mul_f64(1.0 + TIMER_GRACE_AFTER_PULL)
199        } else {
200            period.mul_f64(TIMER_TICK_MIN_FRACTION)
201        }
202    }
203
204    /// Hold the timer off this capture until `until` without a frame counting: a tick that
205    /// published nothing must neither spin the timer nor move the cadence a fresh frame is
206    /// measured against.
207    pub fn defer(&mut self, until: Instant) {
208        self.deferred_until = Some(until);
209    }
210
211    /// Time since this capture last rendered, if it has.
212    pub fn since_last_tick(&self, now: Instant) -> Option<Duration> {
213        self.last_tick
214            .map(|last| now.saturating_duration_since(last))
215    }
216
217    /// How far a pull by `trigger` may bring the next frame forward at `now`.
218    fn budget(&self, trigger: TickTrigger, period: Duration, now: Instant) -> Duration {
219        let cap = period.mul_f64(INPUT_TICK_MIN_FRACTION);
220        if !trigger.refills() {
221            return self.phase_budget.unwrap_or(cap);
222        }
223        match self.borrow_budget {
224            None => cap,
225            Some((left, at)) => (left
226                + now
227                    .saturating_duration_since(at)
228                    .mul_f64(INPUT_BORROW_REFILL))
229            .min(cap),
230        }
231    }
232
233    /// The earliest `trigger`, one that pulls forward, may render: a whole period after the
234    /// last frame, less what its budget still allows it to borrow. A caller that waits until
235    /// then need not test `due` again, since a budget only grows with time.
236    pub fn pull_at(&self, trigger: TickTrigger, period: Duration, now: Instant) -> Instant {
237        self.last_tick.map_or(now, |last| {
238            last + period.saturating_sub(self.budget(trigger, period, now))
239        })
240    }
241
242    /// When the timer is next due for this capture: a period after the last frame, and the
243    /// grace beyond that after an input frame the input paced.
244    pub fn next_due(&self, period: Duration, now: Instant) -> Instant {
245        let due = self.last_tick.map_or(now, |last| {
246            last + if self.last_paced {
247                period.mul_f64(1.0 + TIMER_GRACE_AFTER_PULL)
248            } else {
249                period
250            }
251        });
252        self.deferred_until.map_or(due, |held| held.max(due))
253    }
254}
255
256#[cfg(test)]
257mod pacing_tests {
258    use super::*;
259
260    const PERIOD: Duration = Duration::from_millis(20);
261
262    fn at(base: Instant, ms: u64) -> Instant {
263        base + Duration::from_millis(ms)
264    }
265
266    /// The earliest pull and the pull gate agree, so a caller may wait for the one and skip
267    /// the other: a fresh capture pulls at once, a full budget buys half a period, and a spent
268    /// one leaves the timer its whole period.
269    #[test]
270    fn the_earliest_pull_is_the_instant_the_gate_opens() {
271        let base = Instant::now();
272        let mut pace = FramePace::default();
273        assert_eq!(
274            pace.pull_at(TickTrigger::Damage, PERIOD, base),
275            base,
276            "a fresh capture waits for nothing"
277        );
278        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
279        let earliest = pace.pull_at(TickTrigger::Damage, PERIOD, base);
280        assert_eq!(earliest, at(base, 10), "a whole budget buys half a period");
281        assert!(pace.due(TickTrigger::Damage, PERIOD, earliest));
282        assert!(!pace.due(
283            TickTrigger::Damage,
284            PERIOD,
285            earliest - Duration::from_millis(1)
286        ));
287        pace.ticked(TickTrigger::Damage, PERIOD, earliest, false);
288        assert_eq!(
289            pace.pull_at(TickTrigger::Damage, PERIOD, earliest),
290            at(base, 30),
291            "a spent budget waits the period out"
292        );
293    }
294
295    #[test]
296    fn a_fresh_capture_is_due_at_once() {
297        let pace = FramePace::default();
298        let now = Instant::now();
299        assert!(pace.due(TickTrigger::Timer, PERIOD, now));
300        assert!(pace.due(TickTrigger::Input, PERIOD, now));
301        assert_eq!(pace.next_due(PERIOD, now), now);
302    }
303
304    #[test]
305    fn the_timer_ticks_at_its_period_with_jitter_slack() {
306        let base = Instant::now();
307        let mut pace = FramePace::default();
308        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
309        assert!(!pace.due(TickTrigger::Timer, PERIOD, at(base, 17)));
310        assert!(pace.due(TickTrigger::Timer, PERIOD, at(base, 18)));
311        assert_eq!(pace.next_due(PERIOD, at(base, 5)), at(base, 20));
312    }
313
314    #[test]
315    fn input_pulls_a_frame_forward_from_a_budget() {
316        let base = Instant::now();
317        let mut pace = FramePace::default();
318        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
319        assert!(
320            !pace.due(TickTrigger::Input, PERIOD, at(base, 9)),
321            "under half a period waits for the timer"
322        );
323        assert!(pace.due(TickTrigger::Input, PERIOD, at(base, 10)));
324        pace.ticked(TickTrigger::Input, PERIOD, at(base, 10), false);
325        assert_eq!(
326            pace.next_due(PERIOD, at(base, 10)),
327            at(base, 30),
328            "a pull off the cadence leaves the timer its period"
329        );
330        assert!(pace.due(TickTrigger::Timer, PERIOD, at(base, 30)));
331        assert!(
332            !pace.due(TickTrigger::Input, PERIOD, at(base, 25)),
333            "the budget is spent for a second pull"
334        );
335        assert!(
336            pace.due(TickTrigger::Input, PERIOD, at(base, 30)),
337            "a whole period on, input takes the due frame"
338        );
339        pace.ticked(TickTrigger::Input, PERIOD, at(base, 30), false);
340        // About 4 ms refilled by now: a pull of 10 ms does not fit, one of 3 ms does.
341        pace.ticked(TickTrigger::Timer, PERIOD, at(base, 120), false);
342        assert!(
343            !pace.due(TickTrigger::Input, PERIOD, at(base, 130)),
344            "a whole pull needs the whole budget back"
345        );
346        assert!(
347            pace.due(TickTrigger::Input, PERIOD, at(base, 137)),
348            "a small pull fits what refilled"
349        );
350        pace.ticked(TickTrigger::Input, PERIOD, at(base, 137), false);
351        pace.ticked(TickTrigger::Timer, PERIOD, at(base, 500), false);
352        assert!(
353            pace.due(TickTrigger::Input, PERIOD, at(base, 510)),
354            "the budget is whole again"
355        );
356    }
357
358    #[test]
359    fn input_a_hair_slower_than_the_cadence_renders_every_move() {
360        let base = Instant::now();
361        let mut pace = FramePace::default();
362        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
363        // A client pacing at 20.4 ms against a 20 ms period: its first move lands late in the
364        // period and pulls its frame; each later move lands after a whole period and renders
365        // at once, and no timer tick falls between two of them.
366        let spacing = Duration::from_micros(20_400);
367        assert!(FramePace::input_paced(Some(spacing), PERIOD));
368        let mut t = 16.0;
369        pace.ticked(TickTrigger::Input, PERIOD, at(base, 16), true);
370        for _ in 0..50 {
371            t += 20.4;
372            let now = base + Duration::from_micros((t * 1000.0) as u64);
373            assert!(
374                pace.next_due(PERIOD, now) > now,
375                "no timer frame slipped in ahead of the move"
376            );
377            assert!(
378                pace.due(TickTrigger::Input, PERIOD, now),
379                "the move renders its own frame"
380            );
381            pace.ticked(TickTrigger::Input, PERIOD, now, true);
382        }
383        let stopped = base + Duration::from_micros((t * 1000.0) as u64);
384        assert_eq!(
385            pace.next_due(PERIOD, stopped),
386            stopped + Duration::from_millis(25),
387            "once the moves stop, the timer resumes after the grace"
388        );
389    }
390
391    #[test]
392    fn a_pull_that_arrives_late_pays_the_budget_back() {
393        let base = Instant::now();
394        let mut pace = FramePace::default();
395        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
396        pace.ticked(TickTrigger::Input, PERIOD, at(base, 10), true);
397        assert!(
398            !pace.due(TickTrigger::Input, PERIOD, at(base, 25)),
399            "the budget is spent"
400        );
401        pace.ticked(TickTrigger::Input, PERIOD, at(base, 40), true);
402        assert!(
403            pace.due(TickTrigger::Input, PERIOD, at(base, 50)),
404            "a pull 10 ms late earned a 10 ms pull back"
405        );
406    }
407
408    #[test]
409    fn only_input_at_the_cadence_holds_the_timer_back() {
410        let base = Instant::now();
411        let mut pace = FramePace::default();
412        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
413        assert!(
414            !FramePace::input_paced(Some(Duration::from_millis(7)), PERIOD),
415            "a 143 Hz client is not paced at 50 Hz"
416        );
417        assert!(
418            !FramePace::input_paced(Some(Duration::from_millis(40)), PERIOD),
419            "nor is one at half the rate"
420        );
421        assert!(!FramePace::input_paced(None, PERIOD), "nor no input at all");
422        pace.ticked(TickTrigger::Input, PERIOD, at(base, 12), false);
423        assert_eq!(
424            pace.next_due(PERIOD, at(base, 12)),
425            at(base, 32),
426            "an unpaced pull leaves the timer its period"
427        );
428        pace.ticked(TickTrigger::Input, PERIOD, at(base, 32), true);
429        assert_eq!(
430            pace.next_due(PERIOD, at(base, 32)),
431            at(base, 57),
432            "a paced one adds the grace"
433        );
434        pace.ticked(TickTrigger::HostFrame, PERIOD, at(base, 57), true);
435        assert_eq!(
436            pace.next_due(PERIOD, at(base, 57)),
437            at(base, 77),
438            "a host frame never does"
439        );
440    }
441
442    #[test]
443    fn damage_at_the_cadence_renders_every_frame_through_vblank_jitter() {
444        let base = Instant::now();
445        let mut pace = FramePace::default();
446        // An application presenting on a 20 ms vblank that fires on a millisecond grid, so its
447        // frames land 19, 20, or 21 ms apart: each is published as it lands, and no timer
448        // tick falls between two of them once the spacing reads as paced.
449        let steps = [21, 19, 20, 21, 21, 19, 20, 19, 21, 20];
450        let mut t = 0u64;
451        pace.ticked(TickTrigger::Damage, PERIOD, at(base, t), false);
452        for (i, step) in steps.iter().cycle().take(60).enumerate() {
453            t += step;
454            let now = at(base, t);
455            if i >= 2 {
456                assert!(
457                    pace.next_due(PERIOD, now) > now,
458                    "no timer frame slipped in ahead of frame {i}"
459                );
460            }
461            assert!(
462                pace.due(TickTrigger::Damage, PERIOD, now),
463                "frame {i} renders as it lands"
464            );
465            pace.ticked(TickTrigger::Damage, PERIOD, now, false);
466        }
467        assert_eq!(
468            pace.next_due(PERIOD, at(base, t)),
469            at(base, t + 25),
470            "once it stops, the timer resumes after the grace"
471        );
472    }
473
474    #[test]
475    fn damage_off_the_cadence_leaves_the_timer_its_period() {
476        let base = Instant::now();
477        let mut pace = FramePace::default();
478        // A pointer sprite moving at 125 Hz against a 20 ms period.
479        for i in 0..40 {
480            pace.ticked(TickTrigger::Damage, PERIOD, at(base, 8 * i), false);
481        }
482        assert_eq!(pace.next_due(PERIOD, at(base, 312)), at(base, 332));
483    }
484
485    #[test]
486    fn a_host_frame_pulls_forward_like_input() {
487        let base = Instant::now();
488        let mut pace = FramePace::default();
489        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
490        assert!(
491            !pace.due(TickTrigger::HostFrame, PERIOD, at(base, 9)),
492            "under half a period waits for the timer"
493        );
494        assert!(pace.due(TickTrigger::HostFrame, PERIOD, at(base, 10)));
495        pace.ticked(TickTrigger::HostFrame, PERIOD, at(base, 10), false);
496        assert_eq!(
497            pace.next_due(PERIOD, at(base, 10)),
498            at(base, 30),
499            "the cadence follows the host's frames"
500        );
501        assert!(
502            !pace.due(TickTrigger::Timer, PERIOD, at(base, 20)),
503            "the old phase's tick is skipped"
504        );
505        assert!(
506            !pace.due(TickTrigger::HostFrame, PERIOD, at(base, 25)),
507            "the budget is spent for a second pull"
508        );
509        assert!(
510            pace.due(TickTrigger::HostFrame, PERIOD, at(base, 30)),
511            "a whole period on, the frame is due"
512        );
513    }
514
515    #[test]
516    fn a_deferred_tick_holds_the_timer_without_counting_a_frame() {
517        let base = Instant::now();
518        let mut pace = FramePace::default();
519        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
520        pace.defer(at(base, 30));
521        assert_eq!(
522            pace.next_due(PERIOD, at(base, 20)),
523            at(base, 30),
524            "the timer waits where it was held to"
525        );
526        assert!(
527            pace.due(TickTrigger::HostFrame, PERIOD, at(base, 21)),
528            "a frame a period on is still due at once"
529        );
530        pace.ticked(TickTrigger::HostFrame, PERIOD, at(base, 21), false);
531        assert_eq!(
532            pace.next_due(PERIOD, at(base, 21)),
533            at(base, 41),
534            "a rendered frame lifts the hold"
535        );
536    }
537
538    #[test]
539    fn a_timer_that_wakes_late_keeps_its_rate() {
540        let base = Instant::now();
541        let mut pace = FramePace::default();
542        let end = base + Duration::from_secs(2);
543        let (mut frames, mut t) = (0, base);
544        loop {
545            t = pace.next_due(PERIOD, t) + Duration::from_micros(700);
546            if t >= end {
547                break;
548            }
549            assert!(pace.due(TickTrigger::Timer, PERIOD, t));
550            pace.ticked(TickTrigger::Timer, PERIOD, t, false);
551            frames += 1;
552        }
553        assert!(
554            (99..=100).contains(&frames),
555            "{frames} frames in two seconds at 50 fps"
556        );
557    }
558
559    #[test]
560    fn a_stalled_timer_starts_its_cadence_again_rather_than_catch_up() {
561        let base = Instant::now();
562        let mut pace = FramePace::default();
563        pace.ticked(TickTrigger::Timer, PERIOD, base, false);
564        pace.ticked(TickTrigger::Timer, PERIOD, at(base, 75), false);
565        assert_eq!(
566            pace.next_due(PERIOD, at(base, 75)),
567            at(base, 95),
568            "a tick a period and more late counts from when it ran"
569        );
570    }
571
572    /// The X11 capture's wait: the earliest a damage pull may render, else the timer, with a
573    /// change always waiting to be read, as a screen redrawn faster than the cadence has.
574    #[test]
575    fn damage_faster_than_the_period_holds_the_rate() {
576        let base = Instant::now();
577        let mut pace = FramePace::default();
578        let end = base + Duration::from_secs(2);
579        let (mut frames, mut t) = (0, base);
580        loop {
581            let due = pace.next_due(PERIOD, t);
582            t = pace.pull_at(TickTrigger::Damage, PERIOD, t).min(due).max(t);
583            if t >= end {
584                break;
585            }
586            pace.ticked(TickTrigger::Damage, PERIOD, t, false);
587            frames += 1;
588            t += Duration::from_micros(100);
589        }
590        assert!(
591            (100..=101).contains(&frames),
592            "{frames} frames in two seconds at 50 fps"
593        );
594    }
595
596    #[test]
597    fn input_faster_than_the_period_raises_the_rate_by_the_refill_at_most() {
598        let base = Instant::now();
599        let mut pace = FramePace::default();
600        let end = base + Duration::from_secs(2);
601        let (mut frames, mut next_input, mut next_timer) = (0, base, base);
602        loop {
603            let t = next_input.min(next_timer);
604            if t >= end {
605                break;
606            }
607            if t == next_timer {
608                if pace.due(TickTrigger::Timer, PERIOD, t) {
609                    pace.ticked(TickTrigger::Timer, PERIOD, t, false);
610                    frames += 1;
611                }
612                next_timer = pace.next_due(PERIOD, t);
613            }
614            if t == next_input {
615                if pace.due(TickTrigger::Input, PERIOD, t) {
616                    pace.ticked(TickTrigger::Input, PERIOD, t, false);
617                    frames += 1;
618                    next_timer = pace.next_due(PERIOD, t);
619                }
620                next_input = t + Duration::from_millis(7);
621            }
622        }
623        assert!(
624            (100..=104).contains(&frames),
625            "{frames} frames in two seconds at 50 fps"
626        );
627    }
628}