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

commit6ea3caa404fb4fc52bcc6c4bdd2f74bda974478b
parent4364e31467
authorLucas Galante <[email protected]>
date2026-08-13 15:13
fix: keep the drop shadow in step with the zoom during camera motion

render_viewport_update — the per-frame path taken by every window while
the camera pans or zooms — computed the scaled geometry and called
update_shadow only inside the 'cce-* keeps live blur' branch. Every other
window (anything in rounded_apps, e.g. claude-desktop, plus any SSD
window) fell into the else branch, which only tears the blur down, so its
shadow node kept the size, blur sigma, offset and clipped region from the
last full render_finish — i.e. from before the gesture started.

The clipped region is what makes this loud rather than subtle: it punches
the window's own box out of the shadow so the shadow only darkens the
desktop around the window. Stale at the pre-zoom scale it is far larger
than the shrunken window (2.6x at the overview's 0.38 zoom), so it eats
the entire shadow ring — the shadow vanishes for the length of the
animation and snaps back when motion settles and render_finish runs.

The blur/radius/size computation is hoisted out of the branch (it was
already correct, just gated), the cce-* test now guards only the blur
call, and update_shadow runs for every window on every motion frame. All
of update_shadow's setters are change-checked or land on a frame that
already forces a whole-output repaint, so this adds no damage.

Also extends the /tmp/cce-ovdbg dump with the shadow node's live state
(enabled/pos/size/sigma/radius/clip) for checking it against the window's
current scale.

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

 src/server/output.rs             |  1 +
 src/server/window.rs             | 41 +++++++++++++++++++++++++---------------
 src/server/wlroots_log_wrapper.c | 18 ++++++++++++++++++
 wrapper.h                        |  1 +
 4 files changed, 46 insertions(+), 15 deletions(-)

diff --git a/src/server/output.rs b/src/server/output.rs
index 53267ca..2bbad61 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -560,6 +560,7 @@ impl Output {
                     ffi::river_scene_node_get_enabled((*window).tree as *mut ffi::wlr_scene_node),
                 );
                 if let Ok(tag) = std::ffi::CString::new(app) {
+                    ffi::river_scene_shadow_dbg((*window).shadow, tag.as_ptr());
                     ffi::river_scene_ovdbg_dump(
                         (*window).tree as *mut ffi::wlr_scene_node,
                         tag.as_ptr(),
diff --git a/src/server/window.rs b/src/server/window.rs
index c840432..53e42d3 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -2482,8 +2482,11 @@ impl Window {
             // Disable backdrop blur during active viewport zoom/pan for maximum performance,
             // EXCEPT for cce-* apps, which we keep blurred during the pan so their translucent
             // backgrounds don't flicker as blur toggles on/off across motion frames.
+            // The geometry below is computed for EVERY window regardless: the drop
+            // shadow has to track the zoom even where live blur does not (see the
+            // update_shadow call at the end of the block).
             let app_id = self.get_app_id_string().unwrap_or_default();
-            if app_id.starts_with("cce-") {
+            {
                 let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
                                 app_id.starts_with("cce-status");
                 let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
@@ -2548,22 +2551,30 @@ impl Window {
                 let radius = if requested.circular { radius } else { widen_corner_radius(radius, actual_w as i32, actual_h as i32) };
                 let width = (actual_w as f64 * self.scale) as i32;
                 let height = (actual_h as f64 * self.scale) as i32;
-                ffi::river_scene_node_enable_blur(
-                    self.tree as *mut ffi::wlr_scene_node,
-                    blur_enabled,
-                    use_optimized,
-                    ignore_transparent,
-                    0,
-                    0,
-                    width,
-                    height,
-                    (radius as f64 * self.scale) as i32,
-                );
+                if app_id.starts_with("cce-") {
+                    ffi::river_scene_node_enable_blur(
+                        self.tree as *mut ffi::wlr_scene_node,
+                        blur_enabled,
+                        use_optimized,
+                        ignore_transparent,
+                        0,
+                        0,
+                        width,
+                        height,
+                        (radius as f64 * self.scale) as i32,
+                    );
+                } else {
+                    // Tearing the blur down: radius is irrelevant, the nodes are destroyed.
+                    ffi::river_scene_node_enable_blur(self.tree as *mut ffi::wlr_scene_node, false, (*self.server).wm.layout.scenefx_optimized_blur, true, 0, 0, 0, 0, 0);
+                }
+                // Every window, blurred or not: the shadow's size, blur sigma,
+                // offset and — critically — the clipped region that punches the
+                // window out of it are all scale-dependent, and nothing else on
+                // the motion path touches them. Left stale they keep the scale
+                // from before the gesture, so the punch-out overruns the shrunken
+                // window and swallows the shadow whole.
                 let want_shadow = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
                 self.update_shadow(width, height, radius, want_shadow);
-            } else {
-                // Tearing the blur down: radius is irrelevant, the nodes are destroyed.
-                ffi::river_scene_node_enable_blur(self.tree as *mut ffi::wlr_scene_node, false, (*self.server).wm.layout.scenefx_optimized_blur, true, 0, 0, 0, 0, 0);
             }
 
             self.scale_only_render_finish();
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index c1fc9bb..c946a4a 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -960,3 +960,21 @@ static void river_ovdbg_buffer_iter(struct wlr_scene_buffer *buffer,
 void river_scene_ovdbg_dump(struct wlr_scene_node *node, const char *tag) {
 	wlr_scene_node_for_each_buffer(node, river_ovdbg_buffer_iter, (void *)tag);
 }
+
+/* Overview-delay/shadow debugging: report a window's drop-shadow node state so
+ * it can be compared against the window's current zoom scale. Everything here
+ * is device px, as update_shadow writes it. */
+void river_scene_shadow_dbg(struct wlr_scene_shadow *shadow, const char *tag) {
+	if (!shadow) {
+		fprintf(stderr, "[ovdbg] %s shadow=NULL\n", tag);
+		return;
+	}
+	struct wlr_scene_node *node = &shadow->node;
+	fprintf(stderr,
+		"[ovdbg] %s shadow en=%d pos=(%d,%d) size=%dx%d sigma=%.1f radius=%d "
+		"clip=(%d,%d %dx%d)\n",
+		tag, node->enabled, node->x, node->y, shadow->width, shadow->height,
+		shadow->blur_sigma, shadow->corner_radius,
+		shadow->clipped_region.area.x, shadow->clipped_region.area.y,
+		shadow->clipped_region.area.width, shadow->clipped_region.area.height);
+}
diff --git a/wrapper.h b/wrapper.h
index 7f02cf3..ef14609 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -282,3 +282,4 @@ void river_scene_rect_set_corner_radius(struct wlr_scene_rect *rect, int radius)
 
 #endif // WRAPPER_H
 void river_scene_ovdbg_dump(struct wlr_scene_node *node, const char *tag);
+void river_scene_shadow_dbg(struct wlr_scene_shadow *shadow, const char *tag);