1use std::collections::VecDeque;
12use std::fmt;
13use std::time::{Duration, Instant};
14
15const DEFAULT_INTERVAL_WINDOW: usize = 240;
16const MIN_ESTIMATION_INTERVALS: usize = 10;
18const MISSED_INTERVAL_FACTOR: f64 = 1.5;
20
21#[derive(Debug)]
27pub struct PacingDiagnostics {
28 intervals: VecDeque<Duration>,
29 window: usize,
30 last_presented_at: Option<Instant>,
31 presented_frames: u64,
32 untimed_frames: u64,
33 missed_intervals: u64,
34}
35
36impl Default for PacingDiagnostics {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl PacingDiagnostics {
43 #[must_use]
45 pub fn new() -> Self {
46 Self::with_window(DEFAULT_INTERVAL_WINDOW)
47 }
48
49 #[must_use]
53 pub fn with_window(window: usize) -> Self {
54 Self {
55 intervals: VecDeque::new(),
56 window: window.max(1),
57 last_presented_at: None,
58 presented_frames: 0,
59 untimed_frames: 0,
60 missed_intervals: 0,
61 }
62 }
63
64 pub fn record_presented(&mut self, presented_at: Instant) {
69 self.presented_frames += 1;
70 if let Some(previous) = self.last_presented_at
71 && let Some(interval) = presented_at.checked_duration_since(previous)
72 && !interval.is_zero()
73 {
74 if let Some(cadence) = self.estimated_cadence()
75 && interval.as_secs_f64() > cadence.as_secs_f64() * MISSED_INTERVAL_FACTOR
76 {
77 self.missed_intervals += 1;
78 }
79 if self.intervals.len() >= self.window {
80 self.intervals.pop_front();
81 }
82 self.intervals.push_back(interval);
83 }
84 self.last_presented_at = Some(presented_at);
85 }
86
87 pub fn record_untimed_presented(&mut self) {
90 self.presented_frames += 1;
91 self.untimed_frames += 1;
92 }
93
94 #[must_use]
98 pub fn estimated_cadence(&self) -> Option<Duration> {
99 if self.intervals.len() < MIN_ESTIMATION_INTERVALS {
100 return None;
101 }
102 let mut sorted: Vec<Duration> = self.intervals.iter().copied().collect();
103 sorted.sort_unstable();
104 Some(sorted[(sorted.len() - 1) / 2])
105 }
106
107 #[must_use]
109 pub fn report(&self) -> PacingReport {
110 let mut sorted: Vec<Duration> = self.intervals.iter().copied().collect();
111 sorted.sort_unstable();
112 let recent_intervals = (!sorted.is_empty()).then(|| IntervalSummary {
113 samples: sorted.len(),
114 min: sorted[0],
115 median: sorted[(sorted.len() - 1) / 2],
116 p95: sorted[percentile_rank(sorted.len(), 95)],
117 max: sorted[sorted.len() - 1],
118 });
119 PacingReport {
120 presented_frames: self.presented_frames,
121 untimed_frames: self.untimed_frames,
122 estimated_cadence: self.estimated_cadence(),
123 recent_intervals,
124 missed_intervals: self.missed_intervals,
125 }
126 }
127}
128
129fn percentile_rank(length: usize, percent: usize) -> usize {
131 (percent * length).div_ceil(100).max(1) - 1
132}
133
134const PACING_STALENESS_LIMIT: Duration = Duration::from_millis(250);
137
138#[derive(Debug)]
158pub struct FramePacer {
159 diagnostics: PacingDiagnostics,
160 last_presented: Option<Instant>,
161 last_schedule_at: Option<Instant>,
162}
163
164impl Default for FramePacer {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl FramePacer {
171 #[must_use]
173 pub fn new() -> Self {
174 Self {
175 diagnostics: PacingDiagnostics::new(),
176 last_presented: None,
177 last_schedule_at: None,
178 }
179 }
180
181 pub fn record_presented(&mut self, presented_at: Instant) {
183 self.diagnostics.record_presented(presented_at);
184 self.last_presented = Some(match self.last_presented {
185 Some(previous) => previous.max(presented_at),
186 None => presented_at,
187 });
188 }
189
190 pub fn record_untimed_presented(&mut self) {
192 self.diagnostics.record_untimed_presented();
193 }
194
195 pub const fn resume(&mut self, now: Instant) {
201 self.last_schedule_at = Some(now);
202 }
203
204 #[must_use]
206 pub fn report(&self) -> PacingReport {
207 self.diagnostics.report()
208 }
209
210 pub fn schedule(&mut self, now: Instant) -> FrameSchedule {
212 let elapsed = self.last_schedule_at.map_or(Duration::ZERO, |previous| {
213 now.saturating_duration_since(previous)
214 });
215 self.last_schedule_at = Some(now);
216 match self.display_intervals(now, elapsed) {
217 Some(frame_delta) => FrameSchedule {
218 frame_delta,
219 paced: true,
220 },
221 None => FrameSchedule {
222 frame_delta: elapsed,
223 paced: false,
224 },
225 }
226 }
227
228 fn display_intervals(&self, now: Instant, elapsed: Duration) -> Option<Duration> {
231 let cadence = self.diagnostics.estimated_cadence()?;
232 let presented = self.last_presented?;
233 if now.saturating_duration_since(presented) > PACING_STALENESS_LIMIT {
234 return None;
235 }
236 let slack = cadence / 4;
242 let intervals = (elapsed + slack).as_nanos() / cadence.as_nanos();
243 u32::try_from(intervals.max(1))
244 .ok()
245 .map(|intervals| cadence * intervals)
246 }
247}
248
249#[derive(Clone, Copy, Debug, Eq, PartialEq)]
251#[must_use = "a schedule carries the frame delta the simulation should consume"]
252pub struct FrameSchedule {
253 frame_delta: Duration,
254 paced: bool,
255}
256
257impl FrameSchedule {
258 pub(crate) const fn idle() -> Self {
260 Self {
261 frame_delta: Duration::ZERO,
262 paced: false,
263 }
264 }
265
266 #[must_use]
268 pub const fn frame_delta(self) -> Duration {
269 self.frame_delta
270 }
271
272 #[must_use]
275 pub const fn paced(self) -> bool {
276 self.paced
277 }
278}
279
280#[derive(Clone, Copy, Debug, Eq, PartialEq)]
282pub struct IntervalSummary {
283 pub samples: usize,
285 pub min: Duration,
287 pub median: Duration,
289 pub p95: Duration,
291 pub max: Duration,
293}
294
295#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297pub struct PacingReport {
298 pub presented_frames: u64,
300 pub untimed_frames: u64,
302 pub estimated_cadence: Option<Duration>,
304 pub recent_intervals: Option<IntervalSummary>,
306 pub missed_intervals: u64,
308}
309
310impl fmt::Display for PacingReport {
311 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
312 write!(
313 formatter,
314 "{} presented frames ({} without a display time)",
315 self.presented_frames, self.untimed_frames
316 )?;
317 if let Some(cadence) = self.estimated_cadence {
318 write!(
319 formatter,
320 ", estimated cadence {:.3} ms",
321 cadence.as_secs_f64() * 1_000.0
322 )?;
323 }
324 if let Some(intervals) = self.recent_intervals {
325 write!(
326 formatter,
327 ", recent intervals (n={}) min {:.3} ms, median {:.3} ms, p95 {:.3} ms, max {:.3} ms",
328 intervals.samples,
329 intervals.min.as_secs_f64() * 1_000.0,
330 intervals.median.as_secs_f64() * 1_000.0,
331 intervals.p95.as_secs_f64() * 1_000.0,
332 intervals.max.as_secs_f64() * 1_000.0,
333 )?;
334 }
335 write!(formatter, ", {} missed intervals", self.missed_intervals)
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use std::time::{Duration, Instant};
342
343 use super::{FramePacer, MIN_ESTIMATION_INTERVALS, PACING_STALENESS_LIMIT, PacingDiagnostics};
344
345 const STEP: Duration = Duration::from_micros(16_667);
346
347 fn pacer_after_steady_presents(count: u32) -> (FramePacer, Instant) {
349 let mut pacer = FramePacer::new();
350 let base = Instant::now();
351 let mut at = base;
352 for _ in 1..count {
353 pacer.record_presented(at);
354 at += STEP;
355 }
356 pacer.record_presented(at);
357 (pacer, at)
358 }
359
360 fn diagnostics_after_steady_frames(count: usize) -> (PacingDiagnostics, Instant) {
361 let mut diagnostics = PacingDiagnostics::new();
362 let base = Instant::now();
363 let mut at = base;
364 for _ in 0..count {
365 diagnostics.record_presented(at);
366 at += STEP;
367 }
368 (diagnostics, at)
369 }
370
371 #[test]
372 fn steady_frames_estimate_their_cadence() {
373 let (diagnostics, _) = diagnostics_after_steady_frames(60);
374 let report = diagnostics.report();
375 assert_eq!(report.presented_frames, 60);
376 assert_eq!(report.untimed_frames, 0);
377 assert_eq!(report.estimated_cadence, Some(STEP));
378 assert_eq!(report.missed_intervals, 0);
379 let intervals = report.recent_intervals.unwrap();
380 assert_eq!(intervals.samples, 59);
381 assert_eq!(intervals.min, STEP);
382 assert_eq!(intervals.max, STEP);
383 }
384
385 #[test]
386 fn a_skipped_vsync_counts_as_missed_once_estimation_exists() {
387 let (mut diagnostics, mut at) = diagnostics_after_steady_frames(30);
388 at += STEP;
389 diagnostics.record_presented(at);
390 assert_eq!(diagnostics.report().missed_intervals, 1);
391 assert_eq!(diagnostics.report().estimated_cadence, Some(STEP));
392 }
393
394 #[test]
395 fn estimation_requires_enough_intervals() {
396 let (diagnostics, _) = diagnostics_after_steady_frames(MIN_ESTIMATION_INTERVALS);
397 assert_eq!(diagnostics.report().estimated_cadence, None);
398 let (diagnostics, _) = diagnostics_after_steady_frames(MIN_ESTIMATION_INTERVALS + 1);
399 assert!(diagnostics.report().estimated_cadence.is_some());
400 }
401
402 #[test]
403 fn untimed_and_non_monotonic_frames_count_without_intervals() {
404 let mut diagnostics = PacingDiagnostics::new();
405 let base = Instant::now();
406 diagnostics.record_presented(base);
407 diagnostics.record_presented(base);
408 diagnostics.record_untimed_presented();
409 let report = diagnostics.report();
410 assert_eq!(report.presented_frames, 3);
411 assert_eq!(report.untimed_frames, 1);
412 assert!(report.recent_intervals.is_none());
413 }
414
415 #[test]
416 fn jittered_schedule_gaps_still_advance_one_display_interval() {
417 let (mut pacer, mut presented) = pacer_after_steady_presents(30);
420 let mut now = presented + Duration::from_millis(1);
421 for jitter_ms in [3_u64, 18, 2, 17, 4, 16] {
422 let schedule = pacer.schedule(now);
423 assert!(schedule.paced());
424 assert_eq!(schedule.frame_delta(), STEP, "jitter {jitter_ms} ms");
425 presented += STEP;
426 pacer.record_presented(presented);
427 now += Duration::from_millis(jitter_ms);
428 }
429 }
430
431 #[test]
432 fn a_late_schedule_advances_by_whole_missed_intervals() {
433 let (mut pacer, last_presented) = pacer_after_steady_presents(30);
434 let first_at = last_presented + Duration::from_millis(1);
435 let _ = pacer.schedule(first_at);
436 let second = pacer.schedule(first_at + STEP * 2 + Duration::from_millis(2));
437 assert!(second.paced());
438 assert_eq!(second.frame_delta(), STEP * 2);
439 }
440
441 #[test]
442 fn a_build_start_spike_short_of_two_intervals_stays_one_interval() {
443 let (mut pacer, last_presented) = pacer_after_steady_presents(30);
444 let first_at = last_presented + Duration::from_millis(1);
445 let _ = pacer.schedule(first_at);
446 let spike = pacer.schedule(first_at + STEP + STEP * 7 / 10);
447 assert!(spike.paced());
448 assert_eq!(spike.frame_delta(), STEP);
449 }
450
451 #[test]
452 fn back_to_back_schedules_keep_the_one_interval_floor() {
453 let (mut pacer, last_presented) = pacer_after_steady_presents(30);
454 let now = last_presented + Duration::from_millis(1);
455 let first = pacer.schedule(now);
456 let second = pacer.schedule(now + Duration::from_millis(1));
457 assert_eq!(first.frame_delta(), STEP);
458 assert_eq!(second.frame_delta(), STEP);
459 }
460
461 #[test]
462 fn missing_and_stale_feedback_fall_back_to_wall_clock() {
463 let mut pacer = FramePacer::new();
464 let base = Instant::now();
465 let unestimated = pacer.schedule(base);
466 assert!(!unestimated.paced());
467 assert_eq!(unestimated.frame_delta(), Duration::ZERO);
468 let next = pacer.schedule(base + Duration::from_millis(7));
469 assert!(!next.paced());
470 assert_eq!(next.frame_delta(), Duration::from_millis(7));
471
472 let (mut pacer, last_presented) = pacer_after_steady_presents(30);
473 let _ = pacer.schedule(last_presented + Duration::from_millis(1));
474 let stale_at = last_presented + Duration::from_millis(1) + PACING_STALENESS_LIMIT + STEP;
475 let stale = pacer.schedule(stale_at);
476 assert!(!stale.paced());
477 assert_eq!(stale.frame_delta(), PACING_STALENESS_LIMIT + STEP);
478 }
479
480 #[test]
481 fn the_interval_window_is_bounded() {
482 let mut diagnostics = PacingDiagnostics::with_window(4);
483 let mut at = Instant::now();
484 for _ in 0..20 {
485 diagnostics.record_presented(at);
486 at += STEP;
487 }
488 assert_eq!(diagnostics.report().recent_intervals.unwrap().samples, 4);
489 }
490}