Fix DRM callback pacing and shared animation timing

This commit is contained in:
Samuel Onoja committed 2026-09-30 05:39:51 +01:00
1 parent 46d33751c6
commit 83574451df
4 files changed
+280 -62

No files matched your search

+196 -34
View File
@@ -28,7 +28,10 @@ use smithay::{
desktop::{layer_map_for_output, utils::OutputPresentationFeedback}, desktop::{layer_map_for_output, utils::OutputPresentationFeedback},
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel}, output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
reexports::{ reexports::{
calloop::EventLoop, calloop::{
EventLoop, RegistrationToken,
timer::{TimeoutAction, Timer},
},
drm::control::{ drm::control::{
Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property, Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property,
}, },
@@ -58,6 +61,8 @@ pub struct DirectBackendState {
input_devices: Vec<LibinputDevice>, input_devices: Vec<LibinputDevice>,
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>, presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>, pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
animation_timer: Option<RegistrationToken>,
animation_active: bool,
} }
impl DirectBackendState { impl DirectBackendState {
@@ -185,6 +190,8 @@ struct DirectOutput {
render_metrics: RenderMetrics, render_metrics: RenderMetrics,
frame_pending: bool, frame_pending: bool,
power_off: bool, power_off: bool,
callback_clock: crate::presentation::CallbackClock,
callback_timer: Option<RegistrationToken>,
lock_frame_pending: bool, lock_frame_pending: bool,
} }
@@ -231,6 +238,8 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
input_devices: Vec::new(), input_devices: Vec::new(),
presentation: HashMap::new(), presentation: HashMap::new(),
connected_outputs: Vec::new(), connected_outputs: Vec::new(),
animation_timer: None,
animation_active: false,
}); });
open_primary_device(event_loop, state, &primary_path)?; open_primary_device(event_loop, state, &primary_path)?;
@@ -288,6 +297,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
} }
} }
} }
state.reset_animation_clock();
tracing::info!("direct session activated"); tracing::info!("direct session activated");
state.lock_input_activity(); state.lock_input_activity();
render_all(state); render_all(state);
@@ -503,12 +513,6 @@ fn open_primary_device(
Kind::Vsync | Kind::HwClock | Kind::HwCompletion, 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 let lock_output = state
.direct_backend .direct_backend
.as_mut() .as_mut()
@@ -532,11 +536,24 @@ fn open_primary_device(
{ {
output.frame_pending = false; 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"); 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) => { DrmEvent::Error(error) => {
@@ -604,6 +621,22 @@ pub(crate) fn wake_locked_outputs(state: &mut Ferese) {
} }
pub fn render_all(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 let outputs = state
.direct_backend .direct_backend
.as_ref() .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) { 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 { if state.session_lock.active && state.session_lock.sleeping {
sleep_locked_outputs(state); sleep_locked_outputs(state);
let asleep = state let asleep = state
@@ -817,23 +851,13 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
Ok(true) Ok(true)
})(); })();
let queued = match rendered { match rendered {
Ok(true) => { Ok(true) => tracing::trace!(?node, ?crtc, "queued DRM frame"),
tracing::trace!(?node, ?crtc, "queued DRM frame"); Ok(false) => (),
true Err(error) => tracing::error!(?node, ?crtc, %error, "failed to render DRM frame"),
} }
Ok(false) => false,
Err(error) => {
tracing::error!(?node, ?crtc, %error, "failed to render DRM frame");
false
}
};
let callback_output = output.output.clone();
device.outputs.insert(crtc, output); device.outputs.insert(crtc, output);
restore_device(state, node, device); restore_device(state, node, device);
if queued {
send_frame_callbacks(state, &callback_output);
}
} }
fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) { 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)) .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) { fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
if state.session_lock.active { if state.session_lock.active {
state.lock_frame_callbacks(output); state.lock_frame_callbacks(output);
return; return;
} }
state.space.elements().for_each(|window| { state
window.send_frame( .space
output, .elements()
state.start_time.elapsed(), .filter(|window| {
Some(Duration::ZERO), state
|_, _| Some(output.clone()), .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) let layers = layer_map_for_output(output)
.layers() .layers()
.cloned() .cloned()
@@ -1373,6 +1533,8 @@ fn create_direct_output(
render_metrics, render_metrics,
frame_pending: false, frame_pending: false,
power_off: false, power_off: false,
callback_clock: Default::default(),
callback_timer: None,
lock_frame_pending: false, lock_frame_pending: false,
}) })
} }
+15 -15
View File
@@ -107,7 +107,7 @@ impl Ferese {
self, self,
&GestureSwipeBeginEvent { &GestureSwipeBeginEvent {
serial: SERIAL_COUNTER.next_serial(), serial: SERIAL_COUNTER.next_serial(),
time: event.time() as u32, time: event.time_msec(),
fingers: event.fingers(), fingers: event.fingers(),
}, },
); );
@@ -122,7 +122,7 @@ impl Ferese {
.gesture_swipe_update( .gesture_swipe_update(
self, self,
&GestureSwipeUpdateEvent { &GestureSwipeUpdateEvent {
time: event.time() as u32, time: event.time_msec(),
delta: event.delta(), delta: event.delta(),
}, },
); );
@@ -158,7 +158,7 @@ impl Ferese {
self, self,
&GestureSwipeEndEvent { &GestureSwipeEndEvent {
serial: SERIAL_COUNTER.next_serial(), serial: SERIAL_COUNTER.next_serial(),
time: event.time() as u32, time: event.time_msec(),
cancelled: event.cancelled(), cancelled: event.cancelled(),
}, },
); );
@@ -184,7 +184,7 @@ impl Ferese {
keycode, keycode,
state, state,
SERIAL_COUNTER.next_serial(), SERIAL_COUNTER.next_serial(),
Event::time(&event) as u32, event.time_msec(),
|data, modifiers, keysym| { |data, modifiers, keysym| {
if state == KeyState::Released { if state == KeyState::Released {
data.portal_shortcuts.release(keycode, Event::time(&event)); data.portal_shortcuts.release(keycode, Event::time(&event));
@@ -316,7 +316,7 @@ impl Ferese {
&MotionEvent { &MotionEvent {
location: position, location: position,
serial: SERIAL_COUNTER.next_serial(), serial: SERIAL_COUNTER.next_serial(),
time: event.time() as u32, time: event.time_msec(),
}, },
); );
pointer.frame(self); pointer.frame(self);
@@ -348,7 +348,7 @@ impl Ferese {
delta: (delta.x / scale_x, delta.y / scale_y).into(), delta: (delta.x / scale_x, delta.y / scale_y).into(),
delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y) delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y)
.into(), .into(),
utime: (event.time_msec() as u64).saturating_mul(1_000), utime: event.time(),
}, },
); );
let requested = self.clamp_pointer_position(position + delta); let requested = self.clamp_pointer_position(position + delta);
@@ -360,7 +360,7 @@ impl Ferese {
&MotionEvent { &MotionEvent {
location, location,
serial: SERIAL_COUNTER.next_serial(), serial: SERIAL_COUNTER.next_serial(),
time: event.time() as u32, time: event.time_msec(),
}, },
); );
pointer.frame(self); pointer.frame(self);
@@ -397,7 +397,7 @@ impl Ferese {
popup, popup,
crate::dimming::DimAnimation::new(f64::from(opacity)), 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); self.focus_window_at(pointer.current_location(), serial, true);
crate::backends::direct::render_all(self); crate::backends::direct::render_all(self);
} }
@@ -411,7 +411,7 @@ impl Ferese {
button: event.button_code(), button: event.button_code(),
state: event.state(), state: event.state(),
serial, serial,
time: event.time() as u32, time: event.time_msec(),
}, },
); );
pointer.frame(self); pointer.frame(self);
@@ -443,7 +443,7 @@ impl Ferese {
button: event.button_code(), button: event.button_code(),
state: event.state(), state: event.state(),
serial, serial,
time: event.time() as u32, time: event.time_msec(),
}, },
); );
pointer.frame(self); pointer.frame(self);
@@ -460,7 +460,7 @@ impl Ferese {
button: event.button_code(), button: event.button_code(),
state: event.state(), state: event.state(),
serial, serial,
time: event.time() as u32, time: event.time_msec(),
}, },
); );
pointer.frame(self); pointer.frame(self);
@@ -490,7 +490,7 @@ impl Ferese {
) { ) {
return; 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 { if horizontal != 0.0 {
frame = frame.value(Axis::Horizontal, horizontal); frame = frame.value(Axis::Horizontal, horizontal);
if let Some(value) = event.amount_v120(Axis::Horizontal) { if let Some(value) = event.amount_v120(Axis::Horizontal) {
@@ -534,7 +534,7 @@ impl Ferese {
slot: event.slot(), slot: event.slot(),
location, location,
serial, serial,
time: event.time() as u32, time: event.time_msec(),
}, },
); );
} }
@@ -550,7 +550,7 @@ impl Ferese {
&TouchMotionEvent { &TouchMotionEvent {
slot: event.slot(), slot: event.slot(),
location, location,
time: event.time() as u32, time: event.time_msec(),
}, },
); );
} }
@@ -561,7 +561,7 @@ impl Ferese {
&UpEvent { &UpEvent {
slot: event.slot(), slot: event.slot(),
serial: SERIAL_COUNTER.next_serial(), serial: SERIAL_COUNTER.next_serial(),
time: event.time() as u32, time: event.time_msec(),
}, },
); );
} }
+53
View File
@@ -17,6 +17,22 @@ use smithay::{
pub(crate) const HANDOFF: Duration = Duration::from_millis(80); pub(crate) const HANDOFF: Duration = Duration::from_millis(80);
pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024; pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024;
#[derive(Default)]
pub(crate) struct CallbackClock {
last_sent: Option<Instant>,
}
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 { pub(crate) fn frame_delta(last: &mut Instant, now: Instant) -> Duration {
let delta = now.saturating_duration_since(*last); let delta = now.saturating_duration_since(*last);
*last = (*last).max(now); *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] #[test]
fn mixed_refresh_outputs_do_not_double_the_animation_clock() { fn mixed_refresh_outputs_do_not_double_the_animation_clock() {
let start = Instant::now(); let start = Instant::now();
+16 -13
View File
@@ -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) { fn activate_output_workspace(&mut self, output: OutputId, workspace: ferese_core::WorkspaceId) {
if let Err(error) = self.output_workspaces.focus_output(output) { if let Err(error) = self.output_workspaces.focus_output(output) {
tracing::error!(%error, "failed to focus output"); tracing::error!(%error, "failed to focus output");
@@ -1023,6 +1019,12 @@ impl Ferese {
let CursorImageStatus::Surface(surface) = &self.cursor_status else { let CursorImageStatus::Surface(surface) = &self.cursor_status else {
return; 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( send_frames_surface_tree(
surface, surface,
@@ -1949,18 +1951,19 @@ impl Ferese {
time: DrmEventTime, time: DrmEventTime,
sequence: u32, sequence: u32,
) { ) {
let delta = self if let Some(backend) = self.direct_backend.as_mut() {
.direct_backend backend.record_presentation(node, crtc, time, sequence);
.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());
} }
} }
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 { fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool {
let delta = delta.mul_f64(self.animation_speed); let delta = delta.mul_f64(self.animation_speed);
let visible_workspaces = self let visible_workspaces = self