use crate::frame_lead::FrameLead;
const HISTORY: usize = 3;
const FULL_HISTORY: usize = 16;
const PRESENT_LOG: usize = 16;
const SKIP_SPACING_NS: i64 = 500_000_000;
const SLOT_TOLERANCE_NS: i64 = 1_000_000;
const SHOWN_FRESH_VSYNCS: i64 = 6;
const MISS_WINDOW: usize = 64;
const SHALLOW_MISSES_TO_RISE: usize = 6;
const BUFFERED_MISSES_TO_RISE: usize = 8;
const FIRST_HOLD_NS: i64 = 4_000_000_000;
const LONGEST_HOLD_NS: i64 = 64_000_000_000;
const STABLE_LEVEL_NS: i64 = 5_000_000_000;
#[derive(Default)]
pub(crate) struct PresentLog {
presents: std::collections::VecDeque<(u32, i64)>,
}
impl PresentLog {
pub(crate) fn record(&mut self, id: u32, presented_ns: i64) {
if self.presents.len() == PRESENT_LOG {
self.presents.pop_front();
}
self.presents.push_back((id, presented_ns));
}
pub(crate) fn shown(&mut self, id: u32, shown_ns: i64) -> Option<ShownPresent> {
let position = self
.presents
.iter()
.position(|(present, _)| *present == id)?;
let presented_ns = self.presents[position].1;
let behind = self
.presents
.iter()
.skip(position + 1)
.filter(|(_, presented)| *presented < shown_ns)
.count();
self.presents.drain(..=position);
Some(ShownPresent {
queued_behind: u32::try_from(behind).ok()?,
presented_ns,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ShownPresent {
pub(crate) queued_behind: u32,
pub(crate) presented_ns: i64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Level {
Shallow,
Buffered,
Unpaced,
}
impl Level {
fn depth(self) -> Option<u32> {
match self {
Self::Shallow => Some(1),
Self::Buffered => Some(2),
Self::Unpaced => None,
}
}
fn misses_to_rise(self) -> Option<usize> {
match self {
Self::Shallow => Some(SHALLOW_MISSES_TO_RISE),
Self::Buffered => Some(BUFFERED_MISSES_TO_RISE),
Self::Unpaced => None,
}
}
fn failure_slot(self) -> Option<usize> {
match self {
Self::Shallow => Some(0),
Self::Buffered => Some(1),
Self::Unpaced => None,
}
}
fn above(self) -> Self {
match self {
Self::Shallow => Self::Buffered,
Self::Buffered | Self::Unpaced => Self::Unpaced,
}
}
fn below(self) -> Self {
match self {
Self::Unpaced => Self::Buffered,
Self::Buffered | Self::Shallow => Self::Shallow,
}
}
}
#[derive(Clone, Copy)]
struct Stage {
level: Level,
since_ns: i64,
until_ns: Option<i64>,
}
pub(crate) struct FramePacer {
started_slot_ns: Option<i64>,
depths: Vec<u32>,
last_shown_ns: Option<i64>,
last_skip_ns: Option<i64>,
last_extra_ns: Option<i64>,
misses: Vec<bool>,
stage: Option<Stage>,
holds_ns: [i64; 2],
failed: [bool; 2],
lead: FrameLead,
}
impl Default for FramePacer {
fn default() -> Self {
Self {
started_slot_ns: None,
depths: Vec::with_capacity(FULL_HISTORY),
last_shown_ns: None,
last_skip_ns: None,
last_extra_ns: None,
misses: Vec::with_capacity(MISS_WINDOW),
stage: None,
holds_ns: [FIRST_HOLD_NS; 2],
failed: [false; 2],
lead: FrameLead::default(),
}
}
}
impl FramePacer {
pub(crate) fn record_shown(&mut self, shown_ns: i64, queued_behind: u32, vsync_period_ns: i64) {
if self.depths.len() == FULL_HISTORY {
self.depths.remove(0);
}
self.depths.push(queued_behind);
let gap = self.last_shown_ns.map(|previous| shown_ns - previous);
self.last_shown_ns = Some(shown_ns);
let Some(stage) = self.stage else {
self.enter(Level::Unpaced, shown_ns, None);
return;
};
if self.misses.len() == MISS_WINDOW {
self.misses.remove(0);
}
self.misses.push(
gap.is_some_and(|gap| gap * 2 > vsync_period_ns * 3 && gap <= vsync_period_ns * 3),
);
let missed = self.misses.iter().filter(|missed| **missed).count();
if stage
.level
.misses_to_rise()
.is_some_and(|threshold| missed >= threshold)
{
self.rise(stage, shown_ns);
}
}
fn retrying(&self, stage: Stage, now_ns: i64) -> bool {
let failed_before = stage
.level
.failure_slot()
.is_some_and(|slot| self.failed[slot]);
failed_before && now_ns - stage.since_ns < STABLE_LEVEL_NS
}
fn rise(&mut self, stage: Stage, now_ns: i64) {
let retried_soon = self.retrying(stage, now_ns);
if let Some(slot) = stage.level.failure_slot() {
self.failed[slot] = true;
}
let above = stage.level.above();
let hold = match above {
Level::Shallow => return,
Level::Buffered => &mut self.holds_ns[0],
Level::Unpaced => &mut self.holds_ns[1],
};
if now_ns - stage.since_ns >= STABLE_LEVEL_NS {
*hold = FIRST_HOLD_NS;
} else if retried_soon {
*hold = (*hold * 4).min(LONGEST_HOLD_NS);
}
let until_ns = now_ns + *hold;
self.lead.fall_back();
self.enter(above, now_ns, Some(until_ns));
}
fn enter(&mut self, level: Level, now_ns: i64, until_ns: Option<i64>) {
self.stage = Some(Stage {
level,
since_ns: now_ns,
until_ns,
});
self.misses.clear();
self.lead.reset();
}
pub(crate) fn record_latency(&mut self, queued_ns: i64, shown_ns: i64) {
if self
.stage
.is_some_and(|stage| stage.level.depth().is_some())
{
self.lead.record(shown_ns - queued_ns, shown_ns);
}
}
pub(crate) fn lead_wake_ns(
&self,
now_ns: i64,
vsync_ns: i64,
vsync_period_ns: i64,
) -> Option<i64> {
let lead_ns = self.lead.lead_ns(vsync_period_ns);
if lead_ns == 0 || vsync_ns <= 0 || vsync_period_ns <= 0 || now_ns < vsync_ns {
return None;
}
let next_slot_ns =
vsync_ns + ((now_ns + lead_ns - vsync_ns) / vsync_period_ns + 1) * vsync_period_ns;
Some(next_slot_ns - lead_ns)
}
fn level(&mut self, now_ns: i64) -> Option<Level> {
let stage = self.stage?;
let hold_over = stage.until_ns.is_none_or(|until| now_ns >= until);
let step_down = match stage.level {
Level::Unpaced => {
hold_over
&& Level::Buffered
.depth()
.is_some_and(|depth| self.queue_deeper_than(depth, FULL_HISTORY))
}
Level::Buffered => stage.until_ns.is_some() && hold_over,
Level::Shallow => false,
};
if !step_down {
return Some(stage.level);
}
let below = stage.level.below();
let until_ns = (below == Level::Buffered).then(|| now_ns + self.holds_ns[0]);
self.enter(below, now_ns, until_ns);
Some(below)
}
fn queue_deeper_than(&self, depth: u32, frames: usize) -> bool {
self.depths.len() >= frames
&& self.depths[self.depths.len() - frames..]
.iter()
.all(|queued| *queued > depth)
}
pub(crate) fn current_level(&self) -> Option<Level> {
self.stage.map(|stage| stage.level)
}
pub(crate) fn current_lead_ns(&self, vsync_period_ns: i64) -> i64 {
self.lead.lead_ns(vsync_period_ns)
}
pub(crate) fn begin_frame(&mut self, now_ns: i64, vsync_ns: i64, vsync_period_ns: i64) -> bool {
let depth = self.level(now_ns).and_then(Level::depth);
let slot_ns = self.open_slot(now_ns, vsync_ns, vsync_period_ns);
let Some(depth) = depth else {
self.started_slot_ns = slot_ns.or(self.started_slot_ns);
return true;
};
if vsync_ns <= 0 || vsync_period_ns <= 0 {
return true;
}
let Some(slot_ns) = slot_ns else {
return self.start_extra(depth, now_ns, vsync_period_ns);
};
self.started_slot_ns = Some(slot_ns);
!self.give_up_slot(depth, now_ns, vsync_period_ns)
}
pub(crate) fn slot_open(&self, now_ns: i64, vsync_ns: i64, vsync_period_ns: i64) -> bool {
let unpaced = self.stage.is_none_or(|stage| stage.level == Level::Unpaced);
unpaced || self.open_slot(now_ns, vsync_ns, vsync_period_ns).is_some()
}
fn open_slot(&self, now_ns: i64, vsync_ns: i64, vsync_period_ns: i64) -> Option<i64> {
if vsync_ns <= 0 || vsync_period_ns <= 0 {
return None;
}
let lead_ns = self.lead.lead_ns(vsync_period_ns);
let elapsed = (now_ns + SLOT_TOLERANCE_NS + lead_ns - vsync_ns).max(0);
let slot_ns = vsync_ns + elapsed / vsync_period_ns * vsync_period_ns;
let taken = self
.started_slot_ns
.is_some_and(|started| slot_ns - started < vsync_period_ns / 2);
(!taken).then_some(slot_ns)
}
fn start_extra(&mut self, depth: u32, now_ns: i64, vsync_period_ns: i64) -> bool {
let shallower = self.depths.last().is_some_and(|last| *last < depth);
let settled = self
.last_extra_ns
.is_none_or(|extra| now_ns - extra > vsync_period_ns * SHOWN_FRESH_VSYNCS);
if depth < 2 || !shallower || !settled || !self.shown_recently(now_ns, vsync_period_ns) {
return false;
}
self.last_extra_ns = Some(now_ns);
true
}
fn give_up_slot(&mut self, depth: u32, now_ns: i64, vsync_period_ns: i64) -> bool {
let skipped_recently = self
.last_skip_ns
.is_some_and(|skipped_at| now_ns - skipped_at < SKIP_SPACING_NS);
if skipped_recently
|| !self.queue_deeper_than(depth, HISTORY)
|| !self.shown_recently(now_ns, vsync_period_ns)
{
return false;
}
self.last_skip_ns = Some(now_ns);
self.depths.clear();
true
}
fn shown_recently(&self, now_ns: i64, vsync_period_ns: i64) -> bool {
self.last_shown_ns
.is_some_and(|shown| now_ns - shown <= vsync_period_ns * SHOWN_FRESH_VSYNCS)
}
}
#[cfg(test)]
#[path = "tests/frame_pacer_tests.rs"]
mod tests;