Fix DRM callback pacing and shared animation timing
This commit is contained in:
1 parent
46d33751c6
commit
83574451df
4 files changed
+270
-52
No files matched your search
@@ -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<LibinputDevice>,
|
||||
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
|
||||
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
|
||||
animation_timer: Option<RegistrationToken>,
|
||||
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<RegistrationToken>,
|
||||
lock_frame_pending: bool,
|
||||
}
|
||||
|
||||
@@ -231,6 +238,8 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, 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<Ferese>, 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,13 +536,26 @@ fn open_primary_device(
|
||||
{
|
||||
output.frame_pending = false;
|
||||
}
|
||||
if focused {
|
||||
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
|
||||
}
|
||||
tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip");
|
||||
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) => {
|
||||
tracing::error!(?node, %error, "DRM event 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
|
||||
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"),
|
||||
}
|
||||
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);
|
||||
restore_device(state, node, device);
|
||||
if queued {
|
||||
send_frame_callbacks(state, &callback_output);
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) {
|
||||
@@ -1120,12 +1144,148 @@ 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| {
|
||||
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(),
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
+15
-15
@@ -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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<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 {
|
||||
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();
|
||||
|
||||
+16
-13
@@ -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
|
||||
|
||||
Reference in new issue
Block a user