From 83574451df5f7d6b77160caec07093a72599336e Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Wed, 30 Sep 2026 05:39:51 +0100 Subject: [PATCH] Fix DRM callback pacing and shared animation timing --- compositor/src/backends/direct.rs | 230 +++++++++++++++++++++++++----- compositor/src/input.rs | 30 ++-- compositor/src/presentation.rs | 53 +++++++ compositor/src/state.rs | 29 ++-- 4 files changed, 280 insertions(+), 62 deletions(-) diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 5e40e77..2ce1390 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -28,7 +28,10 @@ use smithay::{ desktop::{layer_map_for_output, utils::OutputPresentationFeedback}, output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel}, reexports::{ - calloop::EventLoop, + calloop::{ + EventLoop, RegistrationToken, + timer::{TimeoutAction, Timer}, + }, drm::control::{ Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property, }, @@ -58,6 +61,8 @@ pub struct DirectBackendState { input_devices: Vec, presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>, pub(crate) connected_outputs: Vec, + animation_timer: Option, + animation_active: bool, } impl DirectBackendState { @@ -185,6 +190,8 @@ struct DirectOutput { render_metrics: RenderMetrics, frame_pending: bool, power_off: bool, + callback_clock: crate::presentation::CallbackClock, + callback_timer: Option, lock_frame_pending: bool, } @@ -231,6 +238,8 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() input_devices: Vec::new(), presentation: HashMap::new(), connected_outputs: Vec::new(), + animation_timer: None, + animation_active: false, }); open_primary_device(event_loop, state, &primary_path)?; @@ -288,6 +297,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() } } } + state.reset_animation_clock(); tracing::info!("direct session activated"); state.lock_input_activity(); render_all(state); @@ -503,12 +513,6 @@ fn open_primary_device( Kind::Vsync | Kind::HwClock | Kind::HwCompletion, ); } - let focused = state - .direct_backend - .as_ref() - .and_then(|backend| backend.devices.get(&node)) - .and_then(|device| device.outputs.get(&crtc)) - .is_some_and(|output| state.is_focused_output(&output.output)); let lock_output = state .direct_backend .as_mut() @@ -532,11 +536,24 @@ fn open_primary_device( { output.frame_pending = false; } - if focused { - state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence); - } + state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence); tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip"); - render_output(state, node, crtc); + let active = state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.active); + if active { + let was_animating = state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.animation_active); + let animating = state.advance_animations(Instant::now()); + if animating || was_animating { + render_outputs(state, animating); + } else { + render_output(state, node, crtc); + } + } } } DrmEvent::Error(error) => { @@ -604,6 +621,22 @@ pub(crate) fn wake_locked_outputs(state: &mut Ferese) { } pub fn render_all(state: &mut Ferese) { + if state + .direct_backend + .as_ref() + .is_none_or(|backend| !backend.active) + { + return; + } + let animating = state.advance_animations(Instant::now()); + render_outputs(state, animating); +} + +fn render_outputs(state: &mut Ferese, animating: bool) { + if let Some(backend) = state.direct_backend.as_mut() { + backend.animation_active = animating && !state.session_lock.sleeping; + } + arm_animation_timer(state); let outputs = state .direct_backend .as_ref() @@ -694,6 +727,7 @@ fn lid_hides_panel(closed: bool, external_available: bool) -> bool { } fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { + schedule_frame_callbacks(state, node, crtc); if state.session_lock.active && state.session_lock.sleeping { sleep_locked_outputs(state); let asleep = state @@ -817,23 +851,13 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { Ok(true) })(); - let queued = match rendered { - Ok(true) => { - tracing::trace!(?node, ?crtc, "queued DRM frame"); - true - } - Ok(false) => false, - Err(error) => { - tracing::error!(?node, ?crtc, %error, "failed to render DRM frame"); - false - } - }; - let callback_output = output.output.clone(); + match rendered { + Ok(true) => tracing::trace!(?node, ?crtc, "queued DRM frame"), + Ok(false) => (), + Err(error) => tracing::error!(?node, ?crtc, %error, "failed to render DRM frame"), + } device.outputs.insert(crtc, output); restore_device(state, node, device); - if queued { - send_frame_callbacks(state, &callback_output); - } } fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) { @@ -1120,19 +1144,155 @@ fn refresh_duration(state: &Ferese, node: DrmNode, crtc: crtc::Handle) -> Durati .unwrap_or_else(|| Duration::from_millis(16)) } +fn animation_interval(state: &Ferese) -> Duration { + state + .direct_backend + .as_ref() + .into_iter() + .flat_map(|backend| backend.devices.values()) + .flat_map(|device| device.outputs.values()) + .filter_map(|output| output.output.current_mode()) + .filter(|mode| mode.refresh > 0) + .map(|mode| Duration::from_nanos(1_000_000_000_000 / mode.refresh as u64)) + .min() + .unwrap_or(Duration::from_millis(16)) +} + +fn arm_animation_timer(state: &mut Ferese) { + let needed = state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.animation_active && backend.animation_timer.is_none()); + if !needed { + return; + } + let deadline = state.animation_fallback_deadline(animation_interval(state)); + match state + .loop_handle + .insert_source(Timer::from_deadline(deadline), |_, _, state| { + let active = state.direct_backend.as_ref().is_some_and(|backend| { + backend.active && backend.animation_active && !state.session_lock.sleeping + }); + if active { + let deadline = state.animation_fallback_deadline(animation_interval(state)); + if Instant::now() < deadline { + return TimeoutAction::ToInstant(deadline); + } + render_all(state); + if state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.animation_active) + { + return TimeoutAction::ToInstant( + state.animation_fallback_deadline(animation_interval(state)), + ); + } + } + if let Some(backend) = state.direct_backend.as_mut() { + backend.animation_timer = None; + } + TimeoutAction::Drop + }) { + Ok(token) => state.direct_backend.as_mut().unwrap().animation_timer = Some(token), + Err(error) => tracing::warn!(%error, "could not schedule DRM animation fallback"), + } +} + +fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { + if state.session_lock.sleeping { + return; + } + let now = Instant::now(); + let interval = refresh_duration(state, node, crtc); + let Some(output) = state + .direct_backend + .as_ref() + .filter(|backend| backend.active) + .and_then(|backend| backend.devices.get(&node)) + .filter(|device| device.drm.is_active()) + .and_then(|device| device.outputs.get(&crtc)) + else { + return; + }; + if output.callback_timer.is_some() { + return; + } + let identity = output.output.clone(); + let deadline = output.callback_clock.deadline(now, interval); + if deadline <= now { + deliver_frame_callbacks(state, node, crtc, &identity); + return; + } + match state + .loop_handle + .insert_source(Timer::from_deadline(deadline), move |_, _, state| { + deliver_frame_callbacks(state, node, crtc, &identity); + TimeoutAction::Drop + }) { + Ok(token) => { + if let Some(output) = state + .direct_backend + .as_mut() + .and_then(|backend| backend.devices.get_mut(&node)) + .and_then(|device| device.outputs.get_mut(&crtc)) + { + output.callback_timer = Some(token); + } + } + Err(error) => tracing::warn!(%error, "could not schedule DRM frame callbacks"), + } +} + +fn deliver_frame_callbacks( + state: &mut Ferese, + node: DrmNode, + crtc: crtc::Handle, + identity: &Output, +) { + let Some(backend) = state.direct_backend.as_mut() else { + return; + }; + let Some(device) = backend.devices.get_mut(&node) else { + return; + }; + let Some(output) = device + .outputs + .get_mut(&crtc) + .filter(|output| &output.output == identity) + else { + return; + }; + output.callback_timer = None; + if !backend.active || !device.drm.is_active() || state.session_lock.sleeping { + return; + } + output.callback_clock.sent(Instant::now()); + send_frame_callbacks(state, identity); +} + fn send_frame_callbacks(state: &mut Ferese, output: &Output) { if state.session_lock.active { state.lock_frame_callbacks(output); return; } - state.space.elements().for_each(|window| { - window.send_frame( - output, - state.start_time.elapsed(), - Some(Duration::ZERO), - |_, _| Some(output.clone()), - ); - }); + state + .space + .elements() + .filter(|window| { + state + .window_ids + .get(*window) + .is_some_and(|id| state.window_belongs_to_output(*id, output)) + }) + .for_each(|window| { + window.send_frame( + output, + state.start_time.elapsed(), + Some(Duration::ZERO), + |_, _| Some(output.clone()), + ); + }); let layers = layer_map_for_output(output) .layers() .cloned() @@ -1373,6 +1533,8 @@ fn create_direct_output( render_metrics, frame_pending: false, power_off: false, + callback_clock: Default::default(), + callback_timer: None, lock_frame_pending: false, }) } diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 99358a4..ac5eeac 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -107,7 +107,7 @@ impl Ferese { self, &GestureSwipeBeginEvent { serial: SERIAL_COUNTER.next_serial(), - time: event.time() as u32, + time: event.time_msec(), fingers: event.fingers(), }, ); @@ -122,7 +122,7 @@ impl Ferese { .gesture_swipe_update( self, &GestureSwipeUpdateEvent { - time: event.time() as u32, + time: event.time_msec(), delta: event.delta(), }, ); @@ -158,7 +158,7 @@ impl Ferese { self, &GestureSwipeEndEvent { serial: SERIAL_COUNTER.next_serial(), - time: event.time() as u32, + time: event.time_msec(), cancelled: event.cancelled(), }, ); @@ -184,7 +184,7 @@ impl Ferese { keycode, state, SERIAL_COUNTER.next_serial(), - Event::time(&event) as u32, + event.time_msec(), |data, modifiers, keysym| { if state == KeyState::Released { data.portal_shortcuts.release(keycode, Event::time(&event)); @@ -316,7 +316,7 @@ impl Ferese { &MotionEvent { location: position, serial: SERIAL_COUNTER.next_serial(), - time: event.time() as u32, + time: event.time_msec(), }, ); pointer.frame(self); @@ -348,7 +348,7 @@ impl Ferese { delta: (delta.x / scale_x, delta.y / scale_y).into(), delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y) .into(), - utime: (event.time_msec() as u64).saturating_mul(1_000), + utime: event.time(), }, ); let requested = self.clamp_pointer_position(position + delta); @@ -360,7 +360,7 @@ impl Ferese { &MotionEvent { location, serial: SERIAL_COUNTER.next_serial(), - time: event.time() as u32, + time: event.time_msec(), }, ); pointer.frame(self); @@ -397,7 +397,7 @@ impl Ferese { popup, crate::dimming::DimAnimation::new(f64::from(opacity)), )); - pointer.unset_grab(self, serial, event.time() as u32); + pointer.unset_grab(self, serial, event.time_msec()); self.focus_window_at(pointer.current_location(), serial, true); crate::backends::direct::render_all(self); } @@ -411,7 +411,7 @@ impl Ferese { button: event.button_code(), state: event.state(), serial, - time: event.time() as u32, + time: event.time_msec(), }, ); pointer.frame(self); @@ -443,7 +443,7 @@ impl Ferese { button: event.button_code(), state: event.state(), serial, - time: event.time() as u32, + time: event.time_msec(), }, ); pointer.frame(self); @@ -460,7 +460,7 @@ impl Ferese { button: event.button_code(), state: event.state(), serial, - time: event.time() as u32, + time: event.time_msec(), }, ); pointer.frame(self); @@ -490,7 +490,7 @@ impl Ferese { ) { return; } - let mut frame = AxisFrame::new(event.time() as u32).source(source); + let mut frame = AxisFrame::new(event.time_msec()).source(source); if horizontal != 0.0 { frame = frame.value(Axis::Horizontal, horizontal); if let Some(value) = event.amount_v120(Axis::Horizontal) { @@ -534,7 +534,7 @@ impl Ferese { slot: event.slot(), location, serial, - time: event.time() as u32, + time: event.time_msec(), }, ); } @@ -550,7 +550,7 @@ impl Ferese { &TouchMotionEvent { slot: event.slot(), location, - time: event.time() as u32, + time: event.time_msec(), }, ); } @@ -561,7 +561,7 @@ impl Ferese { &UpEvent { slot: event.slot(), serial: SERIAL_COUNTER.next_serial(), - time: event.time() as u32, + time: event.time_msec(), }, ); } diff --git a/compositor/src/presentation.rs b/compositor/src/presentation.rs index c8cb8d5..ce8169f 100644 --- a/compositor/src/presentation.rs +++ b/compositor/src/presentation.rs @@ -17,6 +17,22 @@ use smithay::{ pub(crate) const HANDOFF: Duration = Duration::from_millis(80); pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024; +#[derive(Default)] +pub(crate) struct CallbackClock { + last_sent: Option, +} + +impl CallbackClock { + pub(crate) fn deadline(&self, now: Instant, interval: Duration) -> Instant { + self.last_sent + .map_or(now, |last| (last + interval).max(now)) + } + + pub(crate) fn sent(&mut self, now: Instant) { + self.last_sent = Some(now); + } +} + pub(crate) fn frame_delta(last: &mut Instant, now: Instant) -> Duration { let delta = now.saturating_duration_since(*last); *last = (*last).max(now); @@ -277,6 +293,43 @@ mod tests { )); } + #[test] + fn no_damage_requests_are_paced_without_buffer_submission() { + let start = Instant::now(); + let refresh = Duration::from_millis(16); + let mut clock = CallbackClock::default(); + assert_eq!(clock.deadline(start, refresh), start); + clock.sent(start); + for millisecond in 1..16 { + assert_eq!( + clock.deadline(start + Duration::from_millis(millisecond), refresh), + start + refresh + ); + } + let late = start + Duration::from_millis(50); + assert_eq!(clock.deadline(late, refresh), late); + clock.sent(late); + assert_eq!(clock.deadline(late, refresh), late + refresh); + } + + #[test] + fn independent_output_callback_clocks_do_not_sum_their_rates() { + let start = Instant::now(); + let mut slow = CallbackClock::default(); + let mut fast = CallbackClock::default(); + slow.sent(start); + fast.sent(start); + fast.sent(start + Duration::from_millis(8)); + assert_eq!( + slow.deadline(start + Duration::from_millis(8), Duration::from_millis(16)), + start + Duration::from_millis(16) + ); + assert_eq!( + fast.deadline(start + Duration::from_millis(8), Duration::from_millis(8)), + start + Duration::from_millis(16) + ); + } + #[test] fn mixed_refresh_outputs_do_not_double_the_animation_clock() { let start = Instant::now(); diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 60697bc..ee52397 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -788,10 +788,6 @@ impl Ferese { } } - pub(crate) fn is_focused_output(&self, output: &Output) -> bool { - self.output_ids.get(output).copied() == self.output_workspaces.focused_output() - } - fn activate_output_workspace(&mut self, output: OutputId, workspace: ferese_core::WorkspaceId) { if let Err(error) = self.output_workspaces.focus_output(output) { tracing::error!(%error, "failed to focus output"); @@ -1023,6 +1019,12 @@ impl Ferese { let CursorImageStatus::Surface(surface) = &self.cursor_status else { return; }; + let Some(pointer) = self.seat.get_pointer() else { + return; + }; + if self.space.output_under(pointer.current_location()).next() != Some(output) { + return; + } send_frames_surface_tree( surface, @@ -1949,18 +1951,19 @@ impl Ferese { time: DrmEventTime, sequence: u32, ) { - let delta = self - .direct_backend - .as_mut() - .and_then(|backend| backend.record_presentation(node, crtc, time, sequence)); - if delta.is_some() { - // All outputs animate the same scene. Summing each CRTC's frame - // interval makes motion run faster with two monitors (and uneven - // with mixed 60/120 Hz). Advance once by actual elapsed time. - self.advance_animations(Instant::now()); + if let Some(backend) = self.direct_backend.as_mut() { + backend.record_presentation(node, crtc, time, sequence); } } + pub(crate) fn animation_fallback_deadline(&self, interval: std::time::Duration) -> Instant { + self.last_animation_tick + interval.saturating_mul(2) + } + + pub(crate) fn reset_animation_clock(&mut self) { + self.last_animation_tick = Instant::now(); + } + fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool { let delta = delta.mul_f64(self.animation_speed); let visible_workspaces = self