cosmic-comp/src/backend/kms/surface/timings.rs
2025-04-28 18:03:09 +02:00

394 lines
12 KiB
Rust

use std::{collections::VecDeque, num::NonZeroU64, time::Duration};
use smithay::{
backend::drm::DrmNode,
utils::{Clock, Monotonic, Time},
};
use tracing::{debug, error};
const BASE_SAFETY_MARGIN: Duration = Duration::from_millis(3);
const SAMPLE_TIME_WINDOW: usize = 5;
pub struct Timings {
refresh_interval_ns: Option<NonZeroU64>,
min_refresh_interval_ns: Option<NonZeroU64>,
vrr: bool,
vendor: Option<u32>,
pub pending_frame: Option<PendingFrame>,
pub previous_frames: VecDeque<Frame>,
}
#[derive(Debug)]
pub struct PendingFrame {
render_start: Time<Monotonic>,
render_duration_elements: Option<Duration>,
render_duration_draw: Option<Duration>,
presentation_submitted: Option<Time<Monotonic>>,
}
#[derive(Debug)]
pub struct Frame {
pub render_start: Time<Monotonic>,
pub render_duration_elements: Duration,
pub render_duration_draw: Duration,
pub presentation_submitted: Time<Monotonic>,
pub presentation_presented: Time<Monotonic>,
}
impl Frame {
fn render_time(&self) -> Duration {
self.render_duration_elements + self.render_duration_draw
}
fn submit_time(&self) -> Duration {
Time::elapsed(&self.render_start, self.presentation_submitted)
}
fn frame_time(&self) -> Duration {
Time::elapsed(&self.render_start, self.presentation_presented)
}
}
impl Timings {
const CLEANUP: usize = 360;
pub fn new(
refresh_interval: Option<Duration>,
min_interval: Option<Duration>,
vrr: bool,
node: DrmNode,
) -> Self {
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
assert_eq!(interval.as_secs(), 0);
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
} else {
None
};
let min_refresh_interval_ns = if let Some(interval) = &min_interval {
assert_eq!(interval.as_secs(), 0);
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
} else {
None
};
let vendor = if let Ok(vendor) = std::fs::read_to_string(format!(
"/sys/class/drm/renderD{}/device/vendor",
node.minor()
)) {
u32::from_str_radix(&vendor.trim()[2..], 16).ok()
} else {
None
};
Self {
refresh_interval_ns,
min_refresh_interval_ns,
vrr,
vendor,
pending_frame: None,
previous_frames: VecDeque::new(),
}
}
pub fn refresh_interval(&self) -> Duration {
match self.refresh_interval_ns {
Some(ns) => Duration::from_nanos(ns.get()),
None => Duration::ZERO,
}
}
pub fn set_refresh_interval(&mut self, interval: Option<Duration>) {
self.refresh_interval_ns = interval
.map(|duration| duration.subsec_nanos() as u64)
.and_then(NonZeroU64::new);
self.previous_frames.clear();
}
pub fn set_min_refresh_interval(&mut self, min_interval: Option<Duration>) {
self.min_refresh_interval_ns = min_interval
.map(|duration| duration.subsec_nanos() as u64)
.and_then(NonZeroU64::new);
}
pub fn set_vrr(&mut self, vrr: bool) {
self.vrr = vrr;
}
pub fn vrr(&self) -> bool {
self.vrr
}
pub fn start_render(&mut self, clock: &Clock<Monotonic>) {
self.pending_frame = Some(PendingFrame {
render_start: clock.now(),
render_duration_elements: None,
render_duration_draw: None,
presentation_submitted: None,
});
}
pub fn elements_done(&mut self, clock: &Clock<Monotonic>) {
if let Some(frame) = self.pending_frame.as_mut() {
frame.render_duration_elements = Some(Time::elapsed(&frame.render_start, clock.now()));
}
}
pub fn draw_done(&mut self, clock: &Clock<Monotonic>) {
if let Some(frame) = self.pending_frame.as_mut() {
frame.render_duration_draw = Some(
Time::elapsed(&frame.render_start, clock.now())
- frame
.render_duration_elements
.clone()
.unwrap_or(Duration::ZERO),
);
}
}
pub fn submitted_for_presentation(&mut self, clock: &Clock<Monotonic>) {
if let Some(frame) = self.pending_frame.as_mut() {
frame.presentation_submitted = Some(clock.now());
}
}
pub fn presented(&mut self, value: Time<Monotonic>) {
if let Some(frame) = self.pending_frame.take() {
let new_frame = Frame {
render_start: frame.render_start,
render_duration_elements: frame.render_duration_elements.unwrap_or_default(),
render_duration_draw: frame.render_duration_draw.unwrap_or_default(),
presentation_submitted: frame.presentation_submitted.unwrap(),
presentation_presented: value,
};
if new_frame.render_start > new_frame.presentation_submitted {
debug!(
"frame time overflowed: {}",
new_frame.frame_time().as_millis()
);
}
self.previous_frames.push_back(new_frame);
if let Some(overflow) = self.previous_frames.len().checked_sub(Self::CLEANUP * 2) {
self.previous_frames = self.previous_frames.split_off(overflow + Self::CLEANUP);
}
}
}
pub fn discard_current_frame(&mut self) {
let _ = self.pending_frame.take();
}
pub fn max_rendertime(&self) -> Duration {
self.previous_frames
.iter()
.map(|f| f.render_time())
.max()
.unwrap_or(Duration::ZERO)
}
pub fn min_rendertime(&self) -> Duration {
self.previous_frames
.iter()
.map(|f| f.render_time())
.min()
.unwrap_or(Duration::ZERO)
}
pub fn max_frametime(&self, window: usize) -> Duration {
self.previous_frames
.iter()
.rev()
.take(window)
.map(|f| f.frame_time())
.max()
.unwrap_or(Duration::ZERO)
}
pub fn min_frametime(&self, window: usize) -> Duration {
self.previous_frames
.iter()
.rev()
.take(window)
.map(|f| f.frame_time())
.min()
.unwrap_or(Duration::ZERO)
}
pub fn avg_rendertime(&self) -> Duration {
let Some(sum_rendertime) = self
.previous_frames
.iter()
.map(|f| f.render_time())
.try_fold(Duration::ZERO, |acc, x| acc.checked_add(x))
else {
return Duration::ZERO;
};
sum_rendertime
.checked_div(self.previous_frames.len() as u32)
.unwrap_or(Duration::ZERO)
}
pub fn avg_submittime(&self, window: usize) -> Option<Duration> {
if self.previous_frames.len() < window || window == 0 {
return None;
}
Some(
self.previous_frames
.iter()
.rev()
.take(window)
.map(|f| f.submit_time())
.try_fold(Duration::ZERO, |acc, x| acc.checked_add(x))?
/ (window.min(self.previous_frames.len()) as u32),
)
}
pub fn avg_frametime(&self, window: usize) -> Option<Duration> {
if self.previous_frames.len() < window || window == 0 {
return None;
}
Some(
self.previous_frames
.iter()
.rev()
.take(window)
.map(|f| f.frame_time())
.try_fold(Duration::ZERO, |acc, x| acc.checked_add(x))?
/ (window.min(self.previous_frames.len()) as u32),
)
}
pub fn avg_fps(&self) -> f64 {
if self.previous_frames.is_empty() {
return 0.0;
}
let secs = match (self.previous_frames.front(), self.previous_frames.back()) {
(Some(Frame { render_start, .. }), Some(end_frame)) => {
Time::elapsed(render_start, end_frame.render_start.clone()) + end_frame.frame_time()
}
_ => {
return 0.0;
}
}
.as_secs_f64();
1.0 / (secs / self.previous_frames.len() as f64)
}
pub fn next_presentation_time(&self, clock: &Clock<Monotonic>) -> Duration {
let mut now = clock.now().into();
let Some(refresh_interval_ns) = self.refresh_interval_ns else {
return Duration::ZERO;
};
let Some(last_presentation_time): Option<Duration> = self
.previous_frames
.back()
.map(|frame| frame.presentation_presented.into())
else {
return Duration::ZERO;
};
let refresh_interval_ns = refresh_interval_ns.get();
if now <= last_presentation_time {
// Got an early VBlank.
let orig_now = now;
now += Duration::from_nanos(refresh_interval_ns);
if now < last_presentation_time {
// Not sure when this can happen.
error!(
now = ?orig_now,
?last_presentation_time,
"got a 2+ early VBlank, {:?} until presentation",
last_presentation_time - now,
);
now = last_presentation_time + Duration::from_nanos(refresh_interval_ns);
}
}
let since_last = now - last_presentation_time;
let since_last_ns =
since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos());
let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns;
// If VRR is enabled and more than one frame passed since last presentation, assume that we
// can present immediately.
if self.vrr && to_next_ns > refresh_interval_ns {
Duration::ZERO
} else {
last_presentation_time + Duration::from_nanos(to_next_ns) - now
}
}
pub fn past_min_render_time(&self, clock: &Clock<Monotonic>) -> bool {
let now: Duration = clock.now().into();
let Some(min_refresh_interval_ns) = self.min_refresh_interval_ns else {
return true;
};
let Some(last_presentation_time): Option<Duration> = self
.previous_frames
.back()
.map(|frame| frame.presentation_presented.into())
else {
return true;
};
let min_refresh_interval_ns = min_refresh_interval_ns.get();
if now <= last_presentation_time {
return false;
}
const MIN_MARGIN: Duration = Duration::from_millis(3);
let baseline = if let Some(refresh_interval_ns) = self.refresh_interval_ns {
MIN_MARGIN.max(Duration::from_nanos(refresh_interval_ns.get() / 2))
} else {
MIN_MARGIN
};
let next_presentation_time =
last_presentation_time + Duration::from_nanos(min_refresh_interval_ns);
let deadline = next_presentation_time.saturating_sub(
if let Some(avg_submittime) = self.avg_submittime(SAMPLE_TIME_WINDOW) {
avg_submittime
} else {
baseline
} + BASE_SAFETY_MARGIN,
);
now >= deadline
}
pub fn next_render_time(&self, clock: &Clock<Monotonic>) -> Duration {
let Some(refresh_interval) = self.refresh_interval_ns else {
return Duration::ZERO; // we don't know what to expect, so render immediately.
};
const MIN_MARGIN: Duration = Duration::from_millis(3);
let baseline = MIN_MARGIN.max(Duration::from_nanos(refresh_interval.get() / 2));
let estimated_presentation_time = self.next_presentation_time(clock);
if estimated_presentation_time.is_zero() {
return Duration::ZERO;
}
// HACK: Nvidia returns `page_flip`/`commit` early, so we have no information to optimize latency on submission.
if self.vendor == Some(0x10de) {
return Duration::ZERO;
}
let Some(avg_submittime) = self.avg_submittime(SAMPLE_TIME_WINDOW) else {
return estimated_presentation_time.saturating_sub(baseline + BASE_SAFETY_MARGIN);
};
let margin = avg_submittime + BASE_SAFETY_MARGIN;
estimated_presentation_time.saturating_sub(margin)
}
}