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

commit7a48cfc3cce2d818123833787065d31ef361ad65
parent82bd67f9ee
authorLucas Galante <[email protected]>
date2026-09-06 17:40
fix(scene): client backgrounds paint between the grid backdrop and the cells

A wlr-layer-shell Background surface (and the cce-wallpaper window) went
into scene.layers.background, the same tree as each output's base rect and
its grid tree — and draw_grid raises the grid tree to the top of that layer
on every redraw, with the backdrop rect as the first node inside it. So a
client background was always under an opaque backdrop: mapped, enabled,
and never seen.

The background layer now has an ordered middle: `layers.background_clients`
hosts the layer-shell Background surfaces and the wallpaper window; each
output's grid backdrop moves out of grid_tree into its own
`grid_backdrop_tree`, placed just above the output's base rect (below the
clients); grid_tree keeps only the cells and rims and stays raised on top.
The backdrop tree is positioned in lockstep with grid_tree (pan shift, Solid
spec, output layout) and destroyed with it. A client wallpaper therefore
replaces the flat backdrop colour and keeps the cell lattice, which is what
the grid-client comment in window_manager.rs already described as the
intended stacking.

Verified in a shadow session on this build: with no client the desktop is
unchanged; a Background-layer surface shows through the gaps between the
cells with the cells on top; a floating window stacks above both.

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

 src/server/output.rs         | 66 ++++++++++++++++++++++++++++++++++++++++++--
 src/server/output_manager.rs | 12 ++++++++
 src/server/scene.rs          | 13 ++++++++-
 src/server/window_manager.rs |  4 ++-
 4 files changed, 90 insertions(+), 5 deletions(-)

diff --git a/src/server/output.rs b/src/server/output.rs
index fa2daae..3b90d41 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -159,6 +159,12 @@ pub struct Output {
     pub scene_output: *mut ffi::wlr_scene_output,
     pub background_rect: *mut ffi::wlr_scene_rect,
     pub grid_tree: *mut ffi::wlr_scene_tree,
+    /// The grid's backdrop (gap colour, or the Solid spec's colour), kept in
+    /// its own tree under `scene.layers.background_clients` so a client
+    /// background surface paints over it while the cells in `grid_tree` stay
+    /// on top. Positioned in lockstep with `grid_tree`.
+    pub grid_backdrop_tree: *mut ffi::wlr_scene_tree,
+    pub grid_backdrop_rect: *mut ffi::wlr_scene_rect,
     pub adjust_tree: *mut ffi::wlr_scene_tree,
     pub adjust_rects: Vec<*mut ffi::wlr_scene_rect>,
     pub last_adjust_mode: bool,
@@ -381,6 +387,11 @@ impl Output {
                         ffi::wlr_scene_node_destroy(self.grid_tree as *mut ffi::wlr_scene_node);
                         self.grid_tree = std::ptr::null_mut();
                     }
+                    if !self.grid_backdrop_tree.is_null() {
+                        ffi::wlr_scene_node_destroy(self.grid_backdrop_tree as *mut ffi::wlr_scene_node);
+                        self.grid_backdrop_tree = std::ptr::null_mut();
+                        self.grid_backdrop_rect = std::ptr::null_mut();
+                    }
                     self.grid_rect_pool.clear();
                     // The bevel subtree died with grid_tree above.
                     self.grid_bevel_pool.clear();
@@ -457,6 +468,8 @@ impl Output {
             scene_output,
             background_rect: std::ptr::null_mut(),
             grid_tree: std::ptr::null_mut(),
+            grid_backdrop_tree: std::ptr::null_mut(),
+            grid_backdrop_rect: std::ptr::null_mut(),
             adjust_tree: std::ptr::null_mut(),
             adjust_rects: Vec::new(),
             last_adjust_mode: false,
@@ -1164,9 +1177,41 @@ impl Output {
         // Enable the grid tree.
         ffi::wlr_scene_node_set_enabled(self.grid_tree as *mut ffi::wlr_scene_node, true);
 
-        // Keep the grid tree at the top of the background layer to prevent wallpaper windows from overlapping it
+        // Keep the grid tree (cells + rims) at the top of the background layer,
+        // above the client backgrounds in layers.background_clients.
         ffi::wlr_scene_node_raise_to_top(self.grid_tree as *mut ffi::wlr_scene_node);
 
+        // The backdrop goes BELOW the client backgrounds: its own tree, placed
+        // just above this output's base rect (or at the very bottom), so a
+        // layer-shell Background surface or a wallpaper window replaces the flat
+        // colour and keeps the cell lattice.
+        if self.grid_backdrop_tree.is_null() {
+            self.grid_backdrop_tree = ffi::wlr_scene_tree_create((*self.server).scene.layers.background);
+            if self.grid_backdrop_tree.is_null() {
+                return;
+            }
+            if !self.background_rect.is_null() {
+                ffi::wlr_scene_node_lower_to_bottom(self.background_rect as *mut ffi::wlr_scene_node);
+                ffi::wlr_scene_node_place_above(
+                    self.grid_backdrop_tree as *mut ffi::wlr_scene_node,
+                    self.background_rect as *mut ffi::wlr_scene_node,
+                );
+            } else {
+                ffi::wlr_scene_node_lower_to_bottom(self.grid_backdrop_tree as *mut ffi::wlr_scene_node);
+            }
+        }
+        ffi::wlr_scene_node_set_enabled(self.grid_backdrop_tree as *mut ffi::wlr_scene_node, true);
+        let backdrop_tree = self.grid_backdrop_tree;
+        let backdrop_rect = &mut self.grid_backdrop_rect;
+        let mut set_backdrop = |w: i32, h: i32, color_ptr: *const f32| {
+            if backdrop_rect.is_null() {
+                *backdrop_rect = ffi::wlr_scene_rect_create(backdrop_tree, w, h, color_ptr);
+            } else {
+                ffi::wlr_scene_rect_set_size(*backdrop_rect, w, h);
+                ffi::wlr_scene_rect_set_color(*backdrop_rect, color_ptr);
+            }
+        };
+
         let (viewport_w, viewport_h) = self.current.dimensions();
         let spec = wm.layout.background_spec();
         let zoom = crate::policy::background::sanitized_zoom(wm.desk_zoom);
@@ -1307,6 +1352,11 @@ impl Output {
                         x,
                         y,
                     );
+                    ffi::river_scene_node_set_position_if_changed(
+                        backdrop_tree as *mut ffi::wlr_scene_node,
+                        x,
+                        y,
+                    );
                 }
 
                 if force {
@@ -1314,7 +1364,7 @@ impl Output {
                     // backdrop always draws — while a grid client is live it
                     // is the safety net beyond the patch edges during fast
                     // pans; the CELLS yield to the client's rendering.
-                    get_rect(frame.backdrop_w, frame.backdrop_h, grid.gap_color.0.as_ptr(), 0, 0, 0, 0);
+                    set_backdrop(frame.backdrop_w, frame.backdrop_h, grid.gap_color.0.as_ptr());
 
                     if let Some(cells) = frame.cells.as_ref().filter(|_| wm.grid_cells_enabled) {
                         // scenefx fade-inset wire encoding: inset px * 1000
@@ -1385,8 +1435,13 @@ impl Output {
                     self.sent.x,
                     self.sent.y,
                 );
+                ffi::river_scene_node_set_position_if_changed(
+                    backdrop_tree as *mut ffi::wlr_scene_node,
+                    self.sent.x,
+                    self.sent.y,
+                );
                 if force {
-                    get_rect(viewport_w, viewport_h, color.0.as_ptr(), 0, 0, 0, 0);
+                    set_backdrop(viewport_w, viewport_h, color.0.as_ptr());
                 }
             }
         }
@@ -1550,6 +1605,11 @@ unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut
         ffi::wlr_scene_node_destroy((*output).grid_tree as *mut ffi::wlr_scene_node);
         (*output).grid_tree = std::ptr::null_mut();
     }
+    if !(*output).grid_backdrop_tree.is_null() {
+        ffi::wlr_scene_node_destroy((*output).grid_backdrop_tree as *mut ffi::wlr_scene_node);
+        (*output).grid_backdrop_tree = std::ptr::null_mut();
+        (*output).grid_backdrop_rect = std::ptr::null_mut();
+    }
 
     if !(*output).adjust_tree.is_null() {
         ffi::wlr_scene_node_destroy((*output).adjust_tree as *mut ffi::wlr_scene_node);
diff --git a/src/server/output_manager.rs b/src/server/output_manager.rs
index be1984a..fe038a8 100644
--- a/src/server/output_manager.rs
+++ b/src/server/output_manager.rs
@@ -235,6 +235,13 @@ impl OutputManager {
                                 output.sent.y,
                             );
                         }
+                        if !output.grid_backdrop_tree.is_null() {
+                            ffi::wlr_scene_node_set_position(
+                                output.grid_backdrop_tree as *mut ffi::wlr_scene_node,
+                                output.sent.x,
+                                output.sent.y,
+                            );
+                        }
 
                         if output.adjust_tree.is_null() {
                             output.adjust_tree = ffi::wlr_scene_tree_create((*server).scene.layers.top);
@@ -258,6 +265,11 @@ impl OutputManager {
                             output.grid_tree = std::ptr::null_mut();
                             output.grid_rect_pool.clear();
                         }
+                        if !output.grid_backdrop_tree.is_null() {
+                            ffi::wlr_scene_node_destroy(output.grid_backdrop_tree as *mut ffi::wlr_scene_node);
+                            output.grid_backdrop_tree = std::ptr::null_mut();
+                            output.grid_backdrop_rect = std::ptr::null_mut();
+                        }
                         if !output.adjust_tree.is_null() {
                             ffi::wlr_scene_node_destroy(output.adjust_tree as *mut ffi::wlr_scene_node);
                             output.adjust_tree = std::ptr::null_mut();
diff --git a/src/server/scene.rs b/src/server/scene.rs
index f181f53..078145f 100644
--- a/src/server/scene.rs
+++ b/src/server/scene.rs
@@ -6,6 +6,14 @@ use crate::scene_node_data::{SceneNodeData, SceneNodeDataVal};
 
 pub struct SceneLayers {
     pub background: *mut ffi::wlr_scene_tree,
+    /// Client-provided backgrounds — wlr-layer-shell Background surfaces and
+    /// the `cce-wallpaper` window — inside `background`, ABOVE each output's
+    /// base rect and grid backdrop and BELOW its grid cells (`Output::draw_grid`
+    /// keeps the backdrop under this tree and the cell tree above it). A client
+    /// wallpaper therefore replaces the flat backdrop colour and keeps the cell
+    /// lattice; before this tree the grid tree, re-raised on every redraw, buried
+    /// every client background under its opaque backdrop.
+    pub background_clients: *mut ffi::wlr_scene_tree,
     pub bottom: *mut ffi::wlr_scene_tree,
     pub wm: *mut ffi::wlr_scene_tree,
     pub top: *mut ffi::wlr_scene_tree,
@@ -42,6 +50,7 @@ impl Scene {
             locked_tree: std::ptr::null_mut(),
             layers: SceneLayers {
                 background: std::ptr::null_mut(),
+                background_clients: std::ptr::null_mut(),
                 bottom: std::ptr::null_mut(),
                 wm: std::ptr::null_mut(),
                 top: std::ptr::null_mut(),
@@ -98,6 +107,7 @@ impl Scene {
         ffi::wlr_scene_node_set_enabled(locked_tree as *mut ffi::wlr_scene_node, false);
 
         self.layers.background = ffi::wlr_scene_tree_create(normal_tree);
+        self.layers.background_clients = ffi::wlr_scene_tree_create(self.layers.background);
         self.layers.bottom = ffi::wlr_scene_tree_create(normal_tree);
         self.layers.wm = ffi::wlr_scene_tree_create(normal_tree);
         self.layers.top = ffi::wlr_scene_tree_create(normal_tree);
@@ -113,6 +123,7 @@ impl Scene {
 
         if self.layers.border_overlay.is_null()
             || self.layers.background.is_null()
+            || self.layers.background_clients.is_null()
             || self.layers.bottom.is_null()
             || self.layers.wm.is_null()
             || self.layers.top.is_null()
@@ -212,7 +223,7 @@ impl Scene {
     pub unsafe fn layer_surface_tree(&self, layer: u32) -> *mut ffi::wlr_scene_tree {
         // layer is zwlr_layer_shell_v1_layer enum values
         match layer {
-            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND => self.layers.background,
+            ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND => self.layers.background_clients,
             ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM => self.layers.bottom,
             ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_TOP => self.layers.top,
             ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY => self.layers.overlay,
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 39c329a..a52a330 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2114,7 +2114,9 @@ impl WindowManager {
                             // the next unrelated transaction reparented it (the
                             // off-screen reveal delay in overview/zoom).
                             let layer = if (*window).get_app_id_string().as_deref() == Some("cce-wallpaper") {
-                                (*self.server).scene.layers.background
+                                // Between the native backdrop and the fallback
+                                // cells, like a layer-shell Background surface.
+                                (*self.server).scene.layers.background_clients
                             } else if (*window).is_grid() {
                                 // The grid client is a desktop fixture: above
                                 // the native backdrop and fallback cells