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//!
12//! The X11 captures, which the X server's damage reports wake, pace in windows instead
13//! (`FramePace::windowed`): one frame a period, a change taking its period's frame as it lands,
14//! as the NvFBC driver pushes frames, and a change landing in a period a frame already took
15//! sent as an extra frame from a budget that refills slowly, so a source drawing at the cadence
16//! keeps its own periods.
17
18use std::time::{Duration, Instant};
19
20/// What asks a display for a frame: its frame timer, fresh input, a frame the host just
21/// delivered under host capture, or a region the X server reports damaged. All but the timer
22/// say something has actually changed, and so may pull the cadence's phase forward.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum TickTrigger {
25    Timer,
26    Input,
27    HostFrame,
28    Damage,
29}
30
31impl TickTrigger {
32    /// Whether this trigger may render ahead of the timer, moving the cadence to its phase.
33    fn pulls_forward(self) -> bool {
34        !matches!(self, TickTrigger::Timer)
35    }
36
37    /// Whether the time this trigger may pull a frame forward by refills with wall time: fresh
38    /// input's, whose latency the pull is for. A screen that changes faster than the cadence, a
39    /// video or an application rendering unsynced, would otherwise raise the output rate by the
40    /// refill for as long as it runs.
41    fn refills(self) -> bool {
42        matches!(self, TickTrigger::Input)
43    }
44}
45
46/// The share of a frame period the timer's tick may run early by, absorbing its wakeup jitter.
47const TIMER_TICK_MIN_FRACTION: f64 = 0.9;
48/// The share of a frame period that has to pass before input or a host frame may pull the
49/// next frame forward.
50const INPUT_TICK_MIN_FRACTION: f64 = 0.5;
51/// The share of wall time that accrues as time an input pull may bring frames forward by. A
52/// frame pulled forward by some time is one the cadence earns that much later, so the
53/// sustained rate can rise above the configured one by at most this share.
54const INPUT_BORROW_REFILL: f64 = 1.0 / 32.0;
55/// The share of a frame period the timer waits beyond a whole one after an input frame while
56/// the input paces itself at the cadence, so input arriving a little slower than the timer
57/// still renders its own frames instead of landing just behind a timer frame and waiting a
58/// period.
59const TIMER_GRACE_AFTER_PULL: f64 = 0.25;
60/// The share of a frame period before the next window opens in which a change, paced in windows,
61/// takes that window's frame at once rather than waiting for it to open. One frame a window
62/// still holds the rate at the cadence; a source drawing at the cadence whose frames end just
63/// short of the boundary otherwise waits for it every frame (a 60 fps full-screen animation on
64/// Xvfb waited 0.8 ms a frame on average, in some phases of its clock against the windows').
65const WINDOW_EARLY_FRACTION: f64 = 0.25;
66/// The share of a period after the last frame from which a change landing in a window a frame
67/// already took may be sent as an extra frame (`FramePace::windowed`).
68const EXTRA_MIN_FRACTION: f64 = 0.5;
69/// The extra frames a capture paced in windows holds in reserve.
70const EXTRA_CAP: f64 = 2.0;
71/// The share of an extra frame each period adds to that reserve: the sustained rate rises
72/// above the configured one by at most this share. A slower refill leaves a source at the
73/// cadence locked behind a keystroke's extra frame for longer (a 60 Hz animation with a
74/// keystroke every 0.7 s waited 7.6 ms a frame at the median at 1/64, 12.5 ms at 1/128).
75const EXTRA_REFILL: f64 = 1.0 / 32.0;
76/// The periods without a reported change after which the screen counts as still
77/// (`FramePace::still`): more than the timer waits after a change's frame.
78const STILL_PERIODS: f64 = 1.5;
79/// The input spacings, as shares of the frame period, that count as pacing at the cadence:
80/// a client at the cadence give or take its jitter. A faster client is borrowing frames and a
81/// slower one is content the timer paces, and the timer keeps its period behind either.
82const PACED_INPUT_MIN_FRACTION: f64 = 0.75;
83const PACED_INPUT_MAX_FRACTION: f64 = 1.25;
84
85/// A capture's frame pacing: the last frame it rendered, and how far a pull may still bring
86/// the next one forward.
87///
88/// The shared frame timer fires for the earliest due capture; each capture renders on it once
89/// its own period has (nearly) passed. Fresh input may also render a frame, and so may a frame
90/// the host compositor delivers under host capture or a region the X server reports damaged:
91/// a whole period after the last one the pull renders at once, and from half a period on it
92/// brings the frame forward, spending the time it comes early by from a budget that is paid
93/// back by a pull that arrives late and holds half a period at most. Input's budget also
94/// refills at `INPUT_BORROW_REFILL`; the others' does not (`TickTrigger::refills`). A pointer
95/// moving at the client's refresh rate is thus captured as it lands rather than up to a period
96/// later, and a host's or an application's frames are published as they arrive rather than on
97/// the next timer tick, while input at a rate the cadence cannot follow raises the output rate
98/// by no more than the refill, and frames reported faster than the cadence not at all.
99///
100/// After an input frame, while the input's own spacing sits at the cadence
101/// (`PACED_INPUT_MIN_FRACTION` to `PACED_INPUT_MAX_FRACTION` of a period), the timer stands
102/// back for `TIMER_GRACE_AFTER_PULL` beyond a period, so such input keeps rendering its own
103/// frames whether it runs a hair faster or slower than the timer, or jitters around it: a timer
104/// that resumed exactly a period after the frame would render just ahead of a move landing a
105/// little late, which then could not pull and waited a period. When the input stops, the timer
106/// renders after the grace and carries on from there. A faster client, a slower one, and a
107/// host's frames leave the timer its period as before.
108///
109/// Damage the X server reports at the cadence earns the same grace: it is an application
110/// presenting on the server's vblank, which runs on a clock of its own (a framebuffer server's
111/// fake vblank fires on a millisecond timer, so its frames land up to a millisecond either side
112/// of the period). Without the grace a timer tick taken just ahead of such a frame captures the
113/// one before it again, and the frame it missed is published late or never.
114#[derive(Debug, Default)]
115pub struct FramePace {
116    /// Last tick this capture actually rendered.
117    pub last_tick: Option<Instant>,
118    /// Whether that frame was input's, with the input pacing itself at the cadence, so the
119    /// timer stands back for the next move.
120    last_paced: bool,
121    /// Input's budget left when it was last spent, and when that was; a fresh capture holds the
122    /// cap.
123    borrow_budget: Option<(Duration, Instant)>,
124    /// The budget host frames and damage pull from, which refills only from their own late
125    /// frames; a fresh capture holds the cap.
126    phase_budget: Option<Duration>,
127    /// Where the timer was held off to by a tick that rendered nothing (host capture with no
128    /// fresh frame), so it neither spins nor counts against the next frame.
129    deferred_until: Option<Instant>,
130    /// The last damage frame, and the running average of the spacing between damage frames.
131    last_damage: Option<Instant>,
132    /// A still tick gave the next pull a fresh capture's budget, which that pull spends whole.
133    rephase: bool,
134    damage_spacing: Option<Duration>,
135    /// Paced in windows (`Self::windowed`): the end of the last period a frame closed.
136    window_end: Option<Instant>,
137    windowed: bool,
138    /// Paced in windows, the extra frames left when one was last sent, when, and the period
139    /// that owns the elapsed time since then; a fresh capture holds the cap.
140    extra: Option<(f64, Instant, Duration)>,
141}
142
143impl FramePace {
144    /// Pacing in windows, for a capture a change wakes (XShm and DRI3 on the X server's damage
145    /// reports, the compositor on a client's commit, input or a host frame): each
146    /// period is a window holding one frame. A change takes its window's frame as it lands; a
147    /// window no change took ends on the timer's frame. A change is published at once unless a
148    /// change already took its window, in which case it waits for the next window, taking it
149    /// `WINDOW_EARLY_FRACTION` of a period before it opens: the NvFBC capture's schedule, whose
150    /// driver pushes frames the same way. Without windows a change could come forward no
151    /// earlier than half a period after the last frame, and one landing just after a timer
152    /// frame waited that long.
153    ///
154    /// Such a change is instead sent as an extra frame where the reserve holds one
155    /// (`EXTRA_CAP`, refilled at `EXTRA_REFILL` a period), from `EXTRA_MIN_FRACTION` of a
156    /// period after the last frame, and leaves the windows as they were. Taken from the next
157    /// window, it would close that window ahead of a source drawing at the cadence, whose next
158    /// frame then lands in a window already taken and waits as long again: any wait sustains
159    /// itself, a source a hair faster than the cadence grows it to a period, and a 60 Hz
160    /// application on its own clock under a 60 fps capture waited a median 6 ms a frame and up
161    /// to 15, and one keystroke during a 60 Hz animation held each of its frames 12.5 ms.
162    pub fn windowed() -> Self {
163        Self {
164            windowed: true,
165            ..Self::default()
166        }
167    }
168
169    /// Where the window a frame at `now` falls in ends: the open window after the last one a
170    /// frame closed, or a later one where whole windows went by without a tick.
171    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    /// A tick paced in windows: a timer tick ends the open window whether it sent a frame or
185    /// not, which keeps the windows on the timer's schedule; a change's frame closes the window
186    /// it lands in, and a change not sent leaves it open. A tick that came late (a busy capture
187    /// or compositor thread, an encode that ran long) still ends the window it was due for, so
188    /// the next opens on schedule and the rate holds: moving the grid to the late tick gave up
189    /// the time it was late by, every time, and a loaded session streamed below its rate. A
190    /// change taken less than a window after the open window's timer was due ends that window
191    /// too, or a screen changing a little slower than the cadence lost a window to each encode
192    /// that ran past a period. Only windows that passed whole without a tick are given up. A
193    /// change sent before the next window would take it is an extra frame, which spends the
194    /// reserve and leaves the windows as they were.
195    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    /// Whether a capture rendering every `period` is due a frame at `now`, given what asks.
227    /// Paced in windows, a change is due once its window is open and the timer at the open
228    /// window's end, give or take the timer's own lateness.
229    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    /// Whether input spaced `interval` apart paces itself at the cadence of `period`.
250    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    /// Record the frame rendered at `now`. A pull ahead of the period spends the budget by
258    /// what it came early; one behind the period pays back what it came late. A timer tick
259    /// within half a period of when it was due counts as on time, so a timer that wakes a
260    /// little late, as one does under load, keeps the rate instead of losing what it was late
261    /// by every frame. `input_paced` says whether the input driving the capture paces itself
262    /// at the cadence (`Self::input_paced`), which is what lets the timer stand back after an
263    /// input frame.
264    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    /// Fold a damage frame at `now` into the spacing of damage frames, and say whether that
312    /// spacing sits at the cadence of `period`.
313    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    /// How long after the last frame the timer's own tick renders again.
326    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    /// Whether the screen stands still at `now`: no change reported for `STILL_PERIODS` periods,
335    /// longer than the timer waits after a change's frame, so a screen that keeps changing never
336    /// does.
337    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    /// Give the next change the pull a fresh capture has, for a tick that found the screen
343    /// still (`Self::still`): it moves the cadence onto the change and leaves nothing to
344    /// borrow, so the frame after it waits a whole period.
345    pub fn stood_still(&mut self) {
346        self.phase_budget = None;
347        self.rephase = true;
348    }
349
350    /// Record a tick of a capture the Damage extension wakes (the X11 paths), once it is known
351    /// whether it sent a frame. Paced in windows (`Self::windowed`), a timer tick ends the open
352    /// window and a change's frame closes the one it lands in. Otherwise a sent frame counts
353    /// (`Self::ticked`), one that published nothing holds the timer off a period
354    /// (`Self::defer`), and a sent timer's tick that finds the screen still gives the change
355    /// after it a fresh capture's pull (`Self::stood_still`): where every tick is sent
356    /// (streaming mode, XShm), the changes before it spent the budget, which a still screen's
357    /// frames, never late, do not pay back, and each change would wait for the next tick. A
358    /// tick that sent nothing spent nothing, and the budget refills alone.
359    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    /// Hold the timer off this capture until `until` without a frame counting: a tick that
381    /// published nothing must neither spin the timer nor move the cadence a fresh frame is
382    /// measured against.
383    pub fn defer(&mut self, until: Instant) {
384        self.deferred_until = Some(until);
385    }
386
387    /// Time since this capture last rendered, if it has.
388    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    /// How far a pull by `trigger` may bring the next frame forward at `now`.
394    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    /// The earliest `trigger`, one that pulls forward, may render: a whole period after the
410    /// last frame, less what its budget still allows it to borrow. A caller that waits until
411    /// then need not test `due` again, since a budget only grows with time.
412    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    /// Paced in windows, when a change may take the window after the one the last frame
428    /// closed: from `WINDOW_EARLY_FRACTION` of a period before it opens.
429    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    /// Settle the reserve at its old cadence before moving its checkpoint to a changed period.
435    /// The first mutating tick to observe the new period is the rebase anchor: `pull_at` and
436    /// `next_wake` are immutable queries, so until that tick they keep accounting at the old
437    /// period. Whole and fractional old periods are both retained in the reserve rather than
438    /// reset or reinterpreted at the new cadence.
439    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    /// The extra frames a capture paced in windows may send at `now`.
454    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    /// When the timer is next due for this capture: a period after the last frame, and the
461    /// grace beyond that after an input frame the input paced.
462    pub fn next_due(&self, period: Duration, now: Instant) -> Instant {
463        if self.windowed {
464            // The end of the open window, where no change took it, unless a tick that published
465            // nothing held the timer off (`Self::defer`).
466            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    /// Paced in windows, when the window `now` falls in opened: the boundary a display's vblank
480    /// would fall on. None before the first frame.
481    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    /// When a frame timer armed at `now` next has to fire for this capture. It wakes for a window
492    /// still to open whether or not a change waits on it yet: a change landing later has nothing
493    /// else to wake it, since a client that draws on frame callbacks goes quiet after its commit.
494    /// A window already open takes a change as it lands, so only a change that could not be
495    /// rendered keeps the timer close. It never sleeps past the next boundary unless a tick
496    /// deferred it (`Self::defer`): a boundary is where the clients' frame callbacks go out, and
497    /// a wake for something else landing after a change's pull would otherwise sleep to the end
498    /// of the next window, costing a client that draws on those callbacks a frame.
499    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    /// Paced in windows, a change is taken as it lands wherever it falls after a timer frame,
525    /// and the timer frame its window would have ended on is skipped: the rate stays the
526    /// cadence's.
527    #[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            // A change once every 5 periods, at a phase that walks through the window.
538            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    /// A second change in a window a change already took is sent as an extra frame half a
565    /// period after the last, leaving the windows as they were, and once the reserve is spent
566    /// waits for the next window, taken `WINDOW_EARLY_FRACTION` of a period before it opens.
567    #[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    /// Leave the reserve nearly spent through ticks the public gate allows, without reaching
602    /// into the accounting to manufacture a state a capture could not reach.
603    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        // The first ticks at the new rate settle the old reserve and take a window. Nearly a
622        // second elapsed at the old rate must not become a whole reserve at the new one.
623        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        // Rebase 53.75 old periods after the last extra, then record another tick at the new
640        // rate. The old fraction is counted once, and the last 90 ms refill at the new period.
641        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    /// A source drawing at the cadence keeps its own windows through a change landing between
651    /// two of its frames: each of its frames is taken as it lands.
652    #[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    /// Changes faster than the cadence are published one a window, and the extra frames the
673    /// reserve and its refill allow.
674    #[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        // Stepped every 0.1 ms: a change is pending until its window opens, else the timer ends it.
682        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        // The first frame's, one for each window closed, and the extra frames.
699        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    /// With streaming off the still ticks send nothing, and still keep the windows on the
711    /// timer's schedule: a change after a still spell is taken at once.
712    #[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    /// The compositor asks `due` rather than `pull_at`: paced in windows, a change is due as
727    /// its window opens and not before, and the timer at the open window's end.
728    #[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        // The window after the timer's frame is open at once.
734        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        // A change takes it; the next is an extra frame from half a period on, at 13 ms.
738        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        // The timer skips the window the change took and ends the next one, at 40 ms, a
746        // little early counting.
747        assert!(!pace.due(TickTrigger::Timer, PERIOD, at(base, 20)));
748        assert!(pace.due(TickTrigger::Timer, PERIOD, at(base, 39)));
749    }
750
751    /// A tick that published nothing holds the timer off in windows too (host capture with no
752    /// fresh frame), and the next frame lifts the hold.
753    #[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    /// The compositor's loop: the timer is armed only when it fires, a little late as a real
766    /// timer does, a client commits `draws` after each frame's callback and then waits for the
767    /// next, and a commit in an open window is served as it lands. The frames a run renders in
768    /// two seconds at 50 fps.
769    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    /// A client that waits for its callback goes quiet after its commit, so only the timer can
809    /// take a commit that landed in a window a frame already took: it wakes as each window opens,
810    /// and the client keeps the cadence however early in the period it commits, and the extra
811    /// frames at most. A commit taken ahead of its window may take the one that ends just past
812    /// the run, one frame more.
813    #[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    /// The window a moment falls in opened on the boundary before it, whichever frame closed the
826    /// last window: a timer's at the window's end or a change's as it landed.
827    #[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    /// With nothing waiting the timer sleeps past a window already open to its end, so a still
841    /// screen does not spin it, and a change a frame could not take keeps it close.
842    #[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    /// A wake for something else, landing after a change's pull and before the next boundary,
857    /// still wakes the timer at that boundary, where the clients' frame callbacks go out.
858    #[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    /// A timer that fires late, as a busy capture or compositor thread makes it, still ends the
870    /// window it was due for: streaming, the frames sent in 1000 periods stay 1000 whether the
871    /// ticks come on time or up to 0.9 of a period late (moving the grid to each late tick lost
872    /// 9% of them at 0.6 and 23% at 0.9).
873    #[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    /// A screen changing a little slower than the cadence, captured by a loop whose encode now
900    /// and then runs past a period: a change taken after the open window's timer was due ends
901    /// that window, as a late timer tick does, so 1000 periods still send 1000 frames (closing
902    /// the window it landed in gave up the overdue one, 924 frames here).
903    #[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            // The loop's wait: a pending change once the window opens, else the timer.
913            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    /// The earliest pull and the pull gate agree, so a caller may wait for the one and skip
939    /// the other: a fresh capture pulls at once, a full budget buys half a period, and a spent
940    /// one leaves the timer its whole period.
941    #[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        // About 4 ms refilled by now: a pull of 10 ms does not fit, one of 3 ms does.
1013        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        // A client pacing at 20.4 ms against a 20 ms period: its first move lands late in the
1036        // period and pulls its frame; each later move lands after a whole period and renders
1037        // at once, and no timer tick falls between two of them.
1038        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        // An application presenting on a 20 ms vblank that fires on a millisecond grid, so its
1119        // frames land 19, 20, or 21 ms apart: each is published as it lands, and no timer
1120        // tick falls between two of them once the spacing reads as paced.
1121        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        // A pointer sprite moving at 125 Hz against a 20 ms period.
1151        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    /// The X11 capture's wait: the earliest a damage pull may render, else the timer, with a
1245    /// change always waiting to be read, as a screen redrawn faster than the cadence has.
1246    #[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    /// The X11 captures' wait on a screen that stands still between changes: their still ticks
1269    /// give each change the whole pull, so a change is due as it lands from half a period after
1270    /// a tick on, however many came before it (counted without it, the changes spent the budget
1271    /// and waited for the timer), and where the still ticks were sent the frame after it waits a
1272    /// whole period. It holds whether a still tick sends a frame (streaming mode, and XShm
1273    /// always) or publishes nothing.
1274    #[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        // A capture's first frame is always sent.
1285        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            // Where the still ticks were sent, the pull spent a fresh capture's budget whole; where
1309            // none was, the change came a still spell after the last frame, which refilled it. The
1310            // first change follows the first frame, which is sent on a screen still till then.
1311            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    /// The screen stands still only once a period and a half has passed without a change: the
1326    /// timer's tick after a change's frame, graced or not, still counts as a frame.
1327    #[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}