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

commit02e905af0b1f137f2d4966529df5de51238c48e2
parentd5ad150160
authorLucas Galante <[email protected]>
date2026-08-04 20:57
fix: echo absorb also fires in the timeout recovery states

A hot echo loop drives the configure state machine into
TimedOut/TimedOutAcked (acks lag under storm load), and the absorb —
gated on Idle | Committed — disarmed there: it switched itself off at
exactly the moment it exists for. Observed live as a title-flapping
window module sustaining a 372↔456px storm (~3300 configures) at
state=TimedOutAcked, flickering its light_source neighbor.

The gate is now exclusion-based: absorb in every rest state, skip only
Inflight/Acked where a real configure is mid-flight. Absorbing leaves
timeout recovery untouched — a late ack or the next commit still walks
the state back to Idle.

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

 src/server/xdg_toplevel.rs | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index a3e2f74..c0b8018 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -250,10 +250,21 @@ impl XdgToplevel {
                 && self.geometry.height > 0
                 && echo_w == Some(self.geometry.width as u32)
                 && echo_h == Some(self.geometry.height as u32);
+            // Timeout recovery states absorb too: a hot echo loop drives the
+            // machine into TimedOut/TimedOutAcked, and an absorb that disarms
+            // there switches itself off at exactly the moment it exists for
+            // (observed live: a title-flapping window module sustained a
+            // 372↔456 storm at state=TimedOutAcked). Only Inflight/Acked stay
+            // excluded — a real configure is mid-flight there. Absorbing
+            // leaves the timeout recovery untouched: a late ack or the next
+            // commit still walks the state back to Idle.
             if size_is_echo
                 && non_size_equal
                 && bounds_ok
-                && matches!(self.configure_state, ConfigureState::Idle | ConfigureState::Committed)
+                && !matches!(
+                    self.configure_state,
+                    ConfigureState::Inflight(..) | ConfigureState::Acked
+                )
             {
                 let absorbed_bounds = scheduled.bounds;
                 (*self.window).configure_sent.width = echo_w;