1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! How far ahead of its vsync slot a paced frame starts.
//!
//! The compositor shows a frame at the earliest refresh only when the frame
//! is ready by that refresh's deadline, its GPU work included; on a Pixel 9
//! Pro at 120 Hz the deadline falls about 8.3 ms after the vsync callback a
//! frame starts on, and a frame ready a little later waits a whole refresh
//! more. Starting a share of a period early catches the earlier refresh when
//! the frame is short enough. A longer frame only starts early and then
//! waits as long, packed closer to the frame before it, and how early is
//! enough depends on how long the frame and its GPU work take, so the lead
//! is measured rather than assumed: now and then a window of frames runs at
//! one of the other leads, and the lead whose frames reached the screen
//! sooner after being queued is kept.
/// Shown frames whose queue-to-screen times are averaged per window.
pub(crate) const WINDOW: i64 = 90;
/// Frames shown after the lead changes that started before it did, left out
/// of the next window.
pub(crate) const SETTLE: u32 = 6;
/// How much sooner, on average, a trial's frames must reach the screen for
/// its lead to be kept: less is noise.
const MARGIN_NS: i64 = 1_000_000;
/// The leads a frame may start at, in tenths of the refresh period. A short
/// frame on the Pixel 9 Pro needs 0.3; on the Mate 20 X a ticker frame
/// needs 0.3 too, while grid frames, whose GPU work runs 6 ms past their
/// queueing, need 0.5. Longer leads left feed missing vsyncs.
const LEADS_TENTHS: [i64; 3] = [0, 3, 5];
/// How long the kept lead runs before another is tried, and the longest
/// that grows to while trials keep failing.
const FIRST_HOLD_NS: i64 = 2_000_000_000;
const LONGEST_HOLD_NS: i64 = 32_000_000_000;
pub(crate) struct FrameLead {
/// The lead kept, as an index into [`LEADS_TENTHS`].
kept: usize,
/// The lead on trial, if a trial runs.
trial: Option<usize>,
/// The lead tried last, so trials take the other leads in turn.
last_tried: usize,
settle: u32,
sum_ns: i64,
count: i64,
baseline_ns: Option<i64>,
next_trial_ns: Option<i64>,
hold_ns: i64,
}
impl Default for FrameLead {
fn default() -> Self {
Self {
kept: 0,
trial: None,
last_tried: 0,
settle: 0,
sum_ns: 0,
count: 0,
baseline_ns: None,
next_trial_ns: None,
hold_ns: FIRST_HOLD_NS,
}
}
}
impl FrameLead {
/// How long before its slot's vsync a frame starts, for a display
/// refreshing every `vsync_period_ns`.
pub(crate) fn lead_ns(&self, vsync_period_ns: i64) -> i64 {
vsync_period_ns * LEADS_TENTHS[self.trial.unwrap_or(self.kept)] / 10
}
/// Records how long a frame took from being queued to being shown, at
/// `now_ns`: closes a window every [`WINDOW`] frames, starts a trial of
/// another lead once the kept one has held long enough, and keeps the
/// trial's lead when it brought frames to the screen sooner.
pub(crate) fn record(&mut self, latency_ns: i64, now_ns: i64) {
if self.settle > 0 {
self.settle -= 1;
return;
}
self.sum_ns = self.sum_ns.saturating_add(latency_ns);
self.count += 1;
if self.count < WINDOW {
return;
}
let mean_ns = self.sum_ns / WINDOW;
self.sum_ns = 0;
self.count = 0;
if let Some(trial) = self.trial.take() {
self.end_trial(trial, mean_ns, now_ns);
return;
}
self.baseline_ns = Some(mean_ns);
if self.next_trial_ns.is_none_or(|at| now_ns >= at) {
self.trial = Some(self.next_lead_to_try());
self.settle = SETTLE;
}
}
/// Keeps `trial`'s lead when its window's mean beat the kept lead's by
/// the margin. Every failed trial doubles the hold before the next: a
/// trial of a worse lead costs its window's frames, and a scene that has
/// settled on its lead should not keep paying for trials.
fn end_trial(&mut self, trial: usize, mean_ns: i64, now_ns: i64) {
if self
.baseline_ns
.is_some_and(|baseline| mean_ns + MARGIN_NS < baseline)
{
self.kept = trial;
self.baseline_ns = Some(mean_ns);
self.hold_ns = FIRST_HOLD_NS;
} else {
self.settle = SETTLE;
self.hold_ns = (self.hold_ns * 2).min(LONGEST_HOLD_NS);
}
self.next_trial_ns = Some(now_ns + self.hold_ns);
}
/// The next lead other than the kept one, in turn after the last tried.
fn next_lead_to_try(&mut self) -> usize {
let mut next = (self.last_tried + 1) % LEADS_TENTHS.len();
if next == self.kept {
next = (next + 1) % LEADS_TENTHS.len();
}
self.last_tried = next;
next
}
/// Goes back to starting frames on their slot and forgets the window in
/// progress and any trial. A lead the frames cannot keep up with shows as
/// missed vsyncs, and missed vsyncs are what make the pacer rise a level.
pub(crate) fn fall_back(&mut self) {
self.kept = 0;
self.hold_ns = FIRST_HOLD_NS;
self.next_trial_ns = None;
self.reset();
}
/// Forgets the window in progress and any trial, keeping the lead
/// learned so far: latencies from before a change of pacing level say
/// nothing about frames after it.
pub(crate) fn reset(&mut self) {
self.trial = None;
self.settle = SETTLE;
self.sum_ns = 0;
self.count = 0;
self.baseline_ns = None;
}
}
#[cfg(test)]
#[path = "tests/frame_lead_tests.rs"]
mod tests;