Decouple scrolling translation from resize readiness

Track configure-owned presentation dependencies so independent viewport translation can continue while clients resize. Preserve linked reflow waits, native snapshots, handoff deadlines and shared rendering/input coordinates.

Closes #10.
This commit is contained in:
Samuel Onoja authored and GitHub committed 2026-10-04 08:05:44 +01:00
1 parent c1801e1f07
commit d5a829eb16
14 files changed
+1742 -72

No files matched your search

+81 -40
View File
@@ -1,7 +1,19 @@
use super::*;
impl Ferese {
pub(super) fn presentation_now(&self) -> Duration {
#[cfg(test)]
if let Some(now) = self.animation_test_time {
return now;
}
self.start_time.elapsed()
}
pub fn advance_animations(&mut self, now: Instant) -> bool {
#[cfg(test)]
if let Some(time) = self.animation_test_time {
return self.advance_animations_at(Duration::ZERO, time);
}
let delta = crate::presentation::frame_delta(&mut self.last_animation_tick, now);
// Consume wall time even while presentation is paused. A paused
// desktop must not turn suspend/DPMS time into a spring step.
@@ -35,7 +47,10 @@ impl Ferese {
continue;
};
let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0;
let viewport = self.viewport_animations.get(&workspace).is_some_and(moving)
let viewport = self
.presentation_dependencies
.viewport_blocked(workspace, &self.windows)
&& self.viewport_animations.get(&workspace).is_some_and(moving)
&& self
.output_workspaces
.output_for_workspace(workspace)
@@ -51,6 +66,7 @@ impl Ferese {
.chain(workspace.floating.iter().copied())
.any(|other| {
other != *id
&& self.presentation_dependencies.reflow_blocked(other, &self.windows)
&& self
.windows
.window(other)
@@ -80,7 +96,7 @@ impl Ferese {
fn animations_need_tick(&self) -> bool {
if self.focus_swipe.is_some()
|| !self.paused_workspaces.is_empty()
|| self.presentation_dependencies.needs_tick()
|| !self.workspace_slides.is_empty()
|| !self.render.closing.is_empty()
|| !self.dismissing_popups.is_empty()
@@ -125,11 +141,15 @@ impl Ferese {
}
pub(super) fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool {
self.advance_animations_at(delta, self.start_time.elapsed())
self.advance_animations_at(delta, self.presentation_now())
}
// The production caller supplies monotonic wall time; tests supply a fake clock.
pub(super) fn advance_animations_at(&mut self, delta: Duration, now: Duration) -> bool {
#[cfg(test)]
if self.animation_test_time.is_some() {
self.animation_test_time = Some(now);
}
if !self.animations_need_tick() {
self.sync_window_stacking();
return false;
@@ -141,7 +161,11 @@ impl Ferese {
.as_ref()
.is_some_and(|swipe| !self.focus_swipe_is_current(swipe))
{
self.focus_swipe = None;
if let Some(swipe) = self.focus_swipe.take() {
self.presentation_dependencies
.restore_viewport(swipe.workspace, swipe.dependencies);
self.release_presentation_dependencies();
}
}
let mut active_animation = false;
@@ -274,13 +298,8 @@ impl Ferese {
self.render.remove_window(id);
}
// Include transactions expiring on this dispatch: their waiting time
// must not be charged to the first resumed animation step.
let paused_before_expiry = self
.windows
.resizing()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
// Capture holds before expiry so waiting wall time never steps reflow.
let (mut held_reflow, mut held_viewport) = self.presentation_dependencies.holds(&self.windows);
#[cfg(feature = "resize-metrics")]
self.measure_resize_pauses(now);
#[cfg(feature = "resize-metrics")]
@@ -291,15 +310,18 @@ impl Ferese {
}
}
}
self.windows.expire_transactions(now);
let blocked_workspaces = self
let expired = self
.windows
.resizing()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
let mut resume_pauses = std::mem::replace(&mut self.paused_workspaces, blocked_workspaces.clone());
resume_pauses.extend(paused_before_expiry);
.any(|id| self.windows.transaction(id).unwrap().expired(now));
self.windows.expire_transactions(now);
if expired {
self.release_presentation_dependencies();
}
let (released_reflow, released_viewport) = self.presentation_dependencies.holds(&self.windows);
held_reflow.extend(released_reflow);
held_viewport.extend(released_viewport);
let animations_enabled = self.animations_enabled;
let animation_speed = self.animation_speed;
#[cfg(feature = "resize-metrics")]
@@ -309,10 +331,7 @@ impl Ferese {
return false;
}
let waiting_for_client = self
.workspaces
.workspace_for_window(*id)
.is_some_and(|workspace| blocked_workspaces.contains(&workspace));
let waiting_for_client = self.presentation_dependencies.reflow_blocked(*id, &self.windows);
// A shrinking client's destination buffer arrives before the
// animated bounds reach it. Keep the old native pixels covering
// that strip rather than fading them into the neutral resize fill.
@@ -349,7 +368,8 @@ impl Ferese {
});
active_animation |= self.render.snapshots().next().is_some();
// Keep scheduling frames while waiting, so the deadline cannot stall.
active_animation |= !blocked_workspaces.is_empty() || !resume_pauses.is_empty();
active_animation |=
self.windows.resizing().next().is_some() || !held_reflow.is_empty() || !held_viewport.is_empty();
for (workspace, viewport) in &mut self.viewport_animations {
if self
.output_workspaces
@@ -359,7 +379,7 @@ impl Ferese {
continue;
}
if blocked_workspaces.contains(workspace) || resume_pauses.contains(workspace) {
if held_viewport.contains(workspace) {
continue;
}
@@ -380,14 +400,7 @@ impl Ferese {
continue;
};
if self
.workspaces
.workspace_for_window(id)
.is_some_and(|workspace| blocked_workspaces.contains(&workspace) || resume_pauses.contains(&workspace))
{
continue;
}
let held = held_reflow.contains(&id);
let Some(record) = self.windows.record_mut(id) else {
continue;
};
@@ -397,6 +410,8 @@ impl Ferese {
continue;
};
let previous = geometry.visual.current;
geometry.presentation_changed = false;
let zooming = geometry.is_zooming();
let coupled_target = record.coupled_width.as_ref().map(|(_, width)| width.target);
@@ -404,7 +419,9 @@ impl Ferese {
geometry.visual.target.width = geometry.visual.current.width;
geometry.visual.velocity.width = 0.0;
}
active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled);
if !held {
active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled);
}
if zooming != geometry.is_zooming() {
self.stacking_cache.invalidate();
@@ -419,18 +436,28 @@ impl Ferese {
{
if zooming {
sync_scrolling_coordinates(geometry, world_x, viewport, true);
} else if self.animations_enabled {
} else if !held && self.animations_enabled {
active_animation |= world_x.advance(delta, self.spring_config);
} else {
} else if !held {
world_x.snap();
}
if !zooming {
sync_scrolling_coordinates(geometry, world_x, viewport, false);
let mut presented_world = *world_x;
if held {
presented_world.velocity = 0.0;
}
let mut presented_viewport = *viewport;
if held_viewport.contains(workspace) {
presented_viewport.velocity = 0.0;
}
sync_scrolling_coordinates(geometry, &mut presented_world, &presented_viewport, false);
}
}
if let Some((_, width)) = record.coupled_width.as_mut() {
if let Some((_, width)) = record.coupled_width.as_mut()
&& !held
{
let width_active = if self.animations_enabled {
width.advance(delta, self.viewport_spring_config)
} else {
@@ -455,7 +482,9 @@ impl Ferese {
);
}
if let Some(size) = geometry.presentation_size_request(now)
geometry.presentation_changed |= previous != geometry.visual.current;
if !held
&& let Some(size) = geometry.presentation_size_request(now)
&& !natural_pending
&& let Some(toplevel) = window.toplevel()
{
@@ -864,6 +893,10 @@ mod tests {
id,
crate::resize_transaction::ResizeTransaction::new(1.into(), Duration::from_millis(40)),
);
state.rebuild_presentation_dependencies();
state
.presentation_dependencies
.wait_for_viewport(workspace, id, 1.into());
state.advance_animations_at(Duration::from_millis(300), Duration::from_millis(340));
assert!(state.windows.transaction(&id).is_none());
assert_eq!(state.viewport_animations[&workspace], frozen);
@@ -882,6 +915,10 @@ mod tests {
id,
crate::resize_transaction::ResizeTransaction::new(2.into(), Duration::from_millis(356)),
);
state.rebuild_presentation_dependencies();
state
.presentation_dependencies
.wait_for_viewport(workspace, id, 2.into());
state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(456));
let frozen = state.viewport_animations[&workspace];
state.windows.clear_transaction(&id);
@@ -935,10 +972,14 @@ mod tests {
id,
crate::resize_transaction::ResizeTransaction::new(3.into(), Duration::from_millis(800)),
);
state.rebuild_presentation_dependencies();
state
.presentation_dependencies
.wait_for_viewport(workspace, id, 3.into());
state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(900));
assert!(state.paused_workspaces.contains(&workspace));
assert!(state.presentation_dependencies.needs_tick());
state.windows.clear_transaction(&id);
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(916));
assert!(state.paused_workspaces.is_empty());
assert!(!state.presentation_dependencies.needs_tick());
}
}