Scope redraw requests and avoid redundant animation fallback wakeups

Instrument redraw sources, per-output request coalescing, and animation fallback scheduling. Route local visual changes to their affected outputs while preserving deferred grab and gesture updates.

Schedule fallback recovery using each orphaned animation output's refresh interval, suppress it while a normal frame chain exists, and retain requested final frames after scheduling failures. Add mixed-refresh, cursor footprint, frame-chain, and disconnect regressions and document measurement limits.
This commit is contained in:
Samuel Onoja committed 2026-10-02 01:21:11 +01:00
1 parent 28e03a2624
commit 6f2951c6b0
27 files changed
+965 -139

No files matched your search

-4
View File
@@ -72,10 +72,6 @@ impl Ferese {
}
}
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();
#[cfg(feature = "resize-metrics")]
+14 -4
View File
@@ -45,7 +45,7 @@ impl Ferese {
return;
}
self.relayout();
self.relayout_window(window);
}
pub fn toggle_focused_fullscreen(&mut self) {
@@ -58,7 +58,7 @@ impl Ferese {
return;
}
self.relayout();
self.relayout_window(window);
}
pub fn toggle_focused_maximized(&mut self) {
@@ -106,7 +106,7 @@ impl Ferese {
}
}
}
self.relayout();
self.relayout_window(window);
}
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
@@ -124,7 +124,7 @@ impl Ferese {
pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) {
match self.workspaces.set_fullscreen(window, enabled) {
Ok(true) => self.relayout(),
Ok(true) => self.relayout_window(window),
Ok(false) => {}
Err(error) => {
tracing::error!(%error, ?window, enabled, "failed to set fullscreen window")
@@ -195,6 +195,16 @@ impl Ferese {
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
// Cursor rendering reads current_location(), which would lock it again.
// The event-loop epilogue coalesces this redraw after the grab returns.
let outputs = self
.space
.outputs()
.filter(|output| self.window_belongs_to_output(id, output))
.cloned()
.collect::<Vec<_>>();
for output in outputs {
self.defer_output_redraw(output);
}
self.cursor_redraw_pending = true;
}
+2 -1
View File
@@ -73,8 +73,9 @@ impl Ferese {
.as_mut()
.and_then(|cycle| cycle.advance(reverse, &available))
{
let previous = self.overview.selection_state();
self.overview.select_window(id);
crate::backends::direct::render_all(self);
self.redraw_overview_selection(previous);
} else {
self.cancel_focus_cycle();
}
+34 -1
View File
@@ -50,6 +50,35 @@ impl Ferese {
}
pub fn relayout(&mut self) {
self.relayout_outputs(None);
}
pub(crate) fn relayout_window(&mut self, id: WindowId) {
let outputs = self
.space
.outputs()
.filter(|output| self.window_belongs_to_output(id, output))
.cloned()
.collect::<Vec<_>>();
self.relayout_on(&outputs);
}
pub(crate) fn relayout_on(&mut self, outputs: &[Output]) {
self.relayout_outputs(Some(outputs.to_vec()));
}
fn relayout_outputs(&mut self, mut redraw_outputs: Option<Vec<Output>>) {
if let Some(outputs) = redraw_outputs.as_mut() {
outputs.extend(
self.space
.outputs()
.filter(|output| {
self.output_has_animations(output) || self.output_has_pending_visual_changes(output)
})
.cloned(),
);
}
self.refresh_input_capture_zones();
let visible_workspaces = self.visible_workspace_ids();
self.reconcile_workspaces(&visible_workspaces);
@@ -384,7 +413,11 @@ impl Ferese {
self.retarget_overview();
self.send_shell_snapshots();
crate::backends::direct::render_all(self);
if let Some(outputs) = redraw_outputs {
crate::backends::direct::render_on(self, &outputs);
} else {
crate::backends::direct::render_all(self);
}
}
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
+12 -2
View File
@@ -72,7 +72,15 @@ impl Ferese {
swipe.progress = progress;
}
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.cursor_redraw_pending = true;
if let Some(output) = self
.focus_swipe
.as_ref()
.and_then(|swipe| self.output_workspaces.output_for_workspace(swipe.workspace))
.and_then(|id| self.outputs_by_id.get(&id))
.cloned()
{
self.defer_output_redraw(output);
}
}
pub(crate) fn finish_focus_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
@@ -433,7 +441,9 @@ impl Ferese {
.held_progress = Some(progress);
self.refresh_workspace_slide_offsets();
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.cursor_redraw_pending = true;
if let Some(output) = self.outputs_by_id.get(&output).cloned() {
self.defer_output_redraw(output);
}
return;
}
if self.swipe.preview_started() {
+7
View File
@@ -1,6 +1,12 @@
use super::*;
impl Ferese {
pub(crate) fn defer_output_redraw(&mut self, output: Output) {
if !self.output_redraw_pending.contains(&output) {
self.output_redraw_pending.push(output);
}
}
pub fn register_output(&mut self, output: &Output, identity: String) {
if self.output_ids.is_empty() {
self.reset_animation_clock();
@@ -51,6 +57,7 @@ impl Ferese {
return;
};
self.output_redraw_pending.retain(|pending| pending != output);
self.outputs_by_id.remove(&output_id);
self.output_names.remove(&output_id);
self.workspace_slides.remove(&output_id);
+49 -1
View File
@@ -44,8 +44,36 @@ impl Ferese {
})
}
pub(crate) fn output_has_pending_visual_changes(&self, output: &Output) -> bool {
let selected = if self.overview.is_active() {
self.overview.selected()
} else {
self.focused_window
};
self.output_window_records(output, false).any(|(id, record, _)| {
self.window_belongs_to_output(id, output)
&& (record
.focus
.as_ref()
.is_some_and(|focus| focus.needs_update(if selected == Some(id) { 1.0 } else { 0.0 }))
|| record.dimming.as_ref().is_some_and(|dim| {
dim.needs_update(crate::dimming::target(
self.inactive_dim,
self.focused_window,
id,
self.overview.is_presenting(),
))
}))
})
}
pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() {
if self.overview.is_animating(self.spring_config)
|| self
.dismissing_popups
.iter()
.any(|(root, _, _)| self.surface_outputs(root).contains(output))
{
return true;
}
@@ -421,6 +449,26 @@ mod tests {
state.animations_enabled = true;
assert!(!state.sample_frame(&output, Duration::ZERO).animating);
assert!(state.sample_frame(&other, Duration::ZERO).animating);
// Target changes with animations disabled still need their final frame
// on both displays, even though no spring/fade remains active afterward.
state.focused_window = Some(id);
state
.windows
.update(id, |record| record.focus = Some(crate::dimming::DimAnimation::new(1.0)));
state.focused_window = Some(other_id);
assert!(state.output_has_pending_visual_changes(&output));
state.animations_enabled = false;
state.advance_animations(Instant::now());
assert!(!state.output_has_pending_visual_changes(&output));
state.animations_enabled = true;
state.windows.set_geometry(id, geometry);
assert!(state.output_has_animations(&output));
state.unregister_output(&other);
assert!(!state.output_has_animations(&other));
assert!(
state.output_has_animations(&output),
"remaining output keeps its active animation"
);
}
#[test]
+2 -2
View File
@@ -660,7 +660,7 @@ impl Ferese {
self.windows.update(focused, |record| {
record.closing.get_or_insert_default();
});
crate::backends::direct::render_all(self);
crate::backends::direct::render_window(self, focused);
}
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
@@ -672,7 +672,7 @@ impl Ferese {
self.windows.update(id, |record| {
record.closing.get_or_insert_default();
});
crate::backends::direct::render_all(self);
crate::backends::direct::render_window(self, id);
} else {
self.send_window_close(id);
}