fix: schedule pending resizes with bounded deadlines (#42)
Co-authored-by: Samuel Onoja <[email protected]>
This commit is contained in:
1 parent
216289d66c
commit
e980bb2e43
5 files changed
+265
-11
No files matched your search
@@ -1668,10 +1668,37 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan
|
|||||||
fallbacks
|
fallbacks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pending buffers are events with deadlines, not visual animations. Recompute
|
||||||
|
// from current output identities and waits so commit, replacement and teardown
|
||||||
|
// cancel stale wakeups, including mirrored and sleeping outputs.
|
||||||
|
fn resize_deadlines(state: &Ferese) -> Vec<(Output, Instant)> {
|
||||||
|
let Some(backend) = state.direct_backend.as_ref().filter(|backend| backend.can_render()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
if state.session_lock.sleeping {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
backend
|
||||||
|
.devices
|
||||||
|
.values()
|
||||||
|
.filter(|device| device.drm.is_active())
|
||||||
|
.flat_map(|device| device.outputs.values())
|
||||||
|
.filter(|output| output.power.can_render())
|
||||||
|
.filter_map(|output| {
|
||||||
|
let scene = backend.scene_output(state, &output.output);
|
||||||
|
Some((
|
||||||
|
output.output.clone(),
|
||||||
|
state.start_time + state.output_resize_deadline(&scene)?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn arm_animation_timer(state: &mut Ferese) {
|
fn arm_animation_timer(state: &mut Ferese) {
|
||||||
let deadline = animation_fallbacks(state)
|
let deadline = animation_fallbacks(state)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, _, deadline)| *deadline)
|
.map(|(_, _, deadline)| *deadline)
|
||||||
|
.chain(resize_deadlines(state).into_iter().map(|(_, deadline)| deadline))
|
||||||
.min();
|
.min();
|
||||||
let Some(backend) = state.direct_backend.as_mut() else {
|
let Some(backend) = state.direct_backend.as_mut() else {
|
||||||
return;
|
return;
|
||||||
@@ -1698,6 +1725,18 @@ fn arm_animation_timer(state: &mut Ferese) {
|
|||||||
backend.scheduling_metrics.fallback_wakes += 1;
|
backend.scheduling_metrics.fallback_wakes += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let expired_outputs = resize_deadlines(state)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(output, deadline)| (deadline <= now).then_some(output))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !expired_outputs.is_empty() {
|
||||||
|
// Retire waits before rearming, even if a DRM submission is
|
||||||
|
// in flight. redraw advances the shared clock and requests the
|
||||||
|
// released outputs plus any final animation frames elsewhere.
|
||||||
|
render_on(state, &expired_outputs);
|
||||||
|
}
|
||||||
|
|
||||||
// Revalidate identities, power state, activity and normal frame chains
|
// Revalidate identities, power state, activity and normal frame chains
|
||||||
// at dispatch. Disconnected/replaced outputs never get a stale request.
|
// at dispatch. Disconnected/replaced outputs never get a stale request.
|
||||||
for (node, crtc, deadline) in animation_fallbacks(state) {
|
for (node, crtc, deadline) in animation_fallbacks(state) {
|
||||||
|
|||||||
@@ -25,6 +25,17 @@ impl ResizeTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn deadline(self) -> Duration {
|
||||||
|
self.started + DEADLINE
|
||||||
|
}
|
||||||
|
|
||||||
|
// A replacement configure changes readiness, not the bound on an
|
||||||
|
// uninterrupted wait. Clearing the transaction starts a fresh interaction.
|
||||||
|
pub(crate) fn superseded_by(self, mut replacement: Self) -> Self {
|
||||||
|
replacement.started = self.started;
|
||||||
|
replacement
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn serial(self) -> Serial {
|
pub(crate) fn serial(self) -> Serial {
|
||||||
self.serial
|
self.serial
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,40 @@ impl Ferese {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn output_resize_deadline(&self, output: &Output) -> Option<Duration> {
|
||||||
|
self.output_window_records(output, false)
|
||||||
|
.filter(|(id, _, _)| self.window_belongs_to_output(*id, output))
|
||||||
|
.flat_map(|(id, record, _)| {
|
||||||
|
// Client-size waits are retired while visiting mapped surfaces.
|
||||||
|
// Hidden tabs/workspaces must not arm an already-expired timer.
|
||||||
|
let mapped = self
|
||||||
|
.windows
|
||||||
|
.window(id)
|
||||||
|
.is_some_and(|window| self.space.element_location(window).is_some());
|
||||||
|
[
|
||||||
|
record.resize.map(|wait| wait.deadline()),
|
||||||
|
record
|
||||||
|
.geometry
|
||||||
|
.filter(|_| mapped)
|
||||||
|
.and_then(|geometry| geometry.client.commit_deadline()),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.min()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_is_waiting(&self, id: WindowId, geometry: &WindowGeometry) -> bool {
|
||||||
|
self.presentation_dependencies.reflow_blocked(id, &self.windows)
|
||||||
|
|| geometry.client.committed_size.is_some_and(|size| {
|
||||||
|
crate::presentation::resize_needs_old_frame(
|
||||||
|
geometry.visual.current,
|
||||||
|
geometry.logical,
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
|
pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
|
||||||
if self
|
if self
|
||||||
.render
|
.render
|
||||||
@@ -213,7 +247,7 @@ impl Ferese {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.output_window_records(output, false).any(|(id, record, _)| {
|
self.output_window_records(output, true).any(|(id, record, blocked)| {
|
||||||
let Some(mut geometry) = record.geometry else {
|
let Some(mut geometry) = record.geometry else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -222,9 +256,8 @@ impl Ferese {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if record.resize.is_some()
|
if record.opening.is_some()
|
||||||
|| record.opening.is_some()
|
|| (self.render.snapshot(&id).is_some() && !self.snapshot_is_waiting(id, &geometry))
|
||||||
|| self.render.snapshot(&id).is_some()
|
|
||||||
|| record.focus.as_ref().is_some_and(|focus| focus.is_animating())
|
|| record.focus.as_ref().is_some_and(|focus| focus.is_animating())
|
||||||
|| record.shadow.as_ref().is_some_and(|shadow| shadow.is_animating())
|
|| record.shadow.as_ref().is_some_and(|shadow| shadow.is_animating())
|
||||||
|| record
|
|| record
|
||||||
@@ -235,7 +268,7 @@ impl Ferese {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) {
|
if !blocked && geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,13 +276,18 @@ impl Ferese {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.viewports
|
(!self
|
||||||
|
.presentation_dependencies
|
||||||
|
.viewport_blocked(workspace, &self.windows)
|
||||||
|
&& self
|
||||||
|
.viewports
|
||||||
.get(&workspace)
|
.get(&workspace)
|
||||||
.is_some_and(ViewportPresentation::needs_tick)
|
.is_some_and(ViewportPresentation::needs_tick))
|
||||||
|| record.coupled_width.as_ref().is_some_and(|(_, width)| {
|
|| (!blocked
|
||||||
|
&& record.coupled_width.as_ref().is_some_and(|(_, width)| {
|
||||||
let mut width = *width;
|
let mut width = *width;
|
||||||
width.advance(Duration::ZERO, self.viewport_spring_config)
|
width.advance(Duration::ZERO, self.viewport_spring_config)
|
||||||
})
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,6 +412,117 @@ fn predict_geometry(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_resize_waits_without_refresh_cadence_and_recovers() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"state::prediction::tests::pending_resize_waits_without_refresh_cadence_and_recovers",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
use crate::frame_scheduler::FrameScheduler;
|
||||||
|
let mut event_loop = EventLoop::try_new().unwrap();
|
||||||
|
let display = Display::new().unwrap();
|
||||||
|
let config = crate::config::Config::default().runtime_config().unwrap();
|
||||||
|
let mut state = Ferese::new(&mut event_loop, display, config).unwrap();
|
||||||
|
let output = Output::new(
|
||||||
|
"resize-idle".into(),
|
||||||
|
smithay::output::PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: smithay::output::Subpixel::Unknown,
|
||||||
|
make: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
output.change_current_state(
|
||||||
|
Some(smithay::output::Mode {
|
||||||
|
size: (1920, 1080).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some((0, 0).into()),
|
||||||
|
);
|
||||||
|
state.space.map_output(&output, (0, 0));
|
||||||
|
state.register_output(&output, "resize-idle".into());
|
||||||
|
let id = WindowId(1);
|
||||||
|
state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap();
|
||||||
|
let settled = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
|
||||||
|
state.windows.records.insert(
|
||||||
|
id,
|
||||||
|
super::super::window_registry::WindowRecord {
|
||||||
|
geometry: Some(settled),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
state.windows.set_transaction(
|
||||||
|
id,
|
||||||
|
crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO),
|
||||||
|
);
|
||||||
|
state.rebuild_presentation_dependencies();
|
||||||
|
assert_eq!(state.output_resize_deadline(&output), Some(Duration::from_millis(300)));
|
||||||
|
let mut held = settled;
|
||||||
|
held.visual.set_target(Rect::new(100., 0., 400., 300.));
|
||||||
|
state.windows.set_geometry(id, held);
|
||||||
|
// Geometry which cannot advance is not visual activity either.
|
||||||
|
assert!(!state.output_has_animations(&output));
|
||||||
|
for hz in [60, 120, 240] {
|
||||||
|
let interval = Duration::from_nanos(1_000_000_000 / hz);
|
||||||
|
let mut scheduler = FrameScheduler::new(interval);
|
||||||
|
scheduler.request(Duration::ZERO);
|
||||||
|
let mut attempts = 0;
|
||||||
|
for tick in 0..(hz * 3 / 10) {
|
||||||
|
let now = interval * tick as u32;
|
||||||
|
if scheduler.plan(now).is_some() {
|
||||||
|
let plan = scheduler.begin().unwrap();
|
||||||
|
attempts += 1;
|
||||||
|
scheduler.rendered(plan, now, Duration::ZERO, false);
|
||||||
|
if state.sample_frame(&output, Duration::ZERO).animating {
|
||||||
|
scheduler.request(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(attempts, 1, "pending-only render attempts at {hz} Hz");
|
||||||
|
}
|
||||||
|
// Real fades remain live even while buffer-dependent geometry is held.
|
||||||
|
state.windows.update(id, |record| {
|
||||||
|
let mut opening = AnimatedValue::new(0.0);
|
||||||
|
opening.set_target(1.0);
|
||||||
|
record.opening = Some(opening);
|
||||||
|
});
|
||||||
|
assert!(state.output_has_animations(&output));
|
||||||
|
state.windows.update(id, |record| record.opening = None);
|
||||||
|
// A client commit removes the wait and wakes its held geometry.
|
||||||
|
let mut moving = settled;
|
||||||
|
moving.visual.set_target(Rect::new(100., 0., 400., 300.));
|
||||||
|
state.windows.set_geometry(id, moving);
|
||||||
|
state.windows.clear_transaction(&id);
|
||||||
|
state.release_presentation_dependencies();
|
||||||
|
assert_eq!(state.output_resize_deadline(&output), None);
|
||||||
|
assert!(state.output_has_animations(&output));
|
||||||
|
state.windows.geometry_mut(&id).unwrap().client.request_size(
|
||||||
|
ClientSize {
|
||||||
|
width: 800,
|
||||||
|
height: 300,
|
||||||
|
},
|
||||||
|
Duration::ZERO,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.output_resize_deadline(&output),
|
||||||
|
None,
|
||||||
|
"unmapped client-size waits cannot arm expired wakeups"
|
||||||
|
);
|
||||||
|
// Timeout processing still releases a pending wait.
|
||||||
|
state.windows.set_transaction(
|
||||||
|
id,
|
||||||
|
crate::resize_transaction::ResizeTransaction::new(10.into(), Duration::ZERO),
|
||||||
|
);
|
||||||
|
state.advance_animations_at(Duration::ZERO, Duration::from_millis(300));
|
||||||
|
assert!(state.windows.transaction(&id).is_none());
|
||||||
|
state.unregister_output(&output);
|
||||||
|
assert_eq!(state.output_resize_deadline(&output), None);
|
||||||
|
assert!(!state.output_has_animations(&output));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
||||||
fn focus_effect_sampling_preserves_activation_and_output_local_progress() {
|
fn focus_effect_sampling_preserves_activation_and_output_local_progress() {
|
||||||
|
|||||||
@@ -153,7 +153,13 @@ impl<W: Eq + Hash> WindowRegistry<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_transaction(&mut self, id: WindowId, transaction: ResizeTransaction) {
|
pub fn set_transaction(&mut self, id: WindowId, transaction: ResizeTransaction) {
|
||||||
self.update(id, |record| record.resize = Some(transaction));
|
self.update(id, |record| {
|
||||||
|
record.resize = Some(
|
||||||
|
record
|
||||||
|
.resize
|
||||||
|
.map_or(transaction, |pending| pending.superseded_by(transaction)),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_transaction(&mut self, id: &WindowId) {
|
pub fn clear_transaction(&mut self, id: &WindowId) {
|
||||||
@@ -366,6 +372,27 @@ mod tests {
|
|||||||
assert!(windows.record(second).is_some());
|
assert!(windows.record(second).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_configures_preserve_the_pending_wait_bound() {
|
||||||
|
use std::time::Duration;
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let id = windows.allocate_id();
|
||||||
|
windows.register("stalled", id);
|
||||||
|
for serial in 1u32..=30 {
|
||||||
|
let now = Duration::from_millis(u64::from(serial - 1) * 10);
|
||||||
|
windows.set_transaction(id, ResizeTransaction::new(serial.into(), now));
|
||||||
|
windows.expire_transactions(now);
|
||||||
|
assert!(windows.transaction(&id).is_some());
|
||||||
|
assert!(!windows.transaction(&id).unwrap().accepts(Some((serial - 1).into())));
|
||||||
|
}
|
||||||
|
windows.expire_transactions(Duration::from_millis(300));
|
||||||
|
assert!(windows.transaction(&id).is_none());
|
||||||
|
windows.set_transaction(id, ResizeTransaction::new(31.into(), Duration::from_millis(400)));
|
||||||
|
assert!(!windows.transaction(&id).unwrap().expired(Duration::from_millis(699)));
|
||||||
|
windows.expire_transactions(Duration::from_millis(700));
|
||||||
|
assert!(windows.transaction(&id).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn expired_resize_releases_only_its_barrier_and_preserves_geometry() {
|
fn expired_resize_releases_only_its_barrier_and_preserves_geometry() {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ impl ClientGeometry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.last_configured_size = Some(size);
|
self.last_configured_size = Some(size);
|
||||||
self.waiting_since = Some(now);
|
self.waiting_since.get_or_insert(now);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +276,11 @@ impl ClientGeometry {
|
|||||||
self.waiting_since.is_some()
|
self.waiting_since.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The hard bound for the current uninterrupted client-buffer wait.
|
||||||
|
pub fn commit_deadline(&self) -> Option<Duration> {
|
||||||
|
self.waiting_since.map(|started| started + CLIENT_COMMIT_TIMEOUT)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn timed_out(&self, now: Duration) -> bool {
|
pub fn timed_out(&self, now: Duration) -> bool {
|
||||||
self.waiting_since
|
self.waiting_since
|
||||||
.is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT)
|
.is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT)
|
||||||
@@ -545,6 +550,29 @@ fn finite_or(value: f64, fallback: f64) -> f64 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacing_size_requests_does_not_extend_a_stalled_client_wait() {
|
||||||
|
let mut client = ClientGeometry::default();
|
||||||
|
for width in 1..=50 {
|
||||||
|
assert!(client.request_size(
|
||||||
|
ClientSize { width, height: 100 },
|
||||||
|
Duration::from_millis((width as u64 - 1) * 10)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
assert_eq!(client.commit_deadline(), Some(Duration::from_millis(500)));
|
||||||
|
assert!(!client.commit(ClientSize { width: 49, height: 100 }));
|
||||||
|
assert!(client.expire_wait(Duration::from_millis(499)).is_none());
|
||||||
|
assert_eq!(
|
||||||
|
client.expire_wait(Duration::from_millis(500)),
|
||||||
|
Some(ClientSize { width: 50, height: 100 })
|
||||||
|
);
|
||||||
|
assert_eq!(client.commit_deadline(), None);
|
||||||
|
assert!(client.request_size(ClientSize { width: 51, height: 100 }, Duration::from_millis(600)));
|
||||||
|
assert_eq!(client.commit_deadline(), Some(Duration::from_millis(1100)));
|
||||||
|
assert!(client.commit(ClientSize { width: 51, height: 100 }));
|
||||||
|
assert_eq!(client.commit_deadline(), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn returning_fullscreen_window_does_not_resume_hidden_zoom() {
|
fn returning_fullscreen_window_does_not_resume_hidden_zoom() {
|
||||||
let normal = Rect::new(100.0, 50.0, 900.0, 700.0);
|
let normal = Rect::new(100.0, 50.0, 900.0, 700.0);
|
||||||
|
|||||||
Reference in new issue
Block a user