From: Hongchao Liu <[email protected]>

SDL2's SDL_SetRelativeMouseMode(SDL_TRUE) is the standard way to handle
relative pointing devices in grab mode: SDL itself warps the host pointer
back to the window center every frame, so xrel/yrel values in
SDL_MOUSEMOTION events always remain meaningful and the guest cursor
never gets stuck at an edge.

However, under X11 with XInput2 (the default on most Linux desktops) this
relative mode switches SDL to consume only XI_RawMotion events.
Pointer-injection servers such as VNC (via XTestFakeMotionEvent), nested
X servers and some remote desktop setups never produce XI_RawMotion, so
mouse input breaks completely under those environments as soon as the
guest enters relative-input grab mode. SDL does not expose a way to
detect this at runtime (both local X11 and a VNC-forwarded X session
report "x11" from SDL_GetCurrentVideoDriver), and upstream SDL issues
tracking this (libsdl-org/SDL#1116, #4206) have stayed open for years.

Stop enabling SDL_SetRelativeMouseMode on the grab paths and replace
the automatic warping it provides with the approach already used by
the GTK backend (ui/gtk.c gd_motion_event): when the host pointer
reaches the window edge, warp it back to the window center explicitly,
letting subsequent motion events again produce non-zero xrel/yrel.

Specifically:

 - sdl_hide_cursor() is no longer called from sdl_grab_start() or
   sdl_mouse_warp(); both paths open-code the cursor hide so we never
   enter SDL_SetRelativeMouseMode(SDL_TRUE). sdl_hide_cursor() is
   retained as the counterpart of sdl_show_cursor() and marked
   G_GNUC_UNUSED.

 - In handle_mousemotion(), when we are in a grab for a relative
   pointing device, detect edge contact using the window coordinates
   (ev->motion.x/y vs scr_w/h) and SDL_WarpMouseInWindow() the pointer
   to the window center. Window coordinates are used regardless of
   the window-to-surface scaling applied to the sent event, because
   we need to detect the host pointer hitting the SDL window edge.

 - Absolute pointing devices and absolute_enabled paths are untouched.

Tested on Linux/X11 with a relative virtio-mouse: the guest cursor can
be pushed in every direction after repeatedly entering and exiting
grab, and quick drags to the window edge remain responsive. Behavior
for absolute pointing devices (virtio-tablet, qemu-keymap kbd) is
unchanged.

Signed-off-by: Hongchao Liu <[email protected]>
---
 ui/sdl2.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 61 insertions(+), 3 deletions(-)

diff --git a/ui/sdl2.c b/ui/sdl2.c
index 5dd612d9a6..faf610281c 100644
--- a/ui/sdl2.c
+++ b/ui/sdl2.c
@@ -212,7 +212,19 @@ static void sdl_update_caption(struct sdl2_console *scon)
     }
 }
 
-static void sdl_hide_cursor(struct sdl2_console *scon)
+/*
+ * sdl_hide_cursor - hide cursor and optionally enable SDL relative mouse mode.
+ *
+ * NOTE: This function is intentionally NOT called from sdl_grab_start() or
+ * sdl_mouse_warp() because SDL_SetRelativeMouseMode(SDL_TRUE) switches SDL2
+ * on XI2-based X servers to consume only XI_RawMotion events. Pointer-
+ * injection servers (VNC via XTestFakeMotionEvent, nested X, some remote
+ * desktop setups) never produce XI_RawMotion, so mouse input would break
+ * under those servers. Those call sites set the cursor state inline
+ * instead. This function is retained as the counterpart of
+ * sdl_show_cursor() and for potential future use.
+ */
+static void G_GNUC_UNUSED sdl_hide_cursor(struct sdl2_console *scon)
 {
     if (scon->opts->has_show_cursor && scon->opts->show_cursor) {
         return;
@@ -267,7 +279,16 @@ static void sdl_grab_start(struct sdl2_console *scon)
             SDL_WarpMouseInWindow(scon->real_window, guest_x, guest_y);
         }
     } else {
-        sdl_hide_cursor(scon);
+        /*
+         * Directly set cursor state, avoid calling sdl_hide_cursor
+         * which would enable SDL_SetRelativeMouseMode(SDL_TRUE) and
+         * break mouse input under XI2-based pointer-injection X servers
+         * (see the note on sdl_hide_cursor).
+         */
+        if (!(scon->opts->has_show_cursor && scon->opts->show_cursor)) {
+            SDL_ShowCursor(SDL_DISABLE);
+            SDL_SetCursor(sdl_cursor_hidden);
+        }
     }
     SDL_SetWindowGrab(scon->real_window, SDL_TRUE);
     gui_grab = 1;
@@ -526,6 +547,34 @@ static void handle_mousemotion(SDL_Event *ev)
     dy = (int64_t)ev->motion.yrel * surf_h / scr_h;
     if (gui_grab || qemu_input_is_absolute(scon->dcl.con) || absolute_enabled) 
{
         sdl_send_mouse_event(scon, dx, dy, x, y, ev->motion.state);
+
+        /*
+         * For relative pointing devices in grab mode we do not enable
+         * SDL_SetRelativeMouseMode, because under XI2-based X servers
+         * that switches SDL to consume only XI_RawMotion events, which
+         * some pointer-injection servers (VNC via XTestFakeMotionEvent,
+         * nested X, ...) never produce.
+         *
+         * Without relative mode, SDL_SetWindowGrab clamps the host
+         * pointer at the window edge without wrapping, so xrel/yrel
+         * become zero once the pointer hits a side and the guest
+         * cursor gets stuck in that direction.
+         *
+         * Mirror the approach used by the GTK backend (ui/gtk.c):
+         * when the pointer reaches any window edge, warp it back to
+         * the window center so subsequent motion can generate deltas
+         * in every direction again. Edge detection uses window
+         * coordinates (ev->motion.x/y, scr_w/h) regardless of the
+         * window-to-surface scaling applied above.
+         */
+        if (!qemu_input_is_absolute(scon->dcl.con) && !absolute_enabled
+            && gui_grab) {
+            if (ev->motion.x <= 0 || ev->motion.x >= scr_w - 1 ||
+                ev->motion.y <= 0 || ev->motion.y >= scr_h - 1) {
+                SDL_WarpMouseInWindow(scon->real_window,
+                                      scr_w / 2, scr_h / 2);
+            }
+        }
     }
 }
 
@@ -748,7 +797,16 @@ static void sdl_mouse_warp(DisplayChangeListener *dcl,
             }
         }
     } else if (gui_grab) {
-        sdl_hide_cursor(scon);
+        /*
+         * Directly set cursor state, avoid calling sdl_hide_cursor
+         * which would enable SDL_SetRelativeMouseMode(SDL_TRUE) and
+         * break mouse input under XI2-based pointer-injection X servers
+         * (see the note on sdl_hide_cursor).
+         */
+        if (!(scon->opts->has_show_cursor && scon->opts->show_cursor)) {
+            SDL_ShowCursor(SDL_DISABLE);
+            SDL_SetCursor(sdl_cursor_hidden);
+        }
     }
     guest_cursor = on;
     guest_x = x, guest_y = y;
-- 
2.34.1


Reply via email to