git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit1f5d8bd3365beb3acc697c14e9aa098664481c0e
parentcdaa5239cc
authorLucas Galante <[email protected]>
date2026-07-28 14:54
fix: force a full repaint on camera-motion frames — stale-zoom slivers

Zooming left graphical artifacts around windows (reported as "shadow
glitches" on claude — which, ssd=false, has no compositor shadow at
all): slivers of the PREVIOUS zoom level's frame surviving next to the
rescaled windows, full-size text ghosts at the edges, and idle windows
rendering entirely at the old scale. Root cause is damage
under-reporting: a camera-motion frame re-lays-out the whole screen,
but repaint regions come only from individual node changes — an idle
window's old pixels are nobody's damage, and rounding gaps between
old/new node boxes leave seams. update_viewport_local motion frames
and the viewport settle now damage the entire output via the new
river_scene_output_damage_whole wrapper (the public-field replica of
scenefx's internal scene_output_damage_whole; pending_commit_damage
lives behind the WLR_PRIVATE member). Full-screen repaint during
camera motion costs nothing — the whole screen is changing anyway.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/server/window_manager.rs     | 20 +++++++++++++++++---
 src/server/wlroots_log_wrapper.c | 18 ++++++++++++++++++
 wrapper.h                        |  1 +
 3 files changed, 36 insertions(+), 3 deletions(-)

diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 3ab35d8..3425edb 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -1671,14 +1671,22 @@ impl WindowManager {
             }
         }
 
-        // Commit outputs or schedule frame updates
+        // Commit outputs or schedule frame updates. A camera-motion frame
+        // re-lays-out the whole screen but per-node damage under-reports at
+        // the seams (stale slivers of the previous zoom level survive — an
+        // idle window's old pixels are nobody's damage), so motion forces a
+        // full repaint.
         let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
         let mut curr = (*outputs_list).next;
         while curr != outputs_list {
             let next = (*curr).next;
             let output = crate::container_of!(curr, crate::output::Output, link);
             if (*output).sent.state == crate::output::OutputStateValue::Enabled {
-                ffi::wlr_output_schedule_frame((*output).wlr_output);
+                if moved && !(*output).scene_output.is_null() {
+                    ffi::river_scene_output_damage_whole((*output).scene_output);
+                } else {
+                    ffi::wlr_output_schedule_frame((*output).wlr_output);
+                }
             }
             curr = next;
         }
@@ -1725,13 +1733,19 @@ impl WindowManager {
                 (*window).render_finish();
             }
         }
+        // Settling re-enables blur and re-finishes every window; sweep any
+        // remaining motion-frame slivers with one full repaint.
         let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
         let mut curr = (*outputs_list).next;
         while curr != outputs_list {
             let next = (*curr).next;
             let output = crate::container_of!(curr, crate::output::Output, link);
             if (*output).sent.state == crate::output::OutputStateValue::Enabled {
-                ffi::wlr_output_schedule_frame((*output).wlr_output);
+                if !(*output).scene_output.is_null() {
+                    ffi::river_scene_output_damage_whole((*output).scene_output);
+                } else {
+                    ffi::wlr_output_schedule_frame((*output).wlr_output);
+                }
             }
             curr = next;
         }
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index 3f408e8..a39fe76 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -385,6 +385,24 @@ struct wlr_surface *river_wlr_seat_get_pointer_focused_surface(struct wlr_seat *
 	return seat->pointer_state.focused_surface;
 }
 
+/* Damage the whole output and schedule a frame — the public-field replica of
+ * scenefx's internal scene_output_damage_whole(). Viewport zoom re-lays-out
+ * the entire screen, but per-node damage under-reports at the seams (stale
+ * slivers of the previous zoom level survive), so camera motion forces a
+ * full repaint. */
+void river_scene_output_damage_whole(struct wlr_scene_output *scene_output) {
+	struct wlr_output *output = scene_output->output;
+	pixman_region32_t damage;
+	pixman_region32_init_rect(&damage, 0, 0, output->width, output->height);
+	wlr_output_schedule_frame(output);
+	wlr_damage_ring_add(&scene_output->damage_ring, &damage);
+	/* pending_commit_damage lives in the WLR_PRIVATE member — reaching in is
+	 * the same deal as the pointer_state accesses elsewhere in this file. */
+	pixman_region32_union(&scene_output->WLR_PRIVATE.pending_commit_damage,
+		&scene_output->WLR_PRIVATE.pending_commit_damage, &damage);
+	pixman_region32_fini(&damage);
+}
+
 struct wl_client *river_wlr_seat_client_get_client(struct wlr_seat_client *client) {
 	return client->client;
 }
diff --git a/wrapper.h b/wrapper.h
index 4c2d2db..384b290 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -187,6 +187,7 @@ struct wl_signal *river_wlr_seat_get_start_drag_signal(struct wlr_seat *seat);
 struct wl_signal *river_wlr_seat_get_request_set_primary_selection_signal(struct wlr_seat *seat);
 struct wlr_seat_client *river_wlr_seat_get_pointer_focused_client(struct wlr_seat *seat);
 struct wlr_surface *river_wlr_seat_get_pointer_focused_surface(struct wlr_seat *seat);
+void river_scene_output_damage_whole(struct wlr_scene_output *scene_output);
 struct wl_client *river_wlr_seat_client_get_client(struct wlr_seat_client *client);
 struct wlr_keyboard *river_wlr_seat_get_keyboard(struct wlr_seat *seat);
 struct wl_global *river_wlr_seat_get_global(struct wlr_seat *seat);