use std::{collections::VecDeque, num::NonZeroU64, time::Duration}; use smithay::utils::{Clock, Monotonic, Time}; use tracing::{debug, error}; const FRAME_TIME_BUFFER: Duration = Duration::from_millis(1); const FRAME_TIME_WINDOW: usize = 3; pub struct Timings { refresh_interval_ns: Option, min_refresh_interval_ns: Option, vrr: bool, 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 frame_time(&self) -> Duration { Time::elapsed(&self.render_start, self.presentation_presented) } } impl Timings { const WINDOW_SIZE: usize = 360; pub fn new( refresh_interval: Option, min_interval: Option, vrr: bool, ) -> 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 }; Self { refresh_interval_ns, min_refresh_interval_ns, vrr, 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); while self.previous_frames.len() > Self::WINDOW_SIZE { self.previous_frames.pop_front(); } } } 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 { if self.previous_frames.is_empty() { return Duration::ZERO; } self.previous_frames .iter() .map(|f| f.render_time()) .sum::() / (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() } _ => Duration::ZERO, } .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_presentation_time(&self, clock: &Clock) -> bool { let now: Duration = clock.now().into(); let Some(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 refresh_interval_ns = refresh_interval_ns.get(); if now <= last_presentation_time { return false; } let next = last_presentation_time + Duration::from_nanos(refresh_interval_ns); now >= next } pub fn next_render_time(&self, clock: &Clock) -> Duration { let estimated_presentation_time = self.next_presentation_time(clock); if estimated_presentation_time.is_zero() { return Duration::ZERO; } let Some(avg_frametime) = self.avg_frametime(FRAME_TIME_WINDOW) else { return Duration::ZERO; }; estimated_presentation_time.saturating_sub(avg_frametime + FRAME_TIME_BUFFER) } }