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

commit908c709d9e81a7ea5d24bf10fdc3b9da54374fad
parent2c2f79d7e8
authorLucas Galante <[email protected]>
date2026-08-09 23:15
fix: blur transparency mask samples 1:1 over the blur box

The standard blur node stretched its transparency-mask texture (the
window surface, via ignore_transparent) across the blur rect. A mask
buffer LARGER than the blur box — a client overflow rim, where the
surface extends transparently past the xdg geometry the blur is sized
to — squeezed its transparent rim INTO the box, and ignore_transparent
then killed the backdrop blur in a rim-proportioned band at the
window right/bottom: the data-editor backplate visibly lost its frost
along the bottom/right exactly while a File dropdown (and its overflow
rim) was expanded. Diagnosed at low diff thresholds — through an 81%-
opaque plate the sharp-vs-blurred backdrop difference is under 20/255,
invisible to coarse pixel diffs.

The mask src_box now maps the blur box through the mask buffer own
geometry (1:1 in layout space, using its node coords + dest size);
congruent boxes — every mask before overflow rims existed — skip the
remap and keep the previous full-texture mapping byte-for-byte.

Verified nested A/B with a rim-forcing File menu over a grid gap line:
old binary loses the frost across the bottom band (63% of band pixels
change, the gap line pierces the plate sharply); fixed binary shows
only the expected app-state diffs. build.rs now watches the scenefx
sources — editing them previously shipped a stale static lib.

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

 build.rs                        |  6 ++++++
 scenefx/types/scene/wlr_scene.c | 40 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+)

diff --git a/build.rs b/build.rs
index 09eadd4..3280521 100644
--- a/build.rs
+++ b/build.rs
@@ -4,6 +4,12 @@ use std::path::PathBuf;
 fn main() {
     println!("cargo:rerun-if-changed=src/server/wlroots_log_wrapper.c");
     println!("cargo:rerun-if-changed=wrapper.h");
+    // Vendored scenefx sources: without these, editing a scenefx .c/.h
+    // silently ships a stale static lib (meson only reruns when build.rs
+    // does; meson compile is a fast no-op when nothing changed).
+    println!("cargo:rerun-if-changed=scenefx/types");
+    println!("cargo:rerun-if-changed=scenefx/render");
+    println!("cargo:rerun-if-changed=scenefx/include");
     println!("cargo:rerun-if-changed=protocol/river-xkb-bindings-v1.xml");
     println!("cargo:rerun-if-changed=protocol/river-layer-shell-v1.xml");
     println!("cargo:rerun-if-changed=protocol/river-input-management-v1.xml");
diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index 6aed74d..fc0f487 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -2296,6 +2296,46 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 			mask_transform = wlr_output_transform_invert(mask->transform);
 			mask_transform = wlr_output_transform_compose(mask_transform, data->transform);
 			mask_src_box = mask->src_box;
+
+			// Map the blur box through the mask buffer's own geometry instead
+			// of stretching the whole mask texture over the blur rect: a mask
+			// buffer LARGER than the blur box (a client overflow rim — the
+			// surface extends transparently past the xdg geometry the blur is
+			// sized to) would otherwise squeeze its transparent rim INTO the
+			// box, and ignore_transparent then kills the blur in a rim-scaled
+			// band at the window's right/bottom. Sample exactly the sub-rect
+			// of the mask that overlaps the blur box, 1:1 in layout space.
+			// Congruent boxes (the common case) skip this and keep the
+			// previous full-texture mapping byte-for-byte.
+			if (tex != NULL) {
+				int mask_x, mask_y, blur_x, blur_y;
+				if (wlr_scene_node_coords(&mask->node, &mask_x, &mask_y) &&
+						wlr_scene_node_coords(node, &blur_x, &blur_y)) {
+					int mask_w = 0, mask_h = 0;
+					scene_node_get_size(&mask->node, &mask_w, &mask_h);
+					if (mask_w > 0 && mask_h > 0 &&
+							(mask_w != blur->width || mask_h != blur->height ||
+								mask_x != blur_x || mask_y != blur_y)) {
+						struct wlr_fbox full = mask_src_box;
+						if (full.width <= 0 || full.height <= 0) {
+							full = (struct wlr_fbox){
+								.x = 0,
+								.y = 0,
+								.width = tex->width,
+								.height = tex->height,
+							};
+						}
+						double sx = full.width / mask_w;
+						double sy = full.height / mask_h;
+						mask_src_box = (struct wlr_fbox){
+							.x = full.x + (blur_x - mask_x) * sx,
+							.y = full.y + (blur_y - mask_y) * sy,
+							.width = blur->width * sx,
+							.height = blur->height * sy,
+						};
+					}
+				}
+			}
 		}
 
 		struct fx_corner_radii blur_corners = blur->corners;