diff --git a/src/backends/metal/present.rs b/src/backends/metal/present.rs index d7e0bfa6..be9f266d 100644 --- a/src/backends/metal/present.rs +++ b/src/backends/metal/present.rs @@ -12,6 +12,7 @@ use { SyncFile, }, theme::Color, + time::Time, tree::OutputNode, utils::{errorfmt::ErrorFmt, oserror::OsError, transform_ext::TransformExt}, video::{ @@ -22,7 +23,11 @@ use { }, }, }, - std::rc::{Rc, Weak}, + std::{ + env, + rc::{Rc, Weak}, + sync::LazyLock, + }, uapi::c, }; @@ -79,19 +84,63 @@ enum CursorProgramming { }, } +pub const DEFAULT_PRE_COMMIT_MARGIN: u64 = 16_000_000; // 16ms +pub const MIN_POST_COMMIT_MARGIN: u64 = 1_500_000; // 1.5ms +pub const MAX_POST_COMMIT_MARGIN: u64 = 16_000_000; // 16ms +pub const DEFAULT_POST_COMMIT_MARGIN: u64 = MIN_POST_COMMIT_MARGIN; +pub const POST_COMMIT_MARGIN_DELTA: u64 = 500_000; // 500us + +static NO_FRAME_SCHEDULING: LazyLock = LazyLock::new(|| { + let res = env::var("JAY_NO_FRAME_SCHEDULING").ok().as_deref() == Some("1"); + if res { + log::warn!("Frame scheduling is disabled."); + } + res +}); + impl MetalConnector { pub fn schedule_present(&self) { self.present_trigger.trigger(); } pub async fn present_loop(self: Rc) { + let mut cur_sec = 0; + let mut max = 0; loop { self.present_trigger.triggered().await; + if !self.can_present.get() { + continue; + } + let mut expected_sequence = self.sequence.get() + 1; + let mut start = Time::now_unchecked(); + let use_frame_scheduling = !self.try_async_flip() && !*NO_FRAME_SCHEDULING; + if use_frame_scheduling { + let margin = self.pre_commit_margin.get() + self.post_commit_margin.get(); + let next_present = self.next_flip_nsec.get().saturating_sub(margin); + if start.nsec() < next_present { + self.state.ring.timeout(next_present).await.unwrap(); + start = Time::now_unchecked(); + } else { + expected_sequence += 1; + } + } if let Err(e) = self.present_once().await { log::error!("Could not present: {}", ErrorFmt(e)); continue; } + if use_frame_scheduling { + self.expected_sequence.set(Some(expected_sequence)); + } self.state.set_backend_idle(false); + let duration = start.elapsed(); + max = max.max(duration.as_nanos() as _); + if start.0.tv_sec != cur_sec { + cur_sec = start.0.tv_sec; + self.pre_commit_margin_decay.add(max); + self.pre_commit_margin + .set(self.pre_commit_margin_decay.get()); + max = 0; + } } } @@ -244,6 +293,10 @@ impl MetalConnector { } } + fn try_async_flip(&self) -> bool { + self.tearing_requested.get() && self.dev.supports_async_commit + } + fn program_connector( &self, version: u64, @@ -253,7 +306,7 @@ impl MetalConnector { new_fb: Option<&PresentFb>, ) -> Result<(), MetalError> { let mut changes = self.master.change(); - let mut try_async_flip = self.tearing_requested.get() && self.dev.supports_async_commit; + let mut try_async_flip = self.try_async_flip(); macro_rules! change { ($c:expr, $prop:expr, $new:expr) => {{ if $prop.value.get() != $new { diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index 627c497a..3c80bf87 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -8,7 +8,11 @@ use { HardwareCursorUpdate, Mode, MonitorInfo, }, backends::metal::{ - present::{DirectScanoutCache, PresentFb}, + present::{ + DirectScanoutCache, PresentFb, DEFAULT_POST_COMMIT_MARGIN, + DEFAULT_PRE_COMMIT_MARGIN, MAX_POST_COMMIT_MARGIN, MIN_POST_COMMIT_MARGIN, + POST_COMMIT_MARGIN_DELTA, + }, MetalBackend, MetalError, }, drm_feedback::DrmFeedback, @@ -27,8 +31,8 @@ use { udev::UdevDevice, utils::{ asyncevent::AsyncEvent, bitflags::BitflagsExt, cell_ext::CellExt, clonecell::CloneCell, - copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell, on_change::OnChange, - opaque_cell::OpaqueCell, oserror::OsError, + copyhashmap::CopyHashMap, errorfmt::ErrorFmt, geometric_decay::GeometricDecay, + numcell::NumCell, on_change::OnChange, opaque_cell::OpaqueCell, oserror::OsError, }, video::{ dmabuf::DmaBufId, @@ -464,6 +468,13 @@ pub struct MetalConnector { pub version: NumCell, pub sequence: Cell, + pub expected_sequence: Cell>, + pub pre_commit_margin: Cell, + pub pre_commit_margin_decay: GeometricDecay, + pub post_commit_margin: Cell, + pub post_commit_margin_decay: GeometricDecay, + pub vblank_miss_sec: Cell, + pub vblank_miss_this_sec: NumCell, } impl Debug for MetalConnector { @@ -1055,6 +1066,13 @@ fn create_connector( try_switch_format: Cell::new(false), version: Default::default(), sequence: Default::default(), + expected_sequence: Default::default(), + pre_commit_margin_decay: GeometricDecay::new(0.5, DEFAULT_PRE_COMMIT_MARGIN), + pre_commit_margin: Cell::new(DEFAULT_PRE_COMMIT_MARGIN), + post_commit_margin_decay: GeometricDecay::new(0.1, DEFAULT_POST_COMMIT_MARGIN), + post_commit_margin: Cell::new(DEFAULT_POST_COMMIT_MARGIN), + vblank_miss_sec: Cell::new(0), + vblank_miss_this_sec: Default::default(), }); let futures = ConnectorFutures { _present: backend @@ -1924,6 +1942,30 @@ impl MetalBackend { if let Some(fb) = connector.next_framebuffer.take() { *connector.active_framebuffer.borrow_mut() = Some(fb); } + if let Some(expected) = connector.expected_sequence.take() { + if connector.vblank_miss_sec.replace(tv_sec) != tv_sec { + let n_missed = connector.vblank_miss_this_sec.replace(0); + if n_missed > 0 { + log::debug!("{}: Missed {n_missed} page flips", connector.kernel_id()); + let new_margin = (connector.post_commit_margin.get() + + POST_COMMIT_MARGIN_DELTA) + .min(MAX_POST_COMMIT_MARGIN); + connector.post_commit_margin_decay.reset(new_margin); + connector.post_commit_margin.set(new_margin); + } else { + connector + .post_commit_margin_decay + .add(MIN_POST_COMMIT_MARGIN); + connector + .post_commit_margin + .set(connector.post_commit_margin_decay.get()); + } + } + let actual = connector.sequence.get(); + if expected < actual { + connector.vblank_miss_this_sec.fetch_add(1); + } + } if connector.has_damage.is_not_zero() || connector.cursor_damage.get() || connector.cursor_changed.get() diff --git a/src/time.rs b/src/time.rs index 909d052d..aaddcf04 100644 --- a/src/time.rs +++ b/src/time.rs @@ -58,6 +58,11 @@ impl Time { let nsec = self.0.tv_nsec as u64 / 1_000_000; sec + nsec } + + pub fn elapsed(self) -> Duration { + let now = Self::now_unchecked(); + now - self + } } impl Eq for Time {} diff --git a/src/utils/geometric_decay.rs b/src/utils/geometric_decay.rs index 83466724..8455cd93 100644 --- a/src/utils/geometric_decay.rs +++ b/src/utils/geometric_decay.rs @@ -7,7 +7,6 @@ pub struct GeometricDecay { } impl GeometricDecay { - #[expect(dead_code)] pub fn new(mut p1: f64, reset: u64) -> Self { if p1.is_nan() || p1 < 0.01 { p1 = 0.01; @@ -23,17 +22,14 @@ impl GeometricDecay { } } - #[expect(dead_code)] pub fn reset(&self, v: u64) { self.v.set(v as f64 / self.p1); } - #[expect(dead_code)] pub fn get(&self) -> u64 { (self.p1 * self.v.get()) as u64 } - #[expect(dead_code)] pub fn add(&self, n: u64) { let v = n as f64 + self.p2 * self.v.get(); self.v.set(v);