From 9d522ed51e75d1253793f9f5ec42b8faf36e47e7 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Thu, 20 Nov 2025 12:55:45 +0300 Subject: tty: Throttle VBlanks on displays running faster than expected Co-authored-by: Christian Meissl Co-authored-by: Scott McKendry <39483124+scottmckendry@users.noreply.github.com> --- src/utils/mod.rs | 1 + src/utils/vblank_throttle.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/utils/vblank_throttle.rs (limited to 'src/utils') diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9a32e60f..fdde46f8 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -38,6 +38,7 @@ pub mod scale; pub mod signals; pub mod spawning; pub mod transaction; +pub mod vblank_throttle; pub mod watcher; pub mod xwayland; diff --git a/src/utils/vblank_throttle.rs b/src/utils/vblank_throttle.rs new file mode 100644 index 00000000..ecf2053f --- /dev/null +++ b/src/utils/vblank_throttle.rs @@ -0,0 +1,71 @@ +//! VBlank throttling. +//! +//! Some buggy drivers deliver VBlanks way earlier than necessary. This helper throttles the VBlank +//! in such cases to avoid tearing and to get more consistent timings. + +use std::time::Duration; + +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{LoopHandle, RegistrationToken}; + +use crate::niri::State; + +#[derive(Debug)] +pub struct VBlankThrottle { + event_loop: LoopHandle<'static, State>, + last_vblank_timestamp: Option, + throttle_timer_token: Option, + printed_warning: bool, + output_name: String, +} + +impl VBlankThrottle { + pub fn new(event_loop: LoopHandle<'static, State>, output_name: String) -> Self { + Self { + event_loop, + last_vblank_timestamp: None, + throttle_timer_token: None, + printed_warning: false, + output_name, + } + } + + pub fn throttle( + &mut self, + refresh_interval: Option, + timestamp: Duration, + mut call_vblank: impl FnMut(&mut State) + 'static, + ) -> bool { + if let Some(token) = self.throttle_timer_token.take() { + self.event_loop.remove(token); + } + + if let Some((last, refresh)) = Option::zip(self.last_vblank_timestamp, refresh_interval) { + let passed = timestamp.saturating_sub(last); + if passed < refresh / 2 { + if !self.printed_warning { + self.printed_warning = true; + warn!( + "output {} running faster than expected, throttling vblanks: \ + expected refresh {refresh:?}, got vblank after {passed:?}", + self.output_name + ); + } + + let remaining = refresh - passed; + let token = self + .event_loop + .insert_source(Timer::from_duration(remaining), move |_, _, state| { + call_vblank(state); + TimeoutAction::Drop + }) + .unwrap(); + self.throttle_timer_token = Some(token); + return true; + } + } + + self.last_vblank_timestamp = Some(timestamp); + false + } +} -- cgit