Schedule DRM frames against per-output presentation deadlines
This commit is contained in:
1 parent
9666e4ff6c
commit
a40b899b8c
10 files changed
+1168
-129
No files matched your search
@@ -0,0 +1,368 @@
|
||||
//! Fixed-refresh scheduling policy, independent of DRM and the event loop.
|
||||
use std::time::Duration;
|
||||
|
||||
const MIN_RENDER: Duration = Duration::from_millis(2);
|
||||
const TIMER_MARGIN: Duration = Duration::from_millis(1);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct FramePlan {
|
||||
pub render_at: Duration,
|
||||
pub present_at: Duration,
|
||||
pub requested_at: Duration,
|
||||
pub phase_known: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FrameScheduler {
|
||||
interval: Duration,
|
||||
last_presented: Option<Duration>,
|
||||
last_target: Option<Duration>,
|
||||
requested_at: Option<Duration>,
|
||||
scheduled: Option<FramePlan>,
|
||||
submitted: Option<FramePlan>,
|
||||
last_callback: Option<Duration>,
|
||||
render_estimate: Duration,
|
||||
render_variance: Duration,
|
||||
last_sample: Option<Duration>,
|
||||
extra_margin: Duration,
|
||||
pub missed_deadlines: u64,
|
||||
}
|
||||
|
||||
impl FrameScheduler {
|
||||
pub fn new(interval: Duration) -> Self {
|
||||
Self {
|
||||
interval: interval.max(Duration::from_micros(1)),
|
||||
last_presented: None,
|
||||
last_target: None,
|
||||
requested_at: None,
|
||||
scheduled: None,
|
||||
submitted: None,
|
||||
last_callback: None,
|
||||
render_estimate: MIN_RENDER,
|
||||
render_variance: Duration::ZERO,
|
||||
last_sample: None,
|
||||
extra_margin: Duration::ZERO,
|
||||
missed_deadlines: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, interval: Duration) {
|
||||
let missed = self.missed_deadlines;
|
||||
*self = Self::new(interval);
|
||||
self.missed_deadlines = missed;
|
||||
}
|
||||
|
||||
pub fn request(&mut self, now: Duration) {
|
||||
self.requested_at.get_or_insert(now);
|
||||
}
|
||||
|
||||
fn budget(&self, now: Duration) -> Duration {
|
||||
let idle = self
|
||||
.last_presented
|
||||
.is_none_or(|last| now.saturating_sub(last) >= self.interval * 100);
|
||||
if idle {
|
||||
// A cold GPU is less predictable. Start immediately on idle wakeup.
|
||||
self.interval
|
||||
} else {
|
||||
(self.render_estimate + self.render_variance * 2 + TIMER_MARGIN + self.extra_margin).min(self.interval)
|
||||
}
|
||||
}
|
||||
|
||||
fn phase_at_or_after(&self, time: Duration) -> Duration {
|
||||
let Some(last) = self.last_presented else {
|
||||
return time;
|
||||
};
|
||||
|
||||
let count = time
|
||||
.saturating_sub(last)
|
||||
.as_nanos()
|
||||
.div_ceil(self.interval.as_nanos())
|
||||
.max(1);
|
||||
last + self.interval.mul_f64(count as f64)
|
||||
}
|
||||
|
||||
pub fn plan(&mut self, now: Duration) -> Option<FramePlan> {
|
||||
if self.submitted.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(plan) = self.scheduled {
|
||||
return Some(plan);
|
||||
}
|
||||
|
||||
let requested_at = self.requested_at?;
|
||||
if self.last_presented.is_some_and(|last| last > now + self.interval) {
|
||||
self.last_presented = None;
|
||||
self.last_target = None;
|
||||
}
|
||||
|
||||
let budget = self.budget(now);
|
||||
let idle = self
|
||||
.last_presented
|
||||
.is_none_or(|last| now.saturating_sub(last) >= self.interval * 100);
|
||||
// With one in-flight GBM frame, submitting immediately can hit the
|
||||
// nearest vblank even when the conservative budget is larger than
|
||||
// the remaining time. Do not deliberately skip that presentation.
|
||||
let next = if self.last_presented.is_some() {
|
||||
now + Duration::from_nanos(1)
|
||||
} else {
|
||||
now + self.interval
|
||||
};
|
||||
|
||||
let earliest = next.max(self.last_target.map_or(next, |last| last + self.interval));
|
||||
let present_at = self.phase_at_or_after(earliest);
|
||||
let plan = FramePlan {
|
||||
render_at: if idle {
|
||||
now
|
||||
} else {
|
||||
present_at.saturating_sub(budget).max(now)
|
||||
},
|
||||
present_at,
|
||||
requested_at,
|
||||
phase_known: self.last_presented.is_some(),
|
||||
};
|
||||
|
||||
self.scheduled = Some(plan);
|
||||
Some(plan)
|
||||
}
|
||||
|
||||
pub fn begin(&mut self) -> Option<FramePlan> {
|
||||
let plan = self.scheduled.take()?;
|
||||
self.requested_at = None;
|
||||
self.last_target = Some(plan.present_at);
|
||||
Some(plan)
|
||||
}
|
||||
|
||||
pub fn rendered(&mut self, plan: FramePlan, now: Duration, cost: Duration, submitted: bool) {
|
||||
if submitted {
|
||||
self.submitted = Some(plan);
|
||||
let elapsed = self
|
||||
.last_sample
|
||||
.map_or(Duration::from_millis(500), |last| now.saturating_sub(last));
|
||||
self.last_sample = Some(now);
|
||||
let sample = cost.max(MIN_RENDER);
|
||||
let ratio = (elapsed.as_secs_f64() / 0.5).clamp(0.01, 1.0);
|
||||
let deviation = sample.saturating_sub(self.render_estimate);
|
||||
let variance_ratio = (elapsed.as_secs_f64() / 6.0).clamp(0.001, 0.1);
|
||||
self.render_variance = self.render_variance.mul_f64(1.0 - variance_ratio).max(deviation);
|
||||
self.render_estimate = self.render_estimate.mul_f64(1.0 - ratio) + sample.mul_f64(ratio);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn presented(&mut self, timestamp: Duration) -> Option<FramePlan> {
|
||||
// Regressive or implausibly early events must not shift the phase backward.
|
||||
if self.last_presented.is_some_and(|last| timestamp < last) {
|
||||
self.last_presented = None;
|
||||
self.last_target = None;
|
||||
return self.submitted.take();
|
||||
}
|
||||
|
||||
self.last_presented = Some(timestamp);
|
||||
let plan = self.submitted.take()?;
|
||||
// The first submission has only an estimated phase. Retiring it on
|
||||
// the actual refresh must not carry that estimate into the next frame.
|
||||
self.last_target = Some(timestamp);
|
||||
let late = timestamp.saturating_sub(plan.present_at);
|
||||
if plan.phase_known && late > self.interval / 2 {
|
||||
let missed = (late.as_nanos() + self.interval.as_nanos() / 2) / self.interval.as_nanos();
|
||||
self.missed_deadlines = self.missed_deadlines.saturating_add(missed as u64);
|
||||
// CPU submission timings may omit asynchronous GPU work. Increase
|
||||
// the margin promptly after a real miss; relax it slowly afterward.
|
||||
self.extra_margin = (self.extra_margin + TIMER_MARGIN).min(self.interval / 2);
|
||||
} else {
|
||||
self.extra_margin = self.extra_margin.mul_f64(0.98);
|
||||
}
|
||||
|
||||
Some(plan)
|
||||
}
|
||||
|
||||
pub fn retire_unknown(&mut self) {
|
||||
self.submitted = None;
|
||||
self.last_presented = None;
|
||||
self.last_target = None;
|
||||
}
|
||||
|
||||
pub fn forecast_time(&self, now: Duration, plan: FramePlan) -> Option<Duration> {
|
||||
// Before the first real page flip there is no reliable refresh phase.
|
||||
// A late timer still samples the next physical presentation, while
|
||||
// deadline metrics retain the original scheduled target.
|
||||
plan.phase_known.then(|| {
|
||||
plan.present_at
|
||||
.max(self.phase_at_or_after(now + Duration::from_nanos(1)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn callback_deadline(&self, now: Duration, target: Duration) -> Duration {
|
||||
let tolerance = Duration::from_micros(500).min(self.interval / 8);
|
||||
if target <= now
|
||||
&& self
|
||||
.last_callback
|
||||
.is_none_or(|last| last + self.interval <= now + tolerance)
|
||||
{
|
||||
return now;
|
||||
}
|
||||
|
||||
let earliest = self
|
||||
.last_callback
|
||||
.map_or(now, |last| last + self.interval)
|
||||
.max(target)
|
||||
.max(now);
|
||||
self.phase_at_or_after(earliest)
|
||||
}
|
||||
|
||||
pub fn callback_sent(&mut self, now: Duration) {
|
||||
// Record the refresh slot, not delayed event-loop dispatch time.
|
||||
// Otherwise a callback delivered a few microseconds late can make
|
||||
// the next refresh look too early and halve a client's frame rate.
|
||||
self.last_callback = Some(self.last_presented.map_or(now, |last| {
|
||||
let tolerance = Duration::from_micros(500).min(self.interval / 8);
|
||||
let slots = (now.saturating_sub(last) + tolerance).as_nanos() / self.interval.as_nanos();
|
||||
last + self.interval.mul_f64(slots as f64)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ms(value: u64) -> Duration {
|
||||
Duration::from_millis(value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requests_coalesce_without_postponing_the_frame_and_wait_for_retirement() {
|
||||
let mut scheduler = FrameScheduler::new(ms(10));
|
||||
scheduler.presented(ms(100));
|
||||
scheduler.request(ms(101));
|
||||
let plan = scheduler.plan(ms(101)).unwrap();
|
||||
assert_eq!((plan.render_at, plan.present_at), (ms(107), ms(110)));
|
||||
scheduler.request(ms(102));
|
||||
assert_eq!(scheduler.plan(ms(102)), Some(plan));
|
||||
assert_eq!(scheduler.begin(), Some(plan));
|
||||
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||
scheduler.request(ms(109));
|
||||
assert_eq!(scheduler.plan(ms(109)), None);
|
||||
scheduler.presented(ms(110));
|
||||
assert_eq!(scheduler.plan(ms(110)).unwrap().present_at, ms(120));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_wakeup_starts_early_and_is_not_a_missed_deadline() {
|
||||
let mut scheduler = FrameScheduler::new(ms(10));
|
||||
scheduler.presented(ms(100));
|
||||
scheduler.request(ms(10_100));
|
||||
let plan = scheduler.plan(ms(10_100)).unwrap();
|
||||
assert_eq!(plan.render_at, ms(10_100));
|
||||
scheduler.begin();
|
||||
scheduler.rendered(plan, ms(10_101), ms(1), true);
|
||||
scheduler.presented(ms(10_110));
|
||||
assert_eq!(scheduler.missed_deadlines, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn misses_measure_the_submitted_target_and_expand_the_budget() {
|
||||
let mut scheduler = FrameScheduler::new(ms(10));
|
||||
scheduler.presented(ms(100));
|
||||
scheduler.request(ms(101));
|
||||
let plan = scheduler.plan(ms(101)).unwrap();
|
||||
scheduler.begin();
|
||||
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||
assert_eq!(scheduler.presented(ms(130)), Some(plan));
|
||||
assert_eq!(scheduler.missed_deadlines, 2);
|
||||
assert_eq!(scheduler.budget(ms(131)), ms(4));
|
||||
assert_eq!(scheduler.presented(ms(129)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_retirement_aligns_the_next_frame_to_the_observed_refresh() {
|
||||
let mut scheduler = FrameScheduler::new(ms(10));
|
||||
scheduler.request(ms(100));
|
||||
let plan = scheduler.plan(ms(100)).unwrap();
|
||||
scheduler.begin();
|
||||
scheduler.rendered(plan, ms(102), ms(2), true);
|
||||
scheduler.presented(ms(103));
|
||||
|
||||
scheduler.request(ms(104));
|
||||
assert_eq!(scheduler.plan(ms(104)).unwrap().present_at, ms(113));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_damage_attempts_and_callbacks_do_not_spin_within_a_refresh() {
|
||||
let mut scheduler = FrameScheduler::new(ms(10));
|
||||
scheduler.presented(ms(100));
|
||||
scheduler.request(ms(101));
|
||||
let plan = scheduler.plan(ms(101)).unwrap();
|
||||
scheduler.begin();
|
||||
scheduler.rendered(plan, ms(108), ms(1), false);
|
||||
scheduler.callback_sent(ms(110));
|
||||
scheduler.request(ms(108));
|
||||
assert_eq!(scheduler.plan(ms(108)).unwrap().present_at, ms(120));
|
||||
assert_eq!(scheduler.callback_deadline(ms(111), ms(110)), ms(120));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outputs_keep_independent_phases_and_reset_cancels_obsolete_work() {
|
||||
let mut fast = FrameScheduler::new(ms(4));
|
||||
let mut slow = FrameScheduler::new(ms(16));
|
||||
for scheduler in [&mut fast, &mut slow] {
|
||||
scheduler.presented(ms(100));
|
||||
scheduler.request(ms(101));
|
||||
}
|
||||
|
||||
assert_eq!(fast.plan(ms(101)).unwrap().present_at, ms(104));
|
||||
assert_eq!(slow.plan(ms(101)).unwrap().present_at, ms(116));
|
||||
fast.reset(ms(8));
|
||||
assert_eq!(fast.plan(ms(102)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_dispatch_jitter_does_not_skip_a_refresh_or_duplicate_a_callback() {
|
||||
let mut scheduler = FrameScheduler::new(ms(10));
|
||||
scheduler.presented(ms(100));
|
||||
scheduler.callback_sent(ms(100) + Duration::from_micros(80));
|
||||
scheduler.presented(ms(110));
|
||||
let now = ms(110) + Duration::from_micros(10);
|
||||
assert_eq!(scheduler.callback_deadline(now, now), now);
|
||||
scheduler.callback_sent(now);
|
||||
assert_eq!(scheduler.callback_deadline(now, now), ms(120));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_retirement_and_regressive_timestamps_do_not_strand_pending_work() {
|
||||
let mut scheduler = FrameScheduler::new(ms(10));
|
||||
scheduler.presented(ms(100));
|
||||
scheduler.request(ms(101));
|
||||
let plan = scheduler.plan(ms(101)).unwrap();
|
||||
scheduler.begin();
|
||||
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||
scheduler.request(ms(109));
|
||||
assert_eq!(scheduler.presented(ms(90)), Some(plan));
|
||||
assert!(scheduler.plan(ms(110)).is_some());
|
||||
scheduler.retire_unknown();
|
||||
assert!(scheduler.plan(ms(111)).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cold_gpu_and_late_dispatch_do_not_predict_a_past_or_deliberately_skipped_frame() {
|
||||
let mut scheduler = FrameScheduler::new(ms(10));
|
||||
scheduler.presented(ms(100));
|
||||
scheduler.request(ms(10_101));
|
||||
let plan = scheduler.plan(ms(10_101)).unwrap();
|
||||
assert_eq!(plan.render_at, ms(10_101));
|
||||
assert_eq!(plan.present_at, ms(10_110));
|
||||
assert_eq!(scheduler.forecast_time(ms(10_111), plan), Some(ms(10_120)));
|
||||
|
||||
let mut unknown = FrameScheduler::new(ms(10));
|
||||
unknown.request(ms(100));
|
||||
let plan = unknown.plan(ms(100)).unwrap();
|
||||
assert_eq!(unknown.forecast_time(ms(101), plan), None);
|
||||
unknown.begin();
|
||||
unknown.rendered(plan, ms(102), ms(2), true);
|
||||
unknown.presented(ms(130));
|
||||
assert_eq!(
|
||||
unknown.missed_deadlines, 0,
|
||||
"unknown phase is not a measurable missed deadline"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user