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

commit89b01d4cad3889d0dbc513fb3f7f9651b647a282
parent43dc21db34
authorLucas Galante <[email protected]>
date2026-09-18 20:50
fix(cursor): an X11 cursor is a physical-pixel bitmap, exempt window or not

`x11_scale_for_surface` gave a window named in xwayland_hidpi_except a
cursor drawn at 1, with its window. But the exemption is about the
game's window pixels — it sizes itself to the root ignoring DPI — while
its cursor comes from Wine, which follows the DPI this compositor
publishes: at LogPixels 192 Trackmania's arrow is 64px, and drawn in the
logical world it was twice the desktop's cursor. Every X11 cursor now
takes the screen's X11 factor. Logs the factor at debug.

Verified in an --xwayland --scale 2 shadow at debug level: hovering the
exempt GTK4 X11 window logs "X11 client cursor: drawn at 1/2".

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

 src/server/seat.rs            |  6 ++++++
 src/server/xwayland_window.rs | 22 ++++++++++++++--------
 2 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/src/server/seat.rs b/src/server/seat.rs
index 2d633fc..969b555 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1798,6 +1798,12 @@ unsafe extern "C" fn handle_request_set_cursor(
         } else {
             1.0
         };
+        if scale != 1.0 {
+            log::debug!(
+                "X11 client cursor: drawn at 1/{scale} (hotspot {}, {})",
+                (*event).hotspot_x, (*event).hotspot_y
+            );
+        }
         seat.watch_x11_cursor((*event).surface, scale);
         let (hotspot_x, hotspot_y) = if scale != 1.0 {
             (
diff --git a/src/server/xwayland_window.rs b/src/server/xwayland_window.rs
index 930f269..883ac69 100644
--- a/src/server/xwayland_window.rs
+++ b/src/server/xwayland_window.rs
@@ -213,13 +213,18 @@ pub unsafe fn x11_scale_for(
     x11_scale(server)
 }
 
-/// `x11_scale_for` for a surface instead of an xsurface: the factor of the X11
-/// window that surface belongs to, and 1 for anything that is not X11.
+/// The factor an X11 client's CURSOR is drawn at: the screen's X11 factor
+/// for any X11 surface, 1 for anything that is not X11.
 ///
-/// What a cursor request needs. The scale has to come from the window under
-/// the pointer rather than the screen, because a window named in
-/// `xwayland_hidpi_except` is drawn in the logical world and its cursor
-/// belongs there with it.
+/// What a cursor request needs. Deliberately NOT `x11_scale_for`: the
+/// `xwayland_hidpi_except` exemption is about a game's window pixels — it
+/// sizes itself to the root ignoring DPI, so its buffer is drawn at 1 —
+/// but its cursor comes from the toolkit or Wine underneath, which follow
+/// the DPI this compositor publishes (Xft.dpi / Xcursor.size at 96×scale
+/// and 24×scale). Wine at LogPixels 192 hands Trackmania a 64px arrow;
+/// drawn in the logical world with the window it was twice the desktop's
+/// cursor. Every X11 cursor is a physical-pixel bitmap, exempt window or
+/// not.
 pub unsafe fn x11_scale_for_surface(
     server: *mut crate::server::Server,
     surface: *mut ffi::wlr_surface,
@@ -235,7 +240,7 @@ pub unsafe fn x11_scale_for_surface(
     if xsurface.is_null() {
         return 1.0;
     }
-    x11_scale_for(server, xsurface as *const _)
+    x11_scale(server)
 }
 
 /// Whether any of `patterns` names this window: each is tried against the
@@ -249,7 +254,8 @@ pub unsafe fn x11_scale_for_surface(
 /// 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`).
+/// state (`handle_request_fullscreen`). Its cursor is NOT exempt — see
+/// `x11_scale_for_surface`.
 pub unsafe fn window_is_hidpi_exempt(window: *const crate::window::Window) -> bool {
     if window.is_null() {
         return false;