Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Reset window buffers to native size on the overview exit's landing frame
The landing frame (zoom exactly 1.0) runs on the viewport path,
render_viewport_update -> scale_only_render_finish, which returned early
at scale 1.0 without undoing the previous frame's shrink. Every window
stayed drawn at the ramp's second-to-last zoom (~98%) until its own next
commit or the 120ms viewport settle popped it to full size, so windows
visibly "settled" a beat after the animation ended.
Window::buffers_scaled records that the last scale pass left buffers at
a non-natural dest size; at 1.0 the pass now runs once more to reset
them, and the pre-render loop in output.rs includes such windows.
Decorations take the window's effective scale so they reset with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/server/output.rs | 6 ++-
src/server/window.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 94 insertions(+), 15 deletions(-)
diff --git a/src/server/output.rs b/src/server/output.rs
index bf4e7da2..86aaea12 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -630,10 +630,12 @@ impl Output {
return Ok(());
}
- // Re-apply scale to all windows whose scale is not 1.0 right before rendering
+ // Re-apply scale to all windows whose scale is not 1.0 right before
+ // rendering — and to any whose buffers are still shrunk from a
+ // scale that has just returned to 1.0, which this pass resets.
let wm = &(*self.server).wm;
for &window in wm.windows.iter() {
- if !window.is_null() && ((*window).scale != 1.0 || (*window).x11_buffer_scale() != 1.0) {
+ if !window.is_null() && ((*window).scale != 1.0 || (*window).x11_buffer_scale() != 1.0 || (*window).buffers_scaled) {
(*window).scale_only_render_finish();
}
}
diff --git a/src/server/window.rs b/src/server/window.rs
index 731192cf..1b3f35cb 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -532,6 +532,10 @@ pub struct Window {
pub blur: bool,
pub scale: f64,
pub last_applied_scale: f64,
+ /// The last scale pass left the surface buffers at a dest size other
+ /// than their natural one. Landing back on 1.0 has to undo that once —
+ /// see `scale_only_render_finish`.
+ pub buffers_scaled: bool,
pub virtual_x: f64,
pub virtual_y: f64,
pub resize_start_vx: f64,
@@ -564,6 +568,10 @@ pub struct Window {
pub status_commit_size: (i32, i32),
pub commit: ffi::wl_listener,
pub was_fullscreen: bool,
+ /// A fullscreen window drawn on the desk rather than pinned to its
+ /// output: stepped aside, or sliding back in under the camera. Set by
+ /// `WindowManager::place_fullscreen_windows`, read by the render pass.
+ pub fs_on_desk: bool,
pub saved_width: i32,
pub saved_height: i32,
pub saved_virtual_x: f64,
@@ -649,6 +657,11 @@ impl Window {
/// fullscreen one back over it. Only a desk window displaces it — not
/// its own popups or dialogs, a status segment, or a window that has
/// since been minimized or unmapped.
+ ///
+ /// Stepped aside, it is drawn on the desk at the spot it covered
+ /// (`virtual_x/y`, kept in step with the camera while it is on top), so
+ /// the camera pans away from it like any other window rather than
+ /// leaving it fixed behind the screen.
pub unsafe fn fullscreen_yields(&self) -> bool {
let me = self as *const Window as *mut Window;
for &w in (*self.server).wm.focus_history.iter() {
@@ -684,6 +697,23 @@ impl Window {
false
}
+ /// The camera pan that puts this fullscreen window's desk spot exactly
+ /// on its output — where focusing it pans back to, so a window that
+ /// stepped aside slides in and lands pinned without a jump. `None`
+ /// without an output to fill.
+ pub unsafe fn fullscreen_anchor_pan(&self) -> Option<(f64, f64)> {
+ let output = self.fullscreen_output();
+ if output.is_null() {
+ return None;
+ }
+ let zoom = (*self.server).wm.desk_zoom.max(0.01);
+ let (first_x, first_y, _, _) = self.first_enabled_output_box();
+ Some((
+ self.virtual_x - ((*output).sent.x as f64 - first_x) / zoom,
+ self.virtual_y - ((*output).sent.y as f64 - first_y) / zoom,
+ ))
+ }
+
pub unsafe fn role(&self) -> crate::policy::api::WindowRole {
if self.grid_declared {
return crate::policy::api::WindowRole::Grid;
@@ -890,6 +920,7 @@ impl Window {
blur: false,
scale: 1.0,
last_applied_scale: 1.0,
+ buffers_scaled: false,
virtual_x: unsafe { (*server).wm.desk_pan_x + 100.0 },
virtual_y: unsafe { (*server).wm.desk_pan_y + 100.0 },
resize_start_vx: 0.0,
@@ -904,6 +935,7 @@ impl Window {
status_commit_size: (0, 0),
commit: std::mem::zeroed(),
was_fullscreen: false,
+ fs_on_desk: false,
saved_width: 0,
saved_height: 0,
saved_virtual_x: 0.0,
@@ -3002,6 +3034,13 @@ impl Window {
self.saved_virtual_x = self.virtual_x;
self.saved_virtual_y = self.virtual_y;
self.was_fullscreen = true;
+ // From here on virtual_x/y is the desk spot the window covers
+ // — where the camera is now — so stepping aside leaves it
+ // there (`WindowManager::place_fullscreen_windows`, which
+ // keeps it in step while the window is on top).
+ let (vx, vy) = self.screen_to_virtual((*output).sent.x, (*output).sent.y);
+ self.virtual_x = vx;
+ self.virtual_y = vy;
log::info!("[Fullscreen] Saved window {:?} geometry: {}x{} at ({}, {})", self.get_title_string().as_deref().unwrap_or(""), self.saved_width, self.saved_height, self.saved_virtual_x, self.saved_virtual_y);
}
} else if !new_fullscreen && self.was_fullscreen {
@@ -3456,6 +3495,7 @@ impl Window {
&scale_data_popup as *const ScaleData as *mut std::ffi::c_void,
);
self.last_applied_scale = self.scale;
+ self.buffers_scaled = scale_x != 1.0 || scale_y != 1.0;
}
// During an interactive resize, size the box from the client's
@@ -3511,8 +3551,15 @@ impl Window {
};
if !output.is_null() {
- self.box_geom.x = (*output).sent.x;
- self.box_geom.y = (*output).sent.y;
+ if self.fs_on_desk {
+ // Stepped aside: at its desk spot, which the arrange pass
+ // put in rendering_requested (`place_fullscreen_windows`).
+ self.box_geom.x = requested.x;
+ self.box_geom.y = requested.y;
+ } else {
+ self.box_geom.x = (*output).sent.x;
+ self.box_geom.y = (*output).sent.y;
+ }
let app_id_ptr = self.get_app_id();
let (is_status_bar, is_wallpaper) = if !app_id_ptr.is_null() {
@@ -3524,7 +3571,7 @@ impl Window {
ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, !is_status_bar && !is_wallpaper);
let (width, height) = (*output).sent.dimensions();
- ffi::wlr_scene_rect_set_size(self.fullscreen_background, width as i32, height as i32);
+ self.size_fullscreen_background(width as i32, height as i32);
clip = ffi::wlr_box { x: 0, y: 0, width: width as i32, height: height as i32 };
content_clip = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
@@ -3624,9 +3671,18 @@ impl Window {
// The zoom the overview asks for, times 1/output-scale for an X11
// surface whose buffer is physical pixels (`x11_buffer_scale`).
let eff_scale = self.scale * self.x11_buffer_scale();
+ // At 1.0 there is nothing to apply — unless the previous pass left
+ // the buffers shrunk. An overview exit's landing frame runs on the
+ // viewport path (`render_viewport_update` -> here), not through
+ // `render_finish`, so returning early there left every window drawn
+ // at the ramp's second-to-last zoom (~98%) until the 120ms viewport
+ // settle, or its own next commit, popped it to full size: the
+ // windows visibly "settled" a beat after the animation ended.
if eff_scale == 1.0 {
self.last_applied_scale = 1.0;
- return;
+ if !self.buffers_scaled {
+ return;
+ }
}
// No last_applied_scale short-circuit here: wlroots' scene-surface
@@ -3637,7 +3693,10 @@ impl Window {
// per rendered frame (output.rs render_and_commit), after commits and
// before build_state, and every setter below is change-checked — an
// already-correct tree produces no damage.
- self.last_applied_scale = self.scale;
+ if eff_scale != 1.0 {
+ self.last_applied_scale = self.scale;
+ }
+ self.buffers_scaled = eff_scale != 1.0;
struct ScaleData {
scale: f64,
@@ -3714,7 +3773,7 @@ impl Window {
while curr != list_head {
let next = (*curr).next;
let dec = crate::container_of!(curr, Decoration, link);
- (*dec).scale_only_render_finish();
+ (*dec).scale_only_render_finish(eff_scale);
curr = next;
}
}
@@ -3836,6 +3895,14 @@ impl Window {
self.sync_backdrop_compress();
}
+ if self.fs_on_desk && self.fs_anim.is_none() {
+ let output = self.fullscreen_output();
+ if !output.is_null() {
+ let (w, h) = (*output).sent.dimensions();
+ self.size_fullscreen_background(w as i32, h as i32);
+ }
+ }
+
self.scale_only_render_finish();
self.draw_borders();
}
@@ -4287,6 +4354,18 @@ impl Window {
moving
}
+ /// The black backdrop under a fullscreen surface, at the output's size
+ /// — scaled with the window when it sits on a zoomed-out desk, since
+ /// the surface's buffers shrink with `scale` and the backdrop does not.
+ unsafe fn size_fullscreen_background(&mut self, width: i32, height: i32) {
+ let s = if self.fs_on_desk { self.scale } else { 1.0 };
+ ffi::wlr_scene_rect_set_size(
+ self.fullscreen_background,
+ (width as f64 * s).round() as i32,
+ (height as f64 * s).round() as i32,
+ );
+ }
+
/// The output a fullscreen window fills: the one the WM pinned it to, or
/// the first enabled output (the same fallback manage/render use).
pub unsafe fn fullscreen_output(&self) -> *mut crate::output::Output {
@@ -5706,11 +5785,9 @@ impl Decoration {
}
}
- pub unsafe fn scale_only_render_finish(&mut self) {
- let scale = (*self.window).scale;
- if scale * (*self.window).x11_buffer_scale() == 1.0 {
- return;
- }
+ /// Driven by the window's own pass, which decides when to run it
+ /// (including the one reset pass back at `eff_scale` 1.0).
+ pub unsafe fn scale_only_render_finish(&mut self, eff_scale: f64) {
struct ScaleData {
scale: f64,
@@ -5759,7 +5836,7 @@ impl Decoration {
// leaves it briefly at the old zoom, which restore corrects.
}
- let scale_data = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
+ let scale_data = ScaleData { scale: eff_scale, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
ffi::wlr_scene_node_for_each_buffer(
self.surfaces.tree as *mut ffi::wlr_scene_node,
Some(set_overview_scale_iterator),
@@ -5767,7 +5844,7 @@ impl Decoration {
);
if self.surfaces.saved {
- let scale_data_saved = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
+ let scale_data_saved = ScaleData { scale: eff_scale, ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
ffi::wlr_scene_node_for_each_buffer(
self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
Some(set_overview_scale_iterator),