Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(xwayland): a hidpi-exempt game keeps its own size and takes a fullscreen
Trackmania could not be made fullscreen: every Fullscreen press undid
itself within the frame. The chain: saved state restored the launcher-
sized 1214x689 onto the game's window at map, the game (windowedfull)
pinned that size in fixed hints, and on the fullscreen configure it
re-requested 1214x690 — which the floating rule granted, shrinking the X
window back, so Wine no longer saw a screen-sized rect and withdrew the
_NET_WM_STATE_FULLSCREEN the compositor had set. Meanwhile its request
for the desktop origin was refused ~170 times a second.
A window named in xwayland_hidpi_except is now treated as the
full-screen game the key is for (`window_is_hidpi_exempt`): try_restore
skips it (it sizes itself), handle_request_configure grants its
position like a transient's and moves the virtual origin with it, and a
FULLSCREEN window of any kind holds its size against a client's smaller
request, like a tiled one. handle_request_fullscreen logs.
Verified in an --xwayland --scale 2 shadow with a fixed-size GTK4 X11
window named in the key: a seeded 300x200 saved state is skipped (maps
at 800x500), a self-move to (0,0) is granted with one configure, a
compositor fullscreen holds 640x360 against the client's 800x500
re-request and stays Fullscreen, and floating returns it to 800x500.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/server/config.rs | 9 ++++++
src/server/window.rs | 13 ++++++++
src/server/xwayland_window.rs | 75 +++++++++++++++++++++++++++++++++++++------
3 files changed, 88 insertions(+), 9 deletions(-)
diff --git a/src/server/config.rs b/src/server/config.rs
index e5d55b8..6f88473 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -415,6 +415,15 @@ pub struct WindowManagerConfig {
/// window's WM_CLASS class, its WM_CLASS instance and its title, since
/// every Proton window shares the class `steam_proton`; `*` wildcards as
/// in `rounded_apps`. KDL: `xwayland_hidpi_except "Trackmania"`.
+ ///
+ /// A named window is treated as the full-screen X11 game it is
+ /// (`xwayland_window::window_is_hidpi_exempt`): it is drawn at 1 in the
+ /// logical world, it is NOT restored to a saved size at map (it sizes
+ /// itself to the screen — a restored 1214x689 is what Trackmania then
+ /// pinned in its hints), its own position requests are granted (its
+ /// "windowedfull" asks for the desktop origin, and refusing that was a
+ /// ~170/s configure loop), and only a compositor fullscreen or tiling
+ /// overrides its size.
pub xwayland_hidpi_except: Option<Vec<String>>,
/// Apps whose windows turn trackpad input into a view drag (Space +
/// button) — see `cursor::ViewDrag`. A scroll over one of their popups
diff --git a/src/server/window.rs b/src/server/window.rs
index 325bce9..f8bf111 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1117,6 +1117,19 @@ impl Window {
if matches!(self.impl_type, WindowImpl::Xwayland(_)) && self.state != WindowState::Mapped {
return;
}
+ // A full-screen X11 game (`xwayland_hidpi_except`) sizes itself to
+ // the screen; restoring a saved size onto it is what shrank
+ // Trackmania to the launcher's 1214x689 — the game then pinned that
+ // size in its hints and no fullscreen could take. Mark it restored
+ // so nothing else tries.
+ if crate::xwayland_window::window_is_hidpi_exempt(self as *const Window) {
+ log::info!(
+ "Not restoring saved state for {:?}: named in xwayland_hidpi_except, it places itself",
+ self.get_title_string().unwrap_or_default()
+ );
+ self.restored = true;
+ return;
+ }
let app_id_str = self.get_app_id_string().unwrap_or_default();
if app_id_str.is_empty()
|| app_id_str.starts_with("cce-status")
diff --git a/src/server/xwayland_window.rs b/src/server/xwayland_window.rs
index 95f6e16..a8e4206 100644
--- a/src/server/xwayland_window.rs
+++ b/src/server/xwayland_window.rs
@@ -242,6 +242,38 @@ pub unsafe fn x11_scale_for_surface(
/// WM_CLASS class, the WM_CLASS instance and the title with the
/// `app_id_matches` rules (case-insensitive, `*` wildcards). Empty fields
/// never match.
+/// 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.
+pub unsafe fn window_is_hidpi_exempt(window: *const crate::window::Window) -> bool {
+ if window.is_null() {
+ return false;
+ }
+ let crate::window::WindowImpl::Xwayland(xwindow) = (*window).impl_type else {
+ return false;
+ };
+ if xwindow.is_null() || (*xwindow).xsurface.is_null() {
+ return false;
+ }
+ let server = (*window).server;
+ if server.is_null() || !(*server).wm.xwayland_hidpi || (*server).wm.xwayland_hidpi_except.is_empty() {
+ return false;
+ }
+ let xsurface = (*xwindow).xsurface;
+ let text = |p: *const libc::c_char| -> String {
+ if p.is_null() { String::new() } else { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
+ };
+ hidpi_exempt(
+ &(*server).wm.xwayland_hidpi_except,
+ &text((*xsurface).class),
+ &text((*xsurface).instance),
+ &text((*xsurface).title),
+ )
+}
+
pub fn hidpi_exempt(patterns: &[String], class: &str, instance: &str, title: &str) -> bool {
use crate::window_manager::app_id_matches;
patterns.iter().any(|p| {
@@ -665,7 +697,22 @@ unsafe extern "C" fn handle_request_configure(listener: *mut ffi::wl_listener, d
(*(*xwindow).xsurface).x, (*(*xwindow).xsurface).y, (*(*xwindow).xsurface).width, (*(*xwindow).xsurface).height,
);
- if has_parent {
+ let is_tiled = unsafe {
+ (*window).wm_requested.tiled != 0 || !matches!((*window).tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Utility)
+ };
+ let is_fullscreen = unsafe { (*window).is_fullscreen() };
+
+ // A window named in `xwayland_hidpi_except` is a full-screen X11 game
+ // that sizes AND places itself to the screen (Trackmania's
+ // "windowedfull" asks for (0, 0) at the desktop size). Refusing the
+ // position — answering every request with the compositor's placement —
+ // had Wine re-asking ~170 times a second for as long as the window was
+ // up. It gets the parented treatment: position and size granted, the
+ // virtual origin moved with it. Not while the compositor has it
+ // fullscreen or tiled: then the size is the compositor's (below).
+ let exempt_self_placed = !has_parent && !is_fullscreen && !is_tiled && window_is_hidpi_exempt(window);
+
+ if has_parent || exempt_self_placed {
// Granted on the logical grid rather than verbatim — see `snap_x11`.
// The logical values below are what the window's geometry becomes, so
// handing X anything else is handing it a number this compositor
@@ -700,19 +747,24 @@ unsafe extern "C" fn handle_request_configure(listener: *mut ffi::wl_listener, d
let (vx, vy) = (*window).screen_to_virtual(log_x, log_y);
(*window).virtual_x = vx;
(*window).virtual_y = vy;
+ if exempt_self_placed {
+ // Placed by the client: the camera must not pan to it on spawn.
+ (*window).hint_placed = true;
+ }
(*window).set_dimensions(log_width, log_height);
return;
}
- let is_tiled = unsafe {
- (*window).wm_requested.tiled != 0 || !matches!((*window).tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Utility)
- };
-
- let is_fullscreen = unsafe { (*window).is_fullscreen() };
-
// A floating window normally gets the size it asks for; not while an
- // output is coming or going (see `note_output_change`).
- let hold_size = is_tiled || in_output_change_grace();
+ // output is coming or going (see `note_output_change`), and not while
+ // the compositor has it FULLSCREEN: Wine syncs a window's
+ // _NET_WM_STATE from its own idea of the window rect, so a game whose
+ // fixed-size hints were granted here shrank the X window back the
+ // instant the fullscreen configure went out, then withdrew the
+ // fullscreen state Wine no longer saw as true — every Fullscreen press
+ // on Trackmania undid itself within the same frame. Held at the
+ // fullscreen size, Wine sees a screen-sized rect and keeps the state.
+ let hold_size = is_tiled || is_fullscreen || in_output_change_grace();
if hold_size && !is_tiled {
log::info!(
"XWayland configure request: holding floating '{}' at its own size during output-change grace",
@@ -859,6 +911,11 @@ unsafe extern "C" fn handle_request_maximize(listener: *mut ffi::wl_listener, _d
unsafe extern "C" fn handle_request_fullscreen(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
let xwindow = crate::container_of!(listener, XwaylandWindow, request_fullscreen);
let fullscreen = (*(*xwindow).xsurface).fullscreen;
+ log::info!(
+ "XWayland fullscreen request: title='{}' fullscreen={}",
+ (*(*xwindow).window).get_title_string().unwrap_or_default(),
+ fullscreen,
+ );
(*(*xwindow).window).wm_scheduled.fullscreen_requested = if fullscreen {
crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut())
} else {