Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix: frozen saved-surface copies keep their save-time size — zoom ghosts
The remaining zoom artifacts ("stale shadows around claude that stack
over time") were REAL scene content: frozen buffer copies made by
surfaces.save() (transaction freezes and every unmapping window, which
renders on while Closing) resized by the scale iterators' non-surface
fallback from the buffer's NATURAL size. That size is meaningless for
geometry — HiDPI clients commit scale-2 buffers (2x too big) and
Chromium pads buffers beyond the surface and crops via viewport src
(claude's buffer: 1536x768 natural for a 527x168 window in the nested
repro, ~3x) — so every zoom change ballooned the copies into
full-size-looking ghosts hugging the scaled window, made persistent by
the (correct) full-damage repaint. All four iterator sites now leave
non-surface buffers untouched: a frozen copy keeps the dest/position
it was saved with (correct at save time); zooming mid-transaction
leaves it briefly at the old zoom, which restore corrects.
Also: new `ccectl debug-buffers [app_id|id]` scene introspection (this
found the bug — dumps per-buffer pos/dest/natural/surface-backed for a
window's trees), with river wrappers for dest size and node enabled.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/cce_ctl.rs | 1 +
src/server/window.rs | 88 +++++++++++++++-------------------------
src/server/window_manager.rs | 58 ++++++++++++++++++++++++++
src/server/wlroots_log_wrapper.c | 12 ++++++
wrapper.h | 3 ++
5 files changed, 106 insertions(+), 56 deletions(-)
diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index 014e827..a2fc5fd 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -69,6 +69,7 @@ fn usage(name: &str, to_stderr: bool) {
print(" screenshot # capture the screen to ~/Pictures/screenshots");
print(" screenshot region <x> <y> <w> <h> # capture an on-screen region (logical px)");
print(" screenshot window [app_id|id] # capture a window (focused if omitted; works off-screen)");
+ print(" debug-buffers [app_id|id] # dump a window's scene buffers (pos/dest/natural/surface)");
print(" notify <title> [body]");
print(" bind <mods> <keysym> <action> [args...]");
print(" pbind <mods> <button> <action>");
diff --git a/src/server/window.rs b/src/server/window.rs
index 175193b..582bcdb 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1993,21 +1993,15 @@ impl Window {
let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
}
- } else if data.scale == 1.0 {
- ffi::river_scene_buffer_set_dest_size_if_changed(buffer, 0, 0);
- ffi::river_scene_node_set_position_if_changed(node, sx, sy);
- } else {
- let w = ffi::river_scene_buffer_get_width(buffer);
- let h = ffi::river_scene_buffer_get_height(buffer);
- let dest_w = (w as f64 * data.scale) as i32;
- let dest_h = (h as f64 * data.scale) as i32;
- ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
-
- let (px, py) = get_parent_position_relative_to(node, data.ancestor);
- let dest_x = (px as f64 * (data.scale - 1.0)) as i32;
- let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
- ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
}
+ // Non-surface buffers are frozen SAVED copies (see
+ // save_surface_tree_iter): their natural buffer size is
+ // meaningless for geometry — HiDPI clients commit scale-N
+ // buffers and Chromium pads buffers beyond the surface,
+ // cropping via viewport src — so rescaling from it ballooned
+ // ghosts around the window at any zoom change. A frozen copy
+ // keeps its save-time dest/position; a zoom mid-transaction
+ // leaves it briefly at the old zoom, which restore corrects.
}
let scale_data_surfaces = ScaleData { scale: self.scale, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
@@ -2207,21 +2201,15 @@ impl Window {
let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
}
- } else if data.scale == 1.0 {
- ffi::river_scene_buffer_set_dest_size_if_changed(buffer, 0, 0);
- ffi::river_scene_node_set_position_if_changed(node, sx, sy);
- } else {
- let w = ffi::river_scene_buffer_get_width(buffer);
- let h = ffi::river_scene_buffer_get_height(buffer);
- let dest_w = (w as f64 * data.scale) as i32;
- let dest_h = (h as f64 * data.scale) as i32;
- ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
-
- let (px, py) = get_parent_position_relative_to(node, data.ancestor);
- let dest_x = (px as f64 * (data.scale - 1.0)) as i32;
- let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
- ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
}
+ // Non-surface buffers are frozen SAVED copies (see
+ // save_surface_tree_iter): their natural buffer size is
+ // meaningless for geometry — HiDPI clients commit scale-N
+ // buffers and Chromium pads buffers beyond the surface,
+ // cropping via viewport src — so rescaling from it ballooned
+ // ghosts around the window at any zoom change. A frozen copy
+ // keeps its save-time dest/position; a zoom mid-transaction
+ // leaves it briefly at the old zoom, which restore corrects.
}
let scale_data_surfaces = ScaleData { scale: self.scale, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
@@ -3616,21 +3604,15 @@ impl Decoration {
let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
}
- } else if data.scale == 1.0 {
- ffi::river_scene_buffer_set_dest_size_if_changed(buffer, 0, 0);
- ffi::river_scene_node_set_position_if_changed(node, sx, sy);
- } else {
- let w = ffi::river_scene_buffer_get_width(buffer);
- let h = ffi::river_scene_buffer_get_height(buffer);
- let dest_w = (w as f64 * data.scale) as i32;
- let dest_h = (h as f64 * data.scale) as i32;
- ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
-
- let (px, py) = get_parent_position_relative_to(node, data.ancestor);
- let dest_x = (px as f64 * (data.scale - 1.0)) as i32;
- let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
- ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
}
+ // Non-surface buffers are frozen SAVED copies (see
+ // save_surface_tree_iter): their natural buffer size is
+ // meaningless for geometry — HiDPI clients commit scale-N
+ // buffers and Chromium pads buffers beyond the surface,
+ // cropping via viewport src — so rescaling from it ballooned
+ // ghosts around the window at any zoom change. A frozen copy
+ // keeps its save-time dest/position; a zoom mid-transaction
+ // leaves it briefly at the old zoom, which restore corrects.
}
let scale_data = ScaleData { scale, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
@@ -3692,21 +3674,15 @@ impl Decoration {
let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
}
- } else if data.scale == 1.0 {
- ffi::river_scene_buffer_set_dest_size_if_changed(buffer, 0, 0);
- ffi::river_scene_node_set_position_if_changed(node, sx, sy);
- } else {
- let w = ffi::river_scene_buffer_get_width(buffer);
- let h = ffi::river_scene_buffer_get_height(buffer);
- let dest_w = (w as f64 * data.scale) as i32;
- let dest_h = (h as f64 * data.scale) as i32;
- ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
-
- let (px, py) = get_parent_position_relative_to(node, data.ancestor);
- let dest_x = (px as f64 * (data.scale - 1.0)) as i32;
- let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
- ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
}
+ // Non-surface buffers are frozen SAVED copies (see
+ // save_surface_tree_iter): their natural buffer size is
+ // meaningless for geometry — HiDPI clients commit scale-N
+ // buffers and Chromium pads buffers beyond the surface,
+ // cropping via viewport src — so rescaling from it ballooned
+ // ghosts around the window at any zoom change. A frozen copy
+ // keeps its save-time dest/position; a zoom mid-transaction
+ // leaves it briefly at the old zoom, which restore corrects.
}
let scale_data = ScaleData { scale, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 3425edb..8e3a698 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2191,6 +2191,64 @@ impl WindowManager {
self.stop_panning_animation();
}
match action {
+ // Scene introspection: dump every scene buffer of a window's
+ // trees — position, dest size, natural buffer size,
+ // surface-backed or not. Found the zoom-ghost bug; kept as a
+ // debugging tool.
+ "debug-buffers" => {
+ let win = if parts.len() >= 2 {
+ self.find_window_by_query(&parts[1..].join(" "))
+ } else {
+ self.focused_window()
+ };
+ if win.is_null() {
+ return "error: no matching window\n".to_string();
+ }
+ let mut out = format!(
+ "window box=({},{},{}x{}) scale={} saved={} tree_en={} surf_en={} saved_en={}\n",
+ (*win).box_geom.x, (*win).box_geom.y, (*win).box_geom.width, (*win).box_geom.height,
+ (*win).scale,
+ (*win).surfaces.saved,
+ ffi::river_scene_node_get_enabled((*win).tree as *mut ffi::wlr_scene_node),
+ ffi::river_scene_node_get_enabled((*win).surfaces.tree as *mut ffi::wlr_scene_node),
+ ffi::river_scene_node_get_enabled((*win).surfaces.saved_tree as *mut ffi::wlr_scene_node),
+ );
+ unsafe extern "C" fn dump_iter(
+ buffer: *mut ffi::wlr_scene_buffer,
+ sx: i32,
+ sy: i32,
+ user_data: *mut std::ffi::c_void,
+ ) {
+ let out = &mut *(user_data as *mut String);
+ let node = buffer as *mut ffi::wlr_scene_node;
+ let surface = ffi::river_scene_node_get_surface(node);
+ out.push_str(&format!(
+ " buf sx={} sy={} dest={}x{} natural={}x{} surface={} enabled={}\n",
+ sx,
+ sy,
+ ffi::river_scene_buffer_get_dest_width(buffer),
+ ffi::river_scene_buffer_get_dest_height(buffer),
+ ffi::river_scene_buffer_get_width(buffer),
+ ffi::river_scene_buffer_get_height(buffer),
+ !surface.is_null(),
+ ffi::river_scene_node_get_enabled(node),
+ ));
+ }
+ for (name, node) in [
+ ("surfaces", (*win).surfaces.tree as *mut ffi::wlr_scene_node),
+ ("saved", (*win).surfaces.saved_tree as *mut ffi::wlr_scene_node),
+ ("popup", (*win).popup_tree as *mut ffi::wlr_scene_node),
+ ("whole-tree", (*win).tree as *mut ffi::wlr_scene_node),
+ ] {
+ out.push_str(&format!("[{}]\n", name));
+ ffi::wlr_scene_node_for_each_buffer(
+ node,
+ Some(dump_iter),
+ &mut out as *mut String as *mut std::ffi::c_void,
+ );
+ }
+ return out;
+ }
"status-hide-mode" => {
let enable = if parts.len() >= 2 {
match parts[1] {
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index a39fe76..e3e3313 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -898,3 +898,15 @@ int river_scene_buffer_get_height(struct wlr_scene_buffer *scene_buffer) {
}
return scene_buffer->dst_height;
}
+
+int river_scene_buffer_get_dest_width(struct wlr_scene_buffer *scene_buffer) {
+ return scene_buffer->dst_width;
+}
+
+int river_scene_buffer_get_dest_height(struct wlr_scene_buffer *scene_buffer) {
+ return scene_buffer->dst_height;
+}
+
+bool river_scene_node_get_enabled(struct wlr_scene_node *node) {
+ return node->enabled;
+}
diff --git a/wrapper.h b/wrapper.h
index 384b290..bd996e3 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -269,6 +269,9 @@ void river_scene_node_set_corner_radius(struct wlr_scene_node *node, int radius)
void river_scene_buffer_set_dest_size_if_changed(struct wlr_scene_buffer *scene_buffer, int width, int height);
int river_scene_buffer_get_width(struct wlr_scene_buffer *scene_buffer);
int river_scene_buffer_get_height(struct wlr_scene_buffer *scene_buffer);
+int river_scene_buffer_get_dest_width(struct wlr_scene_buffer *scene_buffer);
+int river_scene_buffer_get_dest_height(struct wlr_scene_buffer *scene_buffer);
+bool river_scene_node_get_enabled(struct wlr_scene_node *node);
void river_scene_node_set_position_if_changed(struct wlr_scene_node *node, int x, int y);