Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix: round the backdrop blur to match the window's corner radius
river_scene_node_enable_blur now takes a corner_radius and applies it to the
standard blur node on both the create and the reuse path, so a node recreated
after a blur toggle no longer comes back square. Applying it there rather than
via river_scene_node_set_corner_radius means a blur node can never exist
without a radius — that helper scans a tree's direct children, so it silently
no-ops on the wrong tree, and was never called on the viewport-update path.
The optimized blur node sits under the standard one and cannot be rounded
(wlr_scene_optimized_blur has no radius field), so it is disabled exactly for
windows with rounded corners. All three call sites that drive a toplevel's
blur — set_rendering_state, the viewport update, and xdg_toplevel's commit —
now compute the same radius; disagreement there flipped corners between
rounded and square depending on which ran last.
CCE_BLUR_LEGACY=1 restores the previous behaviour for A/B testing from one
build; it is a temporary diagnostic.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
src/server/layer_shell.rs | 4 +++
src/server/window.rs | 72 +++++++++++++++++++++++++++++++---------
src/server/wlroots_log_wrapper.c | 15 ++++++++-
src/server/xdg_toplevel.rs | 23 ++++++++++++-
wrapper.h | 2 +-
5 files changed, 97 insertions(+), 19 deletions(-)
diff --git a/src/server/layer_shell.rs b/src/server/layer_shell.rs
index 85fcaf4..b2980db 100644
--- a/src/server/layer_shell.rs
+++ b/src/server/layer_shell.rs
@@ -629,6 +629,10 @@ unsafe extern "C" fn handle_layer_surface_commit(listener: *mut ffi::wl_listener
0,
geom_w,
geom_h,
+ // 0 preserves existing behaviour: layer surfaces (status bar, etc.) never had a
+ // blur radius applied, and their corner rounding is handled separately. Left
+ // deliberately unchanged so this fix stays scoped to toplevels.
+ 0,
);
}
diff --git a/src/server/window.rs b/src/server/window.rs
index 5d4401a..57439d4 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1829,7 +1829,33 @@ impl Window {
if is_status {
ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
}
- let use_optimized = if is_status { false } else { (*self.server).wm.layout.scenefx_optimized_blur };
+ // Hoisted above the blur setup: the blur node needs this radius, and whether
+ // the window wants rounded corners at all decides the optimized-blur question
+ // below.
+ let radius = if self.is_fullscreen() {
+ 0
+ } else if requested.circular {
+ let w = self.rendering_sent.width as i32;
+ let h = self.rendering_sent.height as i32;
+ w.min(h) / 2
+ } else if self.wm_requested.ssd || is_cce_app {
+ (*self.server).wm.layout.backplate_corner_radius
+ } else {
+ 0
+ };
+ // The optimized blur node sits UNDER the standard one and cannot be rounded
+ // (wlr_scene_optimized_blur has no radius field in the vendored scenefx), so
+ // for a window with rounded corners it would keep painting square corners
+ // underneath a correctly rounded standard blur. Trade the optimization away
+ // exactly where it would be visible, and keep it everywhere else.
+ // TEMP DIAGNOSTIC: CCE_BLUR_LEGACY=1 restores pre-fix behaviour (optimized blur
+ // always on, no radius) so both can be A/B'd from one build.
+ let legacy_blur = std::env::var_os("CCE_BLUR_LEGACY").is_some();
+ let use_optimized = if is_status || (radius > 0 && !legacy_blur) {
+ false
+ } else {
+ (*self.server).wm.layout.scenefx_optimized_blur
+ };
let toplevel_w = match self.impl_type {
WindowImpl::Toplevel(toplevel) => {
if toplevel.is_null() { 0 } else { (*toplevel).geometry.width }
@@ -1855,21 +1881,12 @@ impl Window {
0,
width,
height,
+ // width/height above are scaled to device pixels, so the radius must be too
+ // (cf. the window_background rect, which scales it the same way).
+ if legacy_blur { 0 } else { (radius as f64 * self.scale) as i32 },
);
ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, requested.opacity);
- let radius = if self.is_fullscreen() {
- 0
- } else if requested.circular {
- let w = self.rendering_sent.width as i32;
- let h = self.rendering_sent.height as i32;
- w.min(h) / 2
- } else if self.wm_requested.ssd || is_cce_app {
- (*self.server).wm.layout.backplate_corner_radius
- } else {
- 0
- };
-
ffi::river_scene_node_set_corner_radius(
self.surfaces.tree as *mut ffi::wlr_scene_node,
radius,
@@ -2218,7 +2235,26 @@ impl Window {
if is_status {
ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
}
- let use_optimized = if is_status { false } else { (*self.server).wm.layout.scenefx_optimized_blur };
+ // Same radius/optimized reasoning as set_rendering_state. Before, this path
+ // set no radius at all, so a blur node recreated during a pan came back
+ // square and stayed that way.
+ let radius = if self.is_fullscreen() {
+ 0
+ } else if requested.circular {
+ let w = self.rendering_sent.width as i32;
+ let h = self.rendering_sent.height as i32;
+ w.min(h) / 2
+ } else if self.wm_requested.ssd || is_cce_app {
+ (*self.server).wm.layout.backplate_corner_radius
+ } else {
+ 0
+ };
+ let legacy_blur = std::env::var_os("CCE_BLUR_LEGACY").is_some(); // TEMP DIAGNOSTIC
+ let use_optimized = if is_status || (radius > 0 && !legacy_blur) {
+ false
+ } else {
+ (*self.server).wm.layout.scenefx_optimized_blur
+ };
let toplevel_w = match self.impl_type {
WindowImpl::Toplevel(toplevel) => {
if toplevel.is_null() { 0 } else { (*toplevel).geometry.width }
@@ -2244,9 +2280,11 @@ impl Window {
0,
width,
height,
+ if legacy_blur { 0 } else { (radius as f64 * self.scale) as i32 },
);
} else {
- 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);
+ // 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();
@@ -3265,7 +3303,9 @@ impl Decoration {
}
let is_cce_app = app_id.starts_with("cce-");
let blur_enabled = self.rendering_requested.blur && ((*self.window).wm_requested.ssd || is_cce_app || is_status);
- ffi::river_scene_node_enable_blur(self.surfaces.tree as *mut ffi::wlr_scene_node, blur_enabled, (*server).wm.layout.scenefx_optimized_blur, ignore_transparent, 0, 0, 0, 0);
+ // Radius 0 preserves existing behaviour on the layer-surface path (see layer_shell.rs)
+ // — it never had a blur radius applied, and this fix is scoped to toplevels.
+ ffi::river_scene_node_enable_blur(self.surfaces.tree as *mut ffi::wlr_scene_node, blur_enabled, (*server).wm.layout.scenefx_optimized_blur, ignore_transparent, 0, 0, 0, 0, 0);
let scale = (*self.window).scale;
let scaled_x = (self.rendering_requested.offset_x as f64 * scale) as i32;
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index 65b36e9..27ae412 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -694,7 +694,14 @@ static void find_buffer_iterator(struct wlr_scene_buffer *buffer, int sx, int sy
}
}
-void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled, bool optimized, bool ignore_transparent, int x, int y, int width, int height) {
+// `corner_radius` is in the same (scaled, device) pixels as width/height. It is applied
+// here rather than through river_scene_node_set_corner_radius so that a blur node can
+// never exist without it: that helper looks the blur up by scanning a tree's direct
+// children, so aiming it at the wrong tree silently no-ops, and it was never called at
+// all on the viewport-update path. Note the optimized blur node cannot be rounded --
+// wlr_scene_optimized_blur has no radius field -- so callers that need rounded corners
+// must pass optimized = false.
+void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled, bool optimized, bool ignore_transparent, int x, int y, int width, int height, int corner_radius) {
if (node->type != WLR_SCENE_NODE_TREE) {
return;
}
@@ -761,6 +768,12 @@ void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled, boo
wlr_scene_blur_set_size((struct wlr_scene_blur *)std_blur_node, width, height);
}
+ // Set on both the create and the reuse path: a node reused across a resize keeps its
+ // radius, but one recreated after a blur toggle would otherwise come back square.
+ if (std_blur_node) {
+ wlr_scene_blur_set_corner_radius((struct wlr_scene_blur *)std_blur_node, corner_radius);
+ }
+
if (std_blur_node) {
wlr_scene_node_set_position(std_blur_node, x, y);
struct wlr_scene_buffer *source_buffer = NULL;
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index ca39871..98c5eb1 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -449,8 +449,27 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
let geom_h = (actual_h as f64 * scale) as i32;
let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status ||
app_id.starts_with("cce-status");
- let use_optimized = if is_status { false } else { (*(*window).server).wm.layout.scenefx_optimized_blur };
let is_cce_app = app_id.starts_with("cce-");
+ // Must mirror Window::set_rendering_state's radius exactly: both paths drive the same
+ // blur node, so if they disagree the corners flip between rounded and square depending
+ // on which one ran last.
+ let radius = if (*window).is_fullscreen() {
+ 0
+ } else if (*window).rendering_requested.circular {
+ let w = (*window).rendering_sent.width as i32;
+ let h = (*window).rendering_sent.height as i32;
+ w.min(h) / 2
+ } else if (*window).wm_requested.ssd || is_cce_app {
+ (*(*window).server).wm.layout.backplate_corner_radius
+ } else {
+ 0
+ };
+ let legacy_blur = std::env::var_os("CCE_BLUR_LEGACY").is_some(); // TEMP DIAGNOSTIC
+ let use_optimized = if is_status || (radius > 0 && !legacy_blur) {
+ false
+ } else {
+ (*(*window).server).wm.layout.scenefx_optimized_blur
+ };
let blur_enabled = (*window).rendering_requested.blur && ((*window).wm_requested.ssd || is_cce_app || is_status);
ffi::river_scene_node_enable_blur(
(*window).tree as *mut ffi::wlr_scene_node,
@@ -461,6 +480,8 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
0,
geom_w,
geom_h,
+ // geom_w/h are already scaled to device px; the radius must match.
+ if legacy_blur { 0 } else { (radius as f64 * scale) as i32 },
);
let capture_node = &mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node;
diff --git a/wrapper.h b/wrapper.h
index 10300f8..f239a86 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -258,7 +258,7 @@ struct wlr_seat_client *river_wlr_drag_get_seat_client(struct wlr_drag *drag);
void river_wlr_keyboard_init(struct wlr_keyboard *keyboard, void (*led_update)(struct wlr_keyboard *keyboard, uint32_t leds), const char *name);
-void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled, bool optimized, bool ignore_transparent, int x, int y, int width, int height);
+void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled, bool optimized, bool ignore_transparent, int x, int y, int width, int height, int corner_radius);
void river_scene_node_set_opacity(struct wlr_scene_node *node, float opacity);