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

commit94e6e3fb189e1b63bdec079db82aa35b2bdc5b12
parent97a4e0c0c1
authorLucas Galante <[email protected]>
date2026-09-03 21:41
fix(grid): re-issue the grid client's patch after a config reload

cce-grid is a pure function of (patch, style config) and repaints only
when handed a patch. `reload` re-read the desktop keys into the
compositor's own layout and retiled, but update_grid_patches re-sends a
patch only when the current one no longer COVERS the viewport — and a
cell-width or color change leaves coverage untouched. So the client kept
showing the old grid until the camera happened to travel far enough to
need a fresh patch, which read as a reload that "took too long".

A window-level `grid_patch_stale` flag, set by the new
`invalidate_grid_patches` on reload and on a `layout` change to a
desktop key, makes the next arrange re-issue the patch regardless of
coverage (a covering patch already in flight is left to latch first).
Verified in a headless shadow: reload with grid_cell_width 450 -> 300
logs "sent patch #2 ... (style reload)" and the shot shows the new
width within the same second.

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

 src/server/window.rs         |  6 ++++++
 src/server/window_manager.rs | 36 ++++++++++++++++++++++++++++++++----
 2 files changed, 38 insertions(+), 4 deletions(-)

diff --git a/src/server/window.rs b/src/server/window.rs
index 30d6303..42f0a72 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -465,6 +465,11 @@ pub struct Window {
     /// The patch the CURRENT buffer covers — what arrange anchors to.
     pub grid_patch_current: Option<crate::policy::api::GridPatch>,
     pub grid_patch_serial: u32,
+    /// The current patch was rendered under a style config that has since
+    /// changed (reload, or a `layout` change to the desktop keys): re-issue
+    /// it on the next arrange even though its coverage is still fine. See
+    /// `WindowManager::invalidate_grid_patches`.
+    pub grid_patch_stale: bool,
     pub saved_floating_width: i32,
     pub saved_floating_height: i32,
     pub saved_floating_virtual_x: f64,
@@ -723,6 +728,7 @@ impl Window {
             grid_patch_acked: None,
             grid_patch_current: None,
             grid_patch_serial: 0,
+            grid_patch_stale: false,
             saved_floating_width: 0,
             saved_floating_height: 0,
             saved_floating_virtual_x: 0.0,
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 2ac5827..714be91 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2343,11 +2343,16 @@ impl WindowManager {
             if !matches!((*w).state, crate::window::WindowState::Mapped) {
                 continue;
             }
-            if (*w).grid_patch_current.as_ref().map_or(false, &covers) {
+            // A stale patch (style changed since it was rendered) is
+            // re-issued regardless of coverage; a covering patch already in
+            // flight is left to latch first, and the flag then re-sends
+            // once it has become current.
+            let stale = (*w).grid_patch_stale;
+            if !stale && (*w).grid_patch_current.as_ref().map_or(false, &covers) {
                 continue;
             }
             if let Some(cur) = &(*w).grid_patch_current {
-                explain("current", cur);
+                explain(if stale { "stale" } else { "current" }, cur);
             }
             if let Some((_, pending)) = &(*w).grid_patch_pending {
                 if covers(pending) {
@@ -2410,15 +2415,32 @@ impl WindowManager {
                 .cce_window_management
                 .send_grid_patch((*w).ref_key, serial, patch)
             {
-                log::info!("[Grid] sent patch #{serial}: {:.0},{:.0} {:.0}x{:.0} @{:.3}",
-                    patch.x, patch.y, patch.w, patch.h, patch.scale);
+                log::info!("[Grid] sent patch #{serial}: {:.0},{:.0} {:.0}x{:.0} @{:.3}{}",
+                    patch.x, patch.y, patch.w, patch.h, patch.scale,
+                    if stale { " (style reload)" } else { "" });
                 (*w).grid_patch_pending = Some((serial, patch));
+                (*w).grid_patch_stale = false;
             } else {
                 log::info!("[Grid] patch #{serial} not sent (no toplevel resource yet)");
             }
         }
     }
 
+    /// Mark every grid client's rendered patch stale so `update_grid_patches`
+    /// re-issues it on the next arrange even though its coverage is still
+    /// fine. The grid client is a pure function of (patch, style config) and
+    /// repaints only when handed a patch, so after a config reload — or a
+    /// `layout` change to a desktop key — an unmoved viewport kept showing
+    /// the OLD cell size and colors until the camera happened to travel far
+    /// enough to need a fresh patch.
+    pub unsafe fn invalidate_grid_patches(&mut self) {
+        for &w in self.windows.iter() {
+            if !w.is_null() && !(*w).closed && (*w).is_grid() {
+                (*w).grid_patch_stale = true;
+            }
+        }
+    }
+
     pub unsafe fn arrange_views(&mut self) {
         self.update_grid_patches();
         self.update_restore_placeholders();
@@ -4371,6 +4393,9 @@ impl WindowManager {
                 }
                 self.retile_for_grid_change(old_sp);
                 self.remap_saved_entries(&old_sp);
+                if key.starts_with("desktop_") || key.starts_with("grid_cell") {
+                    self.invalidate_grid_patches();
+                }
                 self.dirty_windowing();
                 "ok\n".to_string()
             }
@@ -4827,6 +4852,9 @@ impl WindowManager {
                     // reload too — closed windows must reopen on their
                     // squares, not their stale pixels.
                     self.remap_saved_entries(&old_sp);
+                    // The grid client reads the same desktop keys and only
+                    // repaints when handed a patch: hand it one.
+                    self.invalidate_grid_patches();
                     self.dirty_windowing();
 
                     // Process old PIDs