Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(window): a restored window keeps its size until the client answers
Chrome came back a whole grid cell wider and taller on every login, and
the growth compounded: 3x4 cells became 4x5, then 5x6.
render_start reset the window's scheduled size from the client's
committed xdg geometry on every render pass, for every mode. Before a
client's first ack that geometry is not a response to anything — it is
the size the client asked for on its own. Chromium restores its
remembered bounds with set_window_geometry before it ever acks, and
those bounds are its window PLUS its CSD shadow insets (live: 1436x1118
for a 1404x1076 window), so they always overhang the cell block the
restore just gave it. That wish became box_geom via render_finish, the
next Tiled arrange covered every cell the overhang touched
(snap::tiled_span floors the low edge and CEILS the high one), and
save_state persisted the grown block — which the next session's insets
then overhung in turn. A one-way ratchet with the save/restore loop as
its escapement.
So adopt the client's geometry only once it has ANSWERED a configure
(XdgToplevel::acked_once, set on any ack). A window with no restored
size still seeds its block from the client's first wish, which is where
a freshly launched app's size comes from, and a client that refuses a
too-small restore is still absorbed — one round trip later, after its
ack.
Verified in a shadow against a real Chrome: a restore of 932x260 held
across three logout/login cycles (before: 500x260 -> 932x260 ->
1404x804), a fresh launch still sized itself 1404x804, and a 460x260
restore — under Chrome's 500px minimum — settled at 500x260 without
flapping.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/server/window.rs | 24 ++++++++++++++++++++++--
src/server/xdg_toplevel.rs | 11 +++++++++++
2 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/src/server/window.rs b/src/server/window.rs
index 1ab5941..004f577 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -2891,8 +2891,28 @@ impl Window {
}
_ => {}
}
- self.rendering_scheduled.width = (*toplevel).geometry.width as u32;
- self.rendering_scheduled.height = (*toplevel).geometry.height as u32;
+ // The client's committed geometry is the authority on
+ // this window's size — but only once the client has
+ // ANSWERED a configure. Before its first ack, `geometry`
+ // holds the size the client asked for on its own:
+ // Chromium restores its remembered bounds with
+ // `set_window_geometry` before it ever acks, and those
+ // bounds are its window PLUS its CSD shadow insets, so
+ // they always overhang the cell block the restore just
+ // gave it. Adopting that wish made it `box_geom` (see
+ // `render_finish`), the next Tiled arrange covered every
+ // cell the overhang touched (`snap::tiled_span` floors the
+ // low edge and CEILS the high one), the grown size was
+ // saved, and Chrome came back a whole cell wider and
+ // taller on every login — a one-way ratchet, since each
+ // session's insets sit on top of the last session's block.
+ // A window with no restored size still seeds its block
+ // from the client's first wish, which is where a freshly
+ // launched app's size comes from.
+ if !self.restored || (*toplevel).acked_once {
+ self.rendering_scheduled.width = (*toplevel).geometry.width as u32;
+ self.rendering_scheduled.height = (*toplevel).geometry.height as u32;
+ }
}
}
WindowImpl::Xwayland(xwindow) => {
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index dbeabdf..0d84279 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -24,6 +24,11 @@ pub struct XdgToplevel {
/// change detector for the deferred pointer refresh (see `handle_commit`).
pub last_surface_size: (i32, i32),
pub configure_state: ConfigureState,
+ /// Has the client ever answered a configure? Until it has,
+ /// `geometry` is the size the CLIENT asked for, not a response to one
+ /// — see `Window::render_start`, which refuses to adopt a wish over a
+ /// restored size.
+ pub acked_once: bool,
pub destroy: ffi::wl_listener,
pub ack_configure: ffi::wl_listener,
@@ -64,6 +69,7 @@ impl XdgToplevel {
geometry: std::mem::zeroed(),
last_surface_size: (0, 0),
configure_state: ConfigureState::Idle,
+ acked_once: false,
destroy: std::mem::zeroed(),
ack_configure: std::mem::zeroed(),
@@ -532,6 +538,11 @@ unsafe extern "C" fn handle_ack_configure(
let acked_configure = data as *mut ffi::wlr_xdg_surface_configure;
let serial = (*acked_configure).serial;
+ // Any ack, matching serial or not, proves the client is answering
+ // configures: from here its geometry is a response, and `render_start`
+ // may adopt it again.
+ (*toplevel).acked_once = true;
+
match (*toplevel).configure_state {
ConfigureState::Inflight(s) => {
if serial == s {