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

commitfa1146b1a58169d78a15bb8b8d8ee8ac4acb082b
parent04347c02a9
authorLucas Galante <[email protected]>
date2026-09-04 11:29
perf(blur): margin cache with anchored strip bakes — no settle re-bake for windows hanging off the output

The one re-bake left in a pan was for a window partly off the output:
its bake covered only the on-screen part, so when a pan brought more of
it on screen there was nothing to sample and the thaw re-baked it whole.

The optimized-blur cache is now larger than the output by a quarter of
it on every side, and each node's bake lives at its ANCHOR — where the
node was when it fully baked, plus the margin — with a pixman region of
what the cache actually holds for it. Each frame the optimized node
bakes only the strips it now shows that its region does not cover (a
few pixels tall as a window enters the output), landing them at the
anchor inside the margin; only travel past the margin re-anchors with a
full bake. The sibling blur always samples at the anchor, frozen or
not. The cache copy sets a projection and viewport for the larger
buffer for the duration of the copy — without that, everything past
the output's extent was clipped and blur vanished beyond screen x=960
and y=540 on a 1280x720 output. The reduced-strength optimized path
(unused in cce) now blurs live rather than from a saved unblurred copy,
which is dropped along with its output-sized buffer; net memory is
+1.25 output-sized buffers.

The thaw no longer re-bakes overlapping windows either: with in-place
bakes the later bake already won the shared overlap and the lower
window read it until something below changed, and anchoring keeps
exactly that behaviour, so the flag and the walk are gone.

Headless, three overlapping blurred windows, one hanging off the right
and bottom edges: the first pan baked 21 thin strips as the hidden
bottom entered; every later pan in any direction baked zero times, with
six thin strips for the 20px off the right edge the first time that
side entered. Mid-pan and settled screenshots clean.

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

 .../render/fx_renderer/fx_offscreen_buffers.h      |   9 +-
 scenefx/include/scenefx/render/pass.h              |  10 +-
 scenefx/include/scenefx/types/wlr_scene.h          |  26 ++-
 scenefx/render/fx_renderer/fx_pass.c               |  98 +++++---
 scenefx/types/scene/wlr_scene.c                    | 252 +++++++++++++--------
 src/server/wlroots_log_wrapper.c                   |  33 +--
 6 files changed, 253 insertions(+), 175 deletions(-)

diff --git a/scenefx/include/scenefx/render/fx_renderer/fx_offscreen_buffers.h b/scenefx/include/scenefx/render/fx_renderer/fx_offscreen_buffers.h
index c3e2e02..7f0f7c8 100644
--- a/scenefx/include/scenefx/render/fx_renderer/fx_offscreen_buffers.h
+++ b/scenefx/include/scenefx/render/fx_renderer/fx_offscreen_buffers.h
@@ -12,8 +12,15 @@ struct fx_offscreen_buffers {
 	struct wl_list link; // fx_renderer.offscreen_buffers
 	struct wlr_addon addon;
 
-	// Contains the blurred background for tiled windows
+	// Contains the blurred background for tiled windows. Larger than the
+	// output by cache_margin_* on every side: a bake is stored at its
+	// node's ANCHOR (where the node was when it baked, plus the margin), and
+	// sampled shifted by the node's travel since, so a node that moves with
+	// its backdrop keeps reading its own bake; the margin gives room to bake
+	// the strips a window hanging off the output exposes as it travels,
+	// without re-anchoring the whole bake.
 	struct fx_framebuffer *optimized_blur_buffer;
+	int cache_margin_x, cache_margin_y;
 	// Contains the non-blurred background for tiled windows. Used for blurring
 	// optimized surfaces with an alpha. Just as inefficient as the regular blur.
 	struct fx_framebuffer *optimized_no_blur_buffer;
diff --git a/scenefx/include/scenefx/render/pass.h b/scenefx/include/scenefx/render/pass.h
index 2c6b00d..6ba764a 100644
--- a/scenefx/include/scenefx/render/pass.h
+++ b/scenefx/include/scenefx/render/pass.h
@@ -165,12 +165,12 @@ struct fx_render_blur_pass_options {
 	struct fx_corner_fradii corners;
 	struct clipped_fregion clipped_region;
 	/**
-	 * Buffer-px offset at which the cached optimized-blur buffer is sampled
-	 * (only when it is sampled directly, i.e. optimized and full strength).
-	 * Zero normally; the scene's blur freeze sets it to the desktop's
-	 * screen delta so a moved window keeps reading its own bake.
+	 * Cache shift, buffer px: cache position = screen position + shift.
+	 * A bake writes its blurred pixels at the shift; a sampler reads the
+	 * cache at it. The scene sets it to the node's anchor (its travel since
+	 * its bake) plus the cache margin (see fx_offscreen_buffers).
 	 */
-	int sample_offset_x, sample_offset_y;
+	int cache_shift_x, cache_shift_y;
 };
 
 struct fx_gles_render_pass *fx_get_render_pass(struct wlr_render_pass *render_pass);
diff --git a/scenefx/include/scenefx/types/wlr_scene.h b/scenefx/include/scenefx/types/wlr_scene.h
index 794862b..a570801 100644
--- a/scenefx/include/scenefx/types/wlr_scene.h
+++ b/scenefx/include/scenefx/types/wlr_scene.h
@@ -281,21 +281,23 @@ struct wlr_scene_optimized_blur {
 
 	/**
 	 * Where the node's bake lives in the shared per-output cache: the
-	 * layout coordinates it had, and the output it rendered to, when it
-	 * last baked. The sibling wlr_scene_blur samples the cache shifted by
-	 * the node's travel since then, so a node that moved with its backdrop
-	 * (a desktop pan) keeps reading exactly its own bake — per node, so
-	 * each window's own pixel rounding is honored — and a bake that re-runs
-	 * simply re-anchors. `baked_full` records that the whole box lay inside
-	 * the output, i.e. the bake covers the node wherever it travels; a
-	 * partial bake is re-baked once the scene thaws. `overwritten` is set
-	 * when a later bake landed on this bake's cache region (overlapping
-	 * windows), which also earns a re-bake at thaw.
+	 * layout coordinates it had (its ANCHOR) and the output it rendered to
+	 * when it last fully baked, plus `baked_region` — the layout-space
+	 * region, in anchor space, the cache actually holds for it. The sibling
+	 * wlr_scene_blur samples the cache shifted by the node's travel since
+	 * the anchor, so a node that moved with its backdrop (a desktop pan)
+	 * keeps reading exactly its own bake, per node. As the node travels,
+	 * any part it shows that the region does not cover (a window hanging
+	 * off the output, entering) is baked as a strip into the cache margin
+	 * and added to the region; only travel past the margin re-anchors.
+	 * Overlapping blurred windows share cache space and the later bake
+	 * wins there, exactly as with in-place bakes: the lower window's
+	 * overlap region reads the upper window's bake until something below
+	 * it changes.
 	 */
 	bool baked;
 	int baked_x, baked_y;
-	bool baked_full;
-	bool overwritten;
+	pixman_region32_t baked_region;
 	struct wlr_scene_output *baked_output;
 };
 
diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index 78e2319..eec7c9d 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -37,6 +37,10 @@ static bool cce_blur_debug(void) {
 	return enabled;
 }
 
+static void read_to_buffer_offset(struct fx_gles_render_pass *pass,
+		pixman_region32_t *_region, struct fx_framebuffer *dst_buffer,
+		struct fx_framebuffer *src_buffer, int ox, int oy);
+
 struct fx_render_texture_options fx_render_texture_options_default(
 		const struct wlr_render_texture_options *base) {
 	struct fx_render_texture_options options = {
@@ -92,10 +96,15 @@ bool fx_render_pass_init_offscreen_buffers(struct wlr_render_pass *render_pass,
 			&pass->fx_offscreen_buffers->effects_buffer, &failed);
 	fx_framebuffer_get_or_create_custom(renderer, output->allocator, width, height, true,
 			&pass->fx_offscreen_buffers->effects_buffer_swapped, &failed);
-	fx_framebuffer_get_or_create_custom(renderer, output->allocator, width, height, false,
+	// The optimized-blur cache carries a quarter-output margin on every
+	// side (see fx_offscreen_buffers.cache_margin_*).
+	const int margin_x = width / 4;
+	const int margin_y = height / 4;
+	pass->fx_offscreen_buffers->cache_margin_x = margin_x;
+	pass->fx_offscreen_buffers->cache_margin_y = margin_y;
+	fx_framebuffer_get_or_create_custom(renderer, output->allocator,
+			width + 2 * margin_x, height + 2 * margin_y, false,
 			&pass->fx_offscreen_buffers->optimized_blur_buffer, &failed);
-	fx_framebuffer_get_or_create_custom(renderer, output->allocator, width, height, false,
-			&pass->fx_offscreen_buffers->optimized_no_blur_buffer, &failed);
 
 	// Bind back to the default buffer
 	fx_framebuffer_bind(pass->buffer);
@@ -1340,15 +1349,11 @@ void fx_render_pass_add_blur(struct fx_gles_render_pass *pass,
 	TRACY_ZONE_TEXT_f("Optimized Blur Successfully Used: %d",
 			buffer && fx_options->use_optimized_blur);
 	if (!fx_options->use_optimized_blur || has_strength) {
-		// Render the blur into its own buffer
+		// Render the blur into its own buffer (a reduced-strength blur of
+		// an optimized node too: the cache holds only the full-strength
+		// bake, at its node's anchor).
 		struct fx_render_blur_pass_options blur_options = *fx_options;
-		if (fx_options->use_optimized_blur && has_strength) {
-			// Re-blur the saved non-blurred version of the optimized blur.
-			// Isn't as efficient as just using the optimized blur buffer
-			blur_options.current_buffer = pass->fx_offscreen_buffers->optimized_no_blur_buffer;
-		} else {
-			blur_options.current_buffer = pass->buffer;
-		}
+		blur_options.current_buffer = pass->buffer;
 		buffer = get_main_buffer_blur(pass, &blur_options);
 	}
 	if (!buffer) {
@@ -1370,13 +1375,14 @@ void fx_render_pass_add_blur(struct fx_gles_render_pass *pass,
 		stencil_mask_close(true);
 	}
 
-	// Draw the blurred texture. Sampled straight from the shared cache
-	// (optimized, full strength) it may be shifted by the scene's freeze
-	// offset; a freshly blurred buffer is always in place.
+	// Draw the blurred texture. The shared cache (optimized, full strength)
+	// is drawn so that screen pixel s reads cache pixel s + shift — the
+	// node's bake at its anchor, inside the margin; a freshly blurred
+	// buffer is output-sized and in place.
 	const bool direct_cache = fx_options->use_optimized_blur && !has_strength;
 	tex_options->base.dst_box = (struct wlr_box) {
-		.x = direct_cache ? fx_options->sample_offset_x : 0,
-		.y = direct_cache ? fx_options->sample_offset_y : 0,
+		.x = direct_cache ? -fx_options->cache_shift_x : 0,
+		.y = direct_cache ? -fx_options->cache_shift_y : 0,
 		.width = buffer->buffer->width,
 		.height = buffer->buffer->height,
 	};
@@ -1441,10 +1447,11 @@ bool fx_render_pass_add_optimized_blur(struct fx_gles_render_pass *pass,
 	TRACY_ZONE_TEXT_f("\tSaturation: %f", fx_options->blur_data->saturation);
 	push_fx_debug(renderer);
 
-	// The actual cache re-bake (rare: a healthy session bakes only at startup).
+	// The actual cache bake (a whole node, or a strip it newly exposes).
 	if (cce_blur_debug()) {
-		wlr_log(WLR_INFO, "[scenefx] add_optimized_blur dst_box: %dx%d at (%d, %d)",
-				dst_box.width, dst_box.height, dst_box.x, dst_box.y);
+		wlr_log(WLR_INFO, "[scenefx] add_optimized_blur dst_box: %dx%d at (%d, %d) shift (%d, %d)",
+				dst_box.width, dst_box.height, dst_box.x, dst_box.y,
+				fx_options->cache_shift_x, fx_options->cache_shift_y);
 	}
 
 	pixman_region32_t clip;
@@ -1457,13 +1464,10 @@ bool fx_render_pass_add_optimized_blur(struct fx_gles_render_pass *pass,
 	blur_options.tex_options.base.clip = &clip;
 	struct fx_framebuffer *fx_buffer = get_main_buffer_blur(pass, &blur_options);
 	if (fx_buffer != NULL) {
-		// Render the newly blurred content into the blur_buffer
-		fx_render_pass_read_to_buffer(pass, &clip,
-				pass->fx_offscreen_buffers->optimized_blur_buffer, fx_buffer);
-
-		// Save the current scene pass state
-		fx_render_pass_read_to_buffer(pass, &clip,
-				pass->fx_offscreen_buffers->optimized_no_blur_buffer, pass->buffer);
+		// Land the newly blurred content in the cache at the node's anchor.
+		read_to_buffer_offset(pass, &clip,
+				pass->fx_offscreen_buffers->optimized_blur_buffer, fx_buffer,
+				fx_options->cache_shift_x, fx_options->cache_shift_y);
 	}
 
 	pixman_region32_fini(&clip);
@@ -1473,9 +1477,10 @@ bool fx_render_pass_add_optimized_blur(struct fx_gles_render_pass *pass,
 	return fx_buffer != NULL;
 }
 
-void fx_render_pass_read_to_buffer(struct fx_gles_render_pass *pass,
+/* Copy `_region` of src_buffer into dst_buffer, landing at (+ox, +oy). */
+static void read_to_buffer_offset(struct fx_gles_render_pass *pass,
 		pixman_region32_t *_region, struct fx_framebuffer *dst_buffer,
-		struct fx_framebuffer *src_buffer) {
+		struct fx_framebuffer *src_buffer, int ox, int oy) {
 	if (!_region || !pixman_region32_not_empty(_region)) {
 		return;
 	}
@@ -1484,6 +1489,7 @@ void fx_render_pass_read_to_buffer(struct fx_gles_render_pass *pass,
 	pixman_region32_t region;
 	pixman_region32_init(&region);
 	pixman_region32_copy(&region, _region);
+	pixman_region32_translate(&region, ox, oy);
 
 	struct wlr_texture *src_tex =
 		fx_texture_from_buffer(&pass->buffer->renderer->wlr_renderer, src_buffer->buffer);
@@ -1491,18 +1497,34 @@ void fx_render_pass_read_to_buffer(struct fx_gles_render_pass *pass,
 		goto done;
 	}
 
-	// Draw onto the dst_buffer
+	// Draw onto the dst_buffer. The pass's projection and viewport are the
+	// output's; a larger destination (the margin cache) needs its own for
+	// the duration of the copy, or everything past the output's extent is
+	// clipped away.
+	const int dst_w = dst_buffer->buffer->width;
+	const int dst_h = dst_buffer->buffer->height;
+	const int pass_w = pass->buffer->buffer->width;
+	const int pass_h = pass->buffer->buffer->height;
+	const bool resize = dst_w != pass_w || dst_h != pass_h;
+	float saved_proj[9];
+	if (resize) {
+		memcpy(saved_proj, pass->projection_matrix, sizeof(saved_proj));
+		matrix_projection(pass->projection_matrix, dst_w, dst_h, WL_OUTPUT_TRANSFORM_FLIPPED_180);
+	}
 	fx_framebuffer_bind(dst_buffer);
+	if (resize) {
+		glViewport(0, 0, dst_w, dst_h);
+	}
 	wlr_render_pass_add_texture(&pass->base, &(struct wlr_render_texture_options) {
 		.texture = src_tex,
 		.clip = &region,
 		.transform = WL_OUTPUT_TRANSFORM_NORMAL,
 		.blend_mode = WLR_RENDER_BLEND_MODE_NONE,
 		.dst_box = (struct wlr_box){
-			.x = 0,
-			.y = 0,
-			.width = dst_buffer->buffer->width,
-			.height = dst_buffer->buffer->height,
+			.x = ox,
+			.y = oy,
+			.width = src_buffer->buffer->width,
+			.height = src_buffer->buffer->height,
 		},
 		.src_box = (struct wlr_fbox){
 			.x = 0,
@@ -1515,6 +1537,10 @@ void fx_render_pass_read_to_buffer(struct fx_gles_render_pass *pass,
 
 	// Bind back to the main WLR buffer
 	fx_framebuffer_bind(pass->buffer);
+	if (resize) {
+		memcpy(pass->projection_matrix, saved_proj, sizeof(saved_proj));
+		glViewport(0, 0, pass_w, pass_h);
+	}
 
 done:
 	TRACY_BOTH_ZONES_END;
@@ -1522,6 +1548,12 @@ done:
 	pixman_region32_fini(&region);
 }
 
+void fx_render_pass_read_to_buffer(struct fx_gles_render_pass *pass,
+		pixman_region32_t *_region, struct fx_framebuffer *dst_buffer,
+		struct fx_framebuffer *src_buffer) {
+	read_to_buffer_offset(pass, _region, dst_buffer, src_buffer, 0, 0);
+}
+
 static const char *reset_status_str(GLenum status) {
 	switch (status) {
 	case GL_GUILTY_CONTEXT_RESET_KHR:
diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index d5f8a6c..85906c8 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -191,6 +191,8 @@ void wlr_scene_node_destroy(struct wlr_scene_node *node) {
 		assert(wl_list_empty(&scene_buffer->events.outputs_update.listener_list));
 		assert(wl_list_empty(&scene_buffer->events.output_sample.listener_list));
 		assert(wl_list_empty(&scene_buffer->events.frame_done.listener_list));
+	} else if (node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
+		pixman_region32_fini(&wlr_scene_optimized_blur_from_node(node)->baked_region);
 	} else if (node->type == WLR_SCENE_NODE_TREE) {
 		struct wlr_scene_tree *scene_tree = wlr_scene_tree_from_node(node);
 
@@ -1668,6 +1670,7 @@ struct wlr_scene_optimized_blur *wlr_scene_optimized_blur_create(
 
 	scene_blur->width = width;
 	scene_blur->height = height;
+	pixman_region32_init(&scene_blur->baked_region);
 	// Start dirty so the first render pass bakes the cache; scene_entry_render
 	// only re-bakes when dirty (re-baking on undamaged frames samples stale
 	// pass->buffer content, ghosting whatever was composited above the node).
@@ -2445,34 +2448,145 @@ static float get_luminance_multiplier(const struct wlr_color_luminances *src_lum
 	return (dst_lum->reference / src_lum->reference) * (src_lum->max / dst_lum->max);
 }
 
-static void optimized_blur_flag_overwritten_rec(struct wlr_scene_node *node,
-		struct wlr_scene_optimized_blur *self, const struct wlr_box *box) {
-	if (node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
-		struct wlr_scene_optimized_blur *other = wlr_scene_optimized_blur_from_node(node);
-		if (other != self && other->baked && other->baked_output == self->baked_output) {
-			struct wlr_box theirs = {
-				.x = other->baked_x, .y = other->baked_y,
-				.width = other->width, .height = other->height,
+/* Bake the content below the node within one screen-space layout rect into
+ * the cache, landing at `shift` (buffer px added to the screen buffer
+ * coordinates: the node's anchor plus the cache margin). */
+static bool optimized_blur_bake_rect(struct fx_gles_render_pass *fx_pass,
+		struct wlr_scene *scene, const struct render_data *data,
+		const struct wlr_box *layout_rect, int shift_x, int shift_y) {
+	struct wlr_box buf = {
+		.x = (int)round((layout_rect->x - data->logical.x) * data->scale),
+		.y = (int)round((layout_rect->y - data->logical.y) * data->scale),
+		.width = (int)round(layout_rect->width * data->scale),
+		.height = (int)round(layout_rect->height * data->scale),
+	};
+	if (buf.width <= 0 || buf.height <= 0) {
+		return true;
+	}
+	const float opacity = 1.0f;
+	struct fx_render_blur_pass_options blur_options = {
+		.tex_options = {
+			.base = {
+				.transform = WL_OUTPUT_TRANSFORM_NORMAL,
+				.alpha = &opacity,
+				.blend_mode = WLR_RENDER_BLEND_MODE_NONE,
+				.dst_box = buf,
+			},
+			.clip_box = &buf,
+			.discard_transparent = false,
+			.clipped_region = {0}
+		},
+		.blur_data = &scene->blur_data,
+		.blur_strength = 1.0f,
+		.cache_shift_x = shift_x,
+		.cache_shift_y = shift_y,
+	};
+	return fx_render_pass_add_optimized_blur(fx_pass, &blur_options);
+}
+
+/* The optimized node's per-frame work: a full bake (anchored where the node
+ * is now) when dirty, never baked, baked elsewhere, or travelled past the
+ * cache margin; otherwise only the strips the node now shows that its bake
+ * does not cover, landed at its existing anchor. */
+static void optimized_blur_render(struct wlr_scene *scene,
+		struct wlr_scene_optimized_blur *ob, struct wlr_scene_node *node,
+		struct fx_gles_render_pass *fx_pass, const struct render_data *data,
+		struct wlr_box dst_box) {
+	struct fx_offscreen_buffers *fbos = fx_pass->fx_offscreen_buffers;
+	if (fbos == NULL) {
+		return;
+	}
+	const int mx = fbos->cache_margin_x, my = fbos->cache_margin_y;
+	const bool normal = data->transform == WL_OUTPUT_TRANSFORM_NORMAL;
+	int cx, cy;
+	if (!wlr_scene_node_coords(node, &cx, &cy)) {
+		return;
+	}
+	struct wlr_box cur = { .x = cx, .y = cy, .width = ob->width, .height = ob->height };
+	struct wlr_box vis;
+	if (!wlr_box_intersection(&vis, &cur, &data->logical)) {
+		return;
+	}
+
+	bool full = ob->dirty || !ob->baked || ob->baked_output != data->output || !normal;
+	int dx = 0, dy = 0; // anchor-space shift, layout px: anchor - current
+	if (!full) {
+		dx = ob->baked_x - cx;
+		dy = ob->baked_y - cy;
+		double mlx = mx / data->scale, mly = my / data->scale;
+		if (vis.x + dx < data->logical.x - mlx || vis.y + dy < data->logical.y - mly ||
+				vis.x + dx + vis.width > data->logical.x + data->logical.width + mlx ||
+				vis.y + dy + vis.height > data->logical.y + data->logical.height + mly) {
+			full = true;
+		}
+	}
+
+	if (full) {
+		bool ok;
+		if (normal) {
+			ok = optimized_blur_bake_rect(fx_pass, scene, data, &vis, mx, my);
+		} else {
+			// Transformed output: the whole node in its buffer-space box,
+			// in place (no anchoring on rotated outputs).
+			const float opacity = 1.0f;
+			enum wl_output_transform transform = wlr_output_transform_invert(data->transform);
+			transform = wlr_output_transform_compose(transform, data->transform);
+			struct fx_render_blur_pass_options blur_options = {
+				.tex_options = {
+					.base = {
+						.transform = transform,
+						.alpha = &opacity,
+						.blend_mode = WLR_RENDER_BLEND_MODE_NONE,
+						.dst_box = dst_box,
+					},
+					.clip_box = &dst_box,
+					.discard_transparent = false,
+					.clipped_region = {0}
+				},
+				.blur_data = &scene->blur_data,
+				.blur_strength = 1.0f,
+				.cache_shift_x = mx,
+				.cache_shift_y = my,
 			};
-			struct wlr_box overlap;
-			if (wlr_box_intersection(&overlap, &theirs, box)) {
-				other->overwritten = true;
-			}
+			ok = fx_render_pass_add_optimized_blur(fx_pass, &blur_options);
+		}
+		if (!ok) {
+			return;
 		}
+		ob->dirty = false;
+		ob->baked = true;
+		ob->baked_x = cx;
+		ob->baked_y = cy;
+		ob->baked_output = data->output;
+		pixman_region32_fini(&ob->baked_region);
+		pixman_region32_init_rect(&ob->baked_region, vis.x, vis.y, vis.width, vis.height);
 		return;
 	}
-	if (node->type == WLR_SCENE_NODE_TREE) {
-		struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node);
-		struct wlr_scene_node *child;
-		wl_list_for_each(child, &tree->children, link) {
-			optimized_blur_flag_overwritten_rec(child, self, box);
+
+	// Anchored: what the node shows now, in anchor space, minus what the
+	// cache already holds for it.
+	pixman_region32_t need, uncovered;
+	pixman_region32_init_rect(&need, vis.x + dx, vis.y + dy, vis.width, vis.height);
+	pixman_region32_init(&uncovered);
+	pixman_region32_subtract(&uncovered, &need, &ob->baked_region);
+	int n = 0;
+	const pixman_box32_t *rects = pixman_region32_rectangles(&uncovered, &n);
+	const int shift_x = (int)round(dx * data->scale) + mx;
+	const int shift_y = (int)round(dy * data->scale) + my;
+	for (int i = 0; i < n; i++) {
+		// The strip in screen space (where its content is being composited
+		// this frame); it lands in the cache at the node's anchor.
+		struct wlr_box r = {
+			.x = rects[i].x1 - dx, .y = rects[i].y1 - dy,
+			.width = rects[i].x2 - rects[i].x1, .height = rects[i].y2 - rects[i].y1,
+		};
+		if (optimized_blur_bake_rect(fx_pass, scene, data, &r, shift_x, shift_y)) {
+			pixman_region32_union_rect(&ob->baked_region, &ob->baked_region,
+				rects[i].x1, rects[i].y1, r.width, r.height);
 		}
 	}
-}
-
-static void optimized_blur_flag_overwritten(struct wlr_scene *scene,
-		struct wlr_scene_optimized_blur *self, const struct wlr_box *box) {
-	optimized_blur_flag_overwritten_rec(&scene->tree.node, self, box);
+	pixman_region32_fini(&need);
+	pixman_region32_fini(&uncovered);
 }
 
 static void scene_entry_render(struct render_list_entry *entry, const struct render_data *data) {
@@ -2792,54 +2906,13 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 		break;
 	case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
 		struct wlr_scene_optimized_blur *scene_blur = wlr_scene_optimized_blur_from_node(node);
-		// Re-render the optimized blur buffer when needed. Retry rendering
-		// until there's a visible blur_node. The dirty gate is load-bearing:
-		// mark_dirty damages the node's whole box, so pass->buffer holds fresh
-		// below-node content when the re-bake samples it. Without the gate the
-		// re-bake fires on frames whose damage only grazes the box and blurs
-		// stale pass->buffer pixels — including this very surface composited
-		// above — baking ghosts into the shared cache.
-		if (fx_pass->has_blur && is_scene_blur_enabled(&scene->blur_data)
-				&& scene_blur->dirty) {
-			const float opacity = 1.0f;
-			enum wl_output_transform transform =
-				wlr_output_transform_invert(data->transform);
-			transform = wlr_output_transform_compose(transform, data->transform);
-			struct fx_render_blur_pass_options blur_options = {
-				.tex_options = {
-					.base = {
-						.transform = transform,
-						.alpha = &opacity,
-						.blend_mode = WLR_RENDER_BLEND_MODE_NONE,
-						.dst_box = dst_box,
-					},
-					.clip_box = &dst_box,
-					.discard_transparent = false,
-					.clipped_region = {0}
-				},
-				.blur_data = &scene->blur_data,
-				.blur_strength = 1.0f,
-			};
-			bool result = fx_render_pass_add_optimized_blur(fx_pass, &blur_options);
-			if (result) {
-				scene_blur->dirty = false;
-				int bx, by;
-				if (wlr_scene_node_coords(node, &bx, &by)) {
-					struct wlr_box box = {
-						.x = bx, .y = by,
-						.width = scene_blur->width, .height = scene_blur->height,
-					};
-					scene_blur->baked = true;
-					scene_blur->baked_x = bx;
-					scene_blur->baked_y = by;
-					scene_blur->baked_output = data->output;
-					scene_blur->baked_full = wlr_box_contains_box(&data->logical, &box);
-					scene_blur->overwritten = false;
-					// Any other bake this one landed on top of in the cache
-					// now reads wrong under its owner: flag it for the thaw.
-					optimized_blur_flag_overwritten(scene, scene_blur, &box);
-				}
-			}
+		// Bake (or top up) this node's cache entry. The dirty gate on a full
+		// bake is load-bearing: mark_dirty damages the node's whole box, so
+		// pass->buffer holds fresh below-node content when the bake samples
+		// it; strips are baked only where the node newly shows content, in
+		// the frame that composites it.
+		if (fx_pass->has_blur && is_scene_blur_enabled(&scene->blur_data)) {
+			optimized_blur_render(scene, scene_blur, node, fx_pass, data, dst_box);
 		}
 		break;
 	case WLR_SCENE_NODE_BLUR:;
@@ -2901,14 +2974,16 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 		struct fx_corner_radii blur_corners = blur->corners;
 		fx_corner_radii_transform(node_transform, &blur_corners);
 
-		// Sample the shared cache where this node's own bake lives — the
-		// sibling optimized node's coordinates at its last bake — rather
-		// than at the node's current position: the cache is drawn shifted
-		// by the node's travel since the bake, so screen pixel s shows
-		// cache pixel s - travel, the bake's own pixel for that spot. Only
-		// a bake made on THIS output and covering the whole node is usable
-		// that way; any other (while the scene is not frozen) is re-baked.
-		int freeze_dx = 0, freeze_dy = 0;
+		// Sample the shared cache at this node's own bake: the sibling
+		// optimized node's anchor (its coordinates at its last full bake)
+		// plus the cache margin. The optimized node, rendered just before
+		// this one, has already baked whatever the node newly shows.
+		int cache_mx = 0, cache_my = 0;
+		if (fx_pass->fx_offscreen_buffers) {
+			cache_mx = fx_pass->fx_offscreen_buffers->cache_margin_x;
+			cache_my = fx_pass->fx_offscreen_buffers->cache_margin_y;
+		}
+		int shift_x = cache_mx, shift_y = cache_my;
 		if (blur->should_only_blur_bottom_layer &&
 				data->transform == WL_OUTPUT_TRANSFORM_NORMAL && node->parent) {
 			struct wlr_scene_node *sib;
@@ -2918,21 +2993,10 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 				}
 				struct wlr_scene_optimized_blur *opt = wlr_scene_optimized_blur_from_node(sib);
 				int cur_x, cur_y;
-				if (!wlr_scene_node_coords(sib, &cur_x, &cur_y)) {
-					break;
-				}
-				bool moved = opt->baked && (cur_x != opt->baked_x || cur_y != opt->baked_y);
 				if (opt->baked && opt->baked_output == data->output &&
-						(opt->baked_full || scene->blur_frozen)) {
-					// Anchored: a full bake wherever it went; a partial one
-					// only while frozen (its covered part stays right, and
-					// the thaw re-bakes it).
-					freeze_dx = cur_x - opt->baked_x;
-					freeze_dy = cur_y - opt->baked_y;
-				} else if (!scene->blur_frozen && moved && !opt->dirty) {
-					// Unfrozen, moved since a bake that cannot follow it
-					// (partial, or from another output): bake once here.
-					wlr_scene_optimized_blur_mark_dirty(opt);
+						wlr_scene_node_coords(sib, &cur_x, &cur_y)) {
+					shift_x += (int)round((opt->baked_x - cur_x) * data->scale);
+					shift_y += (int)round((opt->baked_y - cur_y) * data->scale);
 				}
 				break;
 			}
@@ -2962,8 +3026,8 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 			// (layout px -> buffer px), so it reads its own bake. Only
 			// meaningful for an untransformed output; a rotated one gets
 			// the unshifted cache.
-			.sample_offset_x = (int)round(freeze_dx * data->scale),
-			.sample_offset_y = (int)round(freeze_dy * data->scale),
+			.cache_shift_x = shift_x,
+			.cache_shift_y = shift_y,
 		};
 		fx_render_pass_add_blur(fx_pass, &blur_options);
 		break;
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index 1e9ce4a..c1ebb03 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -1086,37 +1086,10 @@ void river_scene_mark_optimized_blur_dirty(struct wlr_scene *scene) {
  * invalidation for the duration of a camera pan. Thawing marks every
  * optimized blur dirty once so the settled frame re-bakes against the
  * final backdrop. */
-/* Thaw: re-bake only what a pan could have left untrustworthy — a bake
- * that never happened, one that did not cover the whole node (it lay
- * partly off the output), or one another bake landed on top of. Everything
- * else keeps sampling its own bake at its travel, exactly as it did
- * mid-pan. */
-static void thaw_optimized_blur_rec(struct wlr_scene_node *node) {
-	if (node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
-		struct wlr_scene_optimized_blur *opt = wlr_scene_optimized_blur_from_node(node);
-		if (!opt->baked || !opt->baked_full || opt->overwritten) {
-			wlr_scene_optimized_blur_mark_dirty(opt);
-		}
-		return;
-	}
-	if (node->type == WLR_SCENE_NODE_TREE) {
-		struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node);
-		struct wlr_scene_node *child;
-		wl_list_for_each(child, &tree->children, link) {
-			thaw_optimized_blur_rec(child);
-		}
-	}
-}
-
 /* See wlr_scene.blur_frozen. Blurs sample the shared cache where their own
- * bake lives, frozen or not, so a pure pan needs no re-bake at either end;
- * the thaw only re-bakes what cannot be trusted. */
+ * bake lives, frozen or not, and bake the strips they newly expose as they
+ * travel, so a pan needs no re-bake at either end: the thaw is just the
+ * flag. */
 void river_scene_set_blur_frozen(struct wlr_scene *scene, bool frozen) {
-	if (scene->blur_frozen == frozen) {
-		return;
-	}
 	scene->blur_frozen = frozen;
-	if (!frozen) {
-		thaw_optimized_blur_rec(&scene->tree.node);
-	}
 }