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, min_refresh_interval_ns: Option, vrr: bool, vendor: Option, pub pending_frame: Option, pub previous_frames: VecDeque, } #[derive(Debug)] pub struct PendingFrame { render_start: Time, render_duration_elements: Option, render_duration_draw: Option, presentation_submitted: Option>, } #[derive(Debug)] pub struct Frame { pub render_start: Time, pub render_duration_elements: Duration, pub render_duration_draw: Duration, pub presentation_submitted: Time, pub presentation_presented: Time, } 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, min_interval: Option, 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) { 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) { 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) { 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) { 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) { 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) { if let Some(frame) = self.pending_frame.as_mut() { frame.presentation_submitted = Some(clock.now()); } } pub fn presented(&mut self, value: Time) { 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 { 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 { 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) -> 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 = 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) -> 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 = 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) -> 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) } }