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

commit43b40804fb6d80aae0342bb5532975b0f17a2d3f
parent1abe0e731e
authorLucas Galante <[email protected]>
date2026-09-15 21:53
perf(blur): keep optimized-blur bakes frozen through animated zooms

A pure pan already froze the bakes, but any zoom frame thawed them and
every optimized blur re-baked on every frame of an overview transition
— measured 34 full bakes over an 18-step zoom-out with two blurred
windows, the most expensive thing the desktop did per frame on a
3840x2400 panel. Three things conspired: the viewport relayout thawed
on zoom, the fallback grid marked every bake dirty whenever the zoom
restructured it, and each window's blur node was resized as it scaled,
which marks the bake dirty unconditionally in scenefx.

Now the freeze holds through zooms, the grid skips its dirty mark
while a gesture is in flight, and a frozen resize keeps its bake (the
anchored path bakes only strips a grown node exposes). The bakes are
sampled at the old scale for the few hundred ms of the flight — a
low-frequency blur under a window in motion — and `finish_viewport_
settle` marks them all dirty once if the zoom moved, so the settled
frame re-bakes against the final backdrop. Measured headless: 0 full
bakes in a zoom-out flight, 2 in a zoom-in (cache-margin travel), and
one per node at settle. `CCE_BLUR_DEBUG` gains an `optimized bake
full` line so bakes can be counted apart from cached samples.

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

 scenefx/types/scene/wlr_scene.c | 15 +++++++++++++++
 src/server/output.rs            |  8 ++++++--
 src/server/window_manager.rs    | 27 +++++++++++++++++++++------
 3 files changed, 42 insertions(+), 8 deletions(-)

diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index edb7376..167e782 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -1693,6 +1693,17 @@ void wlr_scene_optimized_blur_set_size(struct wlr_scene_optimized_blur *blur_nod
 	blur_node->width = width;
 	blur_node->height = height;
 
+	// While the scene is frozen (a camera gesture in flight, see
+	// wlr_scene.blur_frozen) a resize keeps its bake: the anchored render
+	// path bakes only the strips a grown node newly exposes, and a shrunk
+	// one needs nothing. Re-baking here made every frame of a zoom re-bake
+	// every blur, which the freeze exists to avoid; the thaw re-bakes once.
+	struct wlr_scene *scene = scene_node_get_root(&blur_node->node);
+	if (scene != NULL && scene->blur_frozen && blur_node->baked) {
+		scene_node_update(&blur_node->node, NULL);
+		return;
+	}
+
 	wlr_scene_optimized_blur_mark_dirty(blur_node);
 }
 
@@ -2552,6 +2563,10 @@ static void optimized_blur_render(struct wlr_scene *scene,
 	}
 
 	if (full) {
+		if (cce_scene_blur_debug()) {
+			wlr_log(WLR_INFO, "[scenefx] optimized bake full %dx%d dirty=%d baked=%d",
+				vis.width, vis.height, ob->dirty, ob->baked);
+		}
 		bool ok;
 		if (normal) {
 			ok = optimized_blur_bake_rect(fx_pass, scene, data, &vis, mx, my);
diff --git a/src/server/output.rs b/src/server/output.rs
index f32597f..c22d5d3 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -1333,8 +1333,12 @@ impl Output {
             self.grid_force_redraw_frames = 3;
             // Fallback-grid structure (spec/zoom/viewport) is backdrop
             // content in the optimized-blur capture set — same staleness
-            // rule as the client-grid latch.
-            ffi::river_scene_mark_optimized_blur_dirty((*self.server).scene.wlr_scene);
+            // rule as the client-grid latch. Not while a camera gesture
+            // holds the bakes frozen: the zoom restructures the grid every
+            // frame, and the settle re-bakes once at the end.
+            if !wm.viewport_is_active {
+                ffi::river_scene_mark_optimized_blur_dirty((*self.server).scene.wlr_scene);
+            }
         }
         let force = self.grid_force_redraw_frames > 0;
         if force {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 167d30a..fd6ca4d 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -260,6 +260,9 @@ pub struct WindowManager {
     /// than the refresh rate; the last one before a frame wins, so each
     /// frame samples the gesture once instead of relaying out per event.
     pub pinch_pending: Option<(f64, f64, f64)>,
+    /// The zoom when the current viewport gesture froze the blur bakes;
+    /// settling re-bakes only if the zoom moved away from it.
+    pub viewport_freeze_zoom: f64,
     /// An interactive move/resize has pointer motion the client has not
     /// been configured for yet. The seat op recomputes the dragged window's
     /// geometry on every pointer event (cheap, and the arrange pass reads
@@ -3548,6 +3551,9 @@ impl WindowManager {
         // stays "active" so blur is not re-enabled mid-gesture — that on/off churn
         // was the flicker of the blurred desktop grid behind transparent windows.
         if moved {
+            if !self.viewport_is_active {
+                self.viewport_freeze_zoom = self.last_viewport_zoom;
+            }
             self.viewport_is_active = true;
             // Every motion frame moves the screen-sized backdrop under every
             // blurred window; without this, scenefx re-bakes every optimized
@@ -3556,10 +3562,13 @@ impl WindowManager {
             // lives (the coordinates it last baked at), reading exactly its
             // own bake — correct, not stale, because the backdrop moved with
             // it. A zoom changes the scale under the window, which no shift
-            // can compensate: the caches thaw (re-bake per frame) for its
-            // duration and re-freeze on the next pure-pan frame.
+            // can compensate — but re-baking every blur on every frame of a
+            // zoom was the most expensive thing the desktop did, and the
+            // mismatch is a low-frequency blur under a window in flight for
+            // a few hundred ms. So the bakes stay frozen through zooms too,
+            // and `finish_viewport_settle` re-bakes once if the zoom moved.
             let scene = (*self.server).scene.wlr_scene;
-            ffi::river_scene_set_blur_frozen(scene, !zoom_changed);
+            ffi::river_scene_set_blur_frozen(scene, true);
             for &window in self.windows.iter() {
                 if !window.is_null() {
                     (*window).render_viewport_update();
@@ -3642,9 +3651,15 @@ impl WindowManager {
             return;
         }
         self.viewport_is_active = false;
-        // Thaw the blur caches (marks them all dirty once) so the settled
-        // frame re-bakes against the final backdrop.
-        ffi::river_scene_set_blur_frozen((*self.server).scene.wlr_scene, false);
+        // Thaw the blur caches so the settled frame re-bakes against the
+        // final backdrop — every bake, if the gesture changed the zoom
+        // (they were sampled at the old scale in flight); otherwise only
+        // the ones the thaw itself distrusts.
+        let scene = (*self.server).scene.wlr_scene;
+        ffi::river_scene_set_blur_frozen(scene, false);
+        if self.desk_zoom != self.viewport_freeze_zoom {
+            ffi::river_scene_mark_optimized_blur_dirty(scene);
+        }
         for &window in self.windows.iter() {
             if !window.is_null() {
                 (*window).render_finish();