Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(xwayland): an exempt game's whole-root request is the whole screen; its unfullscreen is ignored
With saved-size restore out of the way, Trackmania asked for what its
"windowedfull" always wanted — the whole X root, 3840x2160 at (0, 0) —
and, its pixels being logical under xwayland_hidpi_except, got a window
twice the screen that the arrange pass kept recalling on-desk while the
game re-asked (~60/s). A self-request from an exempt window is now
clamped to the output's logical box and kept on it: the whole root
becomes the whole screen.
Fullscreen still undid itself: Wine syncs _NET_WM_STATE from its own
3840x2400 view of the screen and withdrew FULLSCREEN as soon as it saw
the compositor's 1920x1200 window. A client unfullscreen for an exempt
window the compositor holds fullscreen is now ignored (logged); the key
that set it clears it.
Verified in an --xwayland --scale 2 shadow with a fixed-size GTK4 X11
window named in the key: a 3840x2160@(0,0) request lands as 640x360@(0,0)
with two configure requests total; a compositor fullscreen stays
Fullscreen through the client's unfullscreen; floating returns.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/server/xwayland_window.rs | 69 ++++++++++++++++++++++++++++++++++++-------
1 file changed, 58 insertions(+), 11 deletions(-)
diff --git a/src/server/xwayland_window.rs b/src/server/xwayland_window.rs
index a8e4206..930f269 100644
--- a/src/server/xwayland_window.rs
+++ b/src/server/xwayland_window.rs
@@ -245,9 +245,11 @@ pub unsafe fn x11_scale_for_surface(
/// Whether `window` is an X11 window named in `xwayland_hidpi_except` — a
/// full-screen X11 game, by the key's definition. Such a window sizes and
/// places itself to the screen, and the compositor stays out of its way:
-/// no saved-state restore (`Window::try_restore`), its position requests
-/// are granted (`handle_request_configure`), and only a compositor
-/// fullscreen overrides its size.
+/// no saved-state restore (`Window::try_restore`); its own requests are
+/// granted, clamped to the output's logical box since it sees a
+/// physical-pixel root (`handle_request_configure`); a compositor
+/// fullscreen overrides its size and survives Wine's withdrawal of the
+/// state (`handle_request_fullscreen`).
pub unsafe fn window_is_hidpi_exempt(window: *const crate::window::Window) -> bool {
if window.is_null() {
return false;
@@ -717,16 +719,47 @@ unsafe extern "C" fn handle_request_configure(listener: *mut ffi::wl_listener, d
// The logical values below are what the window's geometry becomes, so
// handing X anything else is handing it a number this compositor
// cannot reproduce.
+ let (mut ex, mut ey, mut ew, mut eh) =
+ ((*event).x as i32, (*event).y as i32, (*event).width as i32, (*event).height as i32);
+ if exempt_self_placed {
+ // The game SEES the physical-pixel root and asks for all of it
+ // (Trackmania's windowedfull: 3840x2160 at (0, 0)), but its
+ // pixels are logical here — granted verbatim that is a window
+ // twice the screen, which the arrange pass then keeps pulling
+ // back on-desk while the game keeps asking, ~60 requests a
+ // second. So the request is answered with at most the output's
+ // logical box, kept on that output: the whole root becomes the
+ // whole screen, which is what the game meant.
+ let out = (*window).fullscreen_output();
+ if !out.is_null() {
+ let ob = (*out).sent.box_layout();
+ let (ox, oy, ow, oh) = (ob.x, ob.y, ob.width, ob.height);
+ ew = from_x11(ew, s).min(ow).max(1);
+ eh = from_x11(eh, s).min(oh).max(1);
+ ex = from_x11(ex, s).clamp(ox, (ox + ow - ew).max(ox));
+ ey = from_x11(ey, s).clamp(oy, (oy + oh - eh).max(oy));
+ ex = to_x11(ex, s);
+ ey = to_x11(ey, s);
+ ew = to_x11(ew, s);
+ eh = to_x11(eh, s);
+ if (ex, ey, ew, eh) != ((*event).x as i32, (*event).y as i32, (*event).width as i32, (*event).height as i32) {
+ log::info!(
+ "XWayland configure request: '{}' is hidpi-exempt; ({}, {}, {}x{}) clamped to the output's logical box as ({}, {}, {}x{})",
+ title, (*event).x, (*event).y, (*event).width, (*event).height, ex, ey, ew, eh,
+ );
+ }
+ }
+ }
(*xwindow).send_configure(X11Geom {
- x: snap_x11((*event).x as i32, s) as i16,
- y: snap_x11((*event).y as i32, s) as i16,
- width: snap_x11((*event).width as i32, s) as u16,
- height: snap_x11((*event).height as i32, s) as u16,
+ x: snap_x11(ex, s) as i16,
+ y: snap_x11(ey, s) as i16,
+ width: snap_x11(ew, s) as u16,
+ height: snap_x11(eh, s) as u16,
});
- let log_x = from_x11((*event).x as i32, s);
- let log_y = from_x11((*event).y as i32, s);
- let log_width = from_x11((*event).width as i32, s) as u32;
- let log_height = from_x11((*event).height as i32, s) as u32;
+ let log_x = from_x11(ex, s);
+ let log_y = from_x11(ey, s);
+ let log_width = from_x11(ew, s) as u32;
+ let log_height = from_x11(eh, s) as u32;
(*window).box_geom.x = log_x;
(*window).box_geom.y = log_y;
@@ -916,6 +949,20 @@ unsafe extern "C" fn handle_request_fullscreen(listener: *mut ffi::wl_listener,
(*(*xwindow).window).get_title_string().unwrap_or_default(),
fullscreen,
);
+ // An exempt game's pixels are logical, so the compositor's fullscreen
+ // is a 1920x1200 X window on a 3840x2400 root. Wine syncs
+ // _NET_WM_STATE from its own idea of the screen and withdraws
+ // FULLSCREEN the moment it sees a window that does not cover its
+ // root — every Fullscreen press on Trackmania was undone by this
+ // request within the frame. The user's fullscreen stands; the key that
+ // set it clears it.
+ if !fullscreen
+ && (*(*xwindow).window).is_fullscreen()
+ && window_is_hidpi_exempt((*xwindow).window)
+ {
+ log::info!("XWayland fullscreen request: ignored — the window is hidpi-exempt and fullscreen by the compositor");
+ return;
+ }
(*(*xwindow).window).wm_scheduled.fullscreen_requested = if fullscreen {
crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut())
} else {