Browse Source

mouse: Make pointer warp emulation via relative mode available to all platforms

Move the Wayland pointer warp emulation code up to the SDL mouse layer, and activate it when a client attempts to warp a hidden mouse cursor when the hint is set.

testrelative adds the ability to test the warp emulation activation/deactivation with the --warp parameter and 'c' key for toggling cursor visibility.
Frank Praznik 8 months ago
parent
commit
66eb2ea443

+ 4 - 0
build-scripts/SDL_migration.cocci

@@ -3596,3 +3596,7 @@ typedef SDL_JoystickGUID, SDL_GUID;
 - SDL_OnApplicationDidBecomeActive
 + SDL_OnApplicationDidEnterForeground
   (...)
+@@
+@@
+- SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP
++ SDL_HINT_MOUSE_EMULATE_WARP_WITH_RELATIVE

+ 1 - 0
docs/README-migration.md

@@ -806,6 +806,7 @@ The following hints have been renamed:
 * SDL_HINT_LINUX_HAT_DEADZONES => SDL_HINT_JOYSTICK_LINUX_HAT_DEADZONES
 * SDL_HINT_LINUX_JOYSTICK_CLASSIC => SDL_HINT_JOYSTICK_LINUX_CLASSIC
 * SDL_HINT_LINUX_JOYSTICK_DEADZONES => SDL_HINT_JOYSTICK_LINUX_DEADZONES
+* SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP => SDL_HINT_MOUSE_EMULATE_WARP_WITH_RELATIVE
 
 The following functions have been removed:
 * SDL_ClearHints() - replaced with SDL_ResetHints()

+ 30 - 28
include/SDL3/SDL_hints.h

@@ -2190,6 +2190,36 @@ extern "C" {
  */
 #define SDL_HINT_MOUSE_DOUBLE_CLICK_TIME    "SDL_MOUSE_DOUBLE_CLICK_TIME"
 
+/**
+ * A variable controlling whether warping a hidden mouse cursor will activate
+ * relative mouse mode.
+ *
+ * When this hint is set and the mouse cursor is hidden, SDL will emulate mouse
+ * warps using relative mouse mode. This can provide smoother and more reliable
+ * mouse motion for some older games, which continuously calculate the distance
+ * travelled by the mouse pointer and warp it back to the center of the window,
+ * rather than using relative mouse motion.
+ *
+ * Note that relative mouse mode may have different mouse acceleration behavior
+ * than pointer warps.
+ *
+ * If your game or application needs to warp the mouse cursor while hidden for
+ * other purposes, such as drawing a software cursor, it should disable this hint.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Attempts to warp the mouse will always be made.
+ * - "1": Some mouse warps will be emulated by forcing relative mouse mode. (default)
+ *
+ * If not set, this is automatically enabled unless an application uses
+ * relative mouse mode directly.
+ *
+ * This hint can be set anytime.
+ *
+ * \since This hint is available since SDL 3.0.0.
+ */
+#define SDL_HINT_MOUSE_EMULATE_WARP_WITH_RELATIVE "SDL_MOUSE_EMULATE_WARP_WITH_RELATIVE"
+
 /**
  * Allow mouse click events when clicking to focus an SDL window.
  *
@@ -3063,34 +3093,6 @@ extern "C" {
  */
 #define SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR "SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR"
 
-/**
- * Enable or disable hidden mouse pointer warp emulation, needed by some older
- * games.
- *
- * Wayland requires the pointer confinement protocol to warp the mouse, but
- * that is just a hint that the compositor is free to ignore, and warping the
- * the pointer to or from regions outside of the focused window is prohibited.
- * When this hint is set and the pointer is hidden, SDL will emulate mouse
- * warps using relative mouse mode. This is required for some older games
- * (such as Source engine games), which warp the mouse to the centre of the
- * screen rather than using relative mouse motion. Note that relative mouse
- * mode may have different mouse acceleration behaviour than pointer warps.
- *
- * The variable can be set to the following values:
- *
- * - "0": Attempts to warp the mouse will be made, if the appropriate protocol
- *   is available.
- * - "1": Some mouse warps will be emulated by forcing relative mouse mode.
- *
- * If not set, this is automatically enabled unless an application uses
- * relative mouse mode directly.
- *
- * This hint can be set anytime.
- *
- * \since This hint is available since SDL 3.0.0.
- */
-#define SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP "SDL_VIDEO_WAYLAND_EMULATE_MOUSE_WARP"
-
 /**
  * A variable controlling whether video mode emulation is enabled under
  * Wayland.

+ 53 - 2
src/events/SDL_mouse.c

@@ -119,6 +119,18 @@ static void SDLCALL SDL_MouseRelativeSystemScaleChanged(void *userdata, const ch
     mouse->enable_relative_system_scale = SDL_GetStringBoolean(hint, SDL_FALSE);
 }
 
+static void SDLCALL SDL_MouseWarpEmulationChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
+{
+    SDL_Mouse *mouse = (SDL_Mouse *)userdata;
+
+    mouse->warp_emulation_hint = SDL_GetStringBoolean(hint, SDL_TRUE);
+
+    if (!mouse->warp_emulation_hint && mouse->warp_emulation_active) {
+        SDL_SetRelativeMouseMode(SDL_FALSE);
+        mouse->warp_emulation_active = SDL_FALSE;
+    }
+}
+
 static void SDLCALL SDL_TouchMouseEventsChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
 {
     SDL_Mouse *mouse = (SDL_Mouse *)userdata;
@@ -211,6 +223,9 @@ int SDL_PreInitMouse(void)
     SDL_AddHintCallback(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE,
                         SDL_MouseRelativeSystemScaleChanged, mouse);
 
+    SDL_AddHintCallback(SDL_HINT_MOUSE_EMULATE_WARP_WITH_RELATIVE,
+                        SDL_MouseWarpEmulationChanged, mouse);
+
     SDL_AddHintCallback(SDL_HINT_TOUCH_MOUSE_EVENTS,
                         SDL_TouchMouseEventsChanged, mouse);
 
@@ -724,7 +739,7 @@ static int SDL_PrivateSendMouseMotion(Uint64 timestamp, SDL_Window *window, SDL_
     float xrel = 0.0f;
     float yrel = 0.0f;
 
-    if (!mouse->relative_mode && mouseID != SDL_TOUCH_MOUSEID && mouseID != SDL_PEN_MOUSEID) {
+    if ((!mouse->relative_mode || mouse->warp_emulation_active) && mouseID != SDL_TOUCH_MOUSEID && mouseID != SDL_PEN_MOUSEID) {
         /* We're not in relative mode, so all mouse events are global mouse events */
         mouseID = SDL_GLOBAL_MOUSE_ID;
     }
@@ -1132,6 +1147,9 @@ void SDL_QuitMouse(void)
     SDL_DelHintCallback(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE,
                         SDL_MouseRelativeSystemScaleChanged, mouse);
 
+    SDL_DelHintCallback(SDL_HINT_MOUSE_EMULATE_WARP_WITH_RELATIVE,
+                        SDL_MouseWarpEmulationChanged, mouse);
+
     SDL_DelHintCallback(SDL_HINT_TOUCH_MOUSE_EVENTS,
                         SDL_TouchMouseEventsChanged, mouse);
 
@@ -1253,9 +1271,24 @@ void SDL_PerformWarpMouseInWindow(SDL_Window *window, float x, float y, SDL_bool
     }
 }
 
+static void SDL_EnableWarpEmulation(SDL_Mouse *mouse)
+{
+    if (!mouse->cursor_shown && mouse->warp_emulation_hint && !mouse->warp_emulation_prohibited) {
+        if (SDL_SetRelativeMouseMode(SDL_TRUE) == 0) {
+            mouse->warp_emulation_active = SDL_TRUE;
+        }
+
+        /* Disable attempts at enabling warp emulation until further notice. */
+        mouse->warp_emulation_prohibited = SDL_TRUE;
+    }
+}
+
 void SDL_WarpMouseInWindow(SDL_Window *window, float x, float y)
 {
-    SDL_PerformWarpMouseInWindow(window, x, y, SDL_FALSE);
+    SDL_Mouse *mouse = SDL_GetMouse();
+    SDL_EnableWarpEmulation(mouse);
+
+    SDL_PerformWarpMouseInWindow(window, x, y, mouse->warp_emulation_active);
 }
 
 int SDL_WarpMouseGlobal(float x, float y)
@@ -1284,6 +1317,18 @@ int SDL_SetRelativeMouseMode(SDL_bool enabled)
     SDL_Mouse *mouse = SDL_GetMouse();
     SDL_Window *focusWindow = SDL_GetKeyboardFocus();
 
+    if (enabled) {
+        if (mouse->warp_emulation_active) {
+            mouse->warp_emulation_active = SDL_FALSE;
+        }
+
+        /* If the app has used relative mode before, it probably shouldn't
+         * also be emulating it using repeated mouse warps, so disable
+         * mouse warp emulation by default.
+         */
+        mouse->warp_emulation_prohibited = SDL_TRUE;
+    }
+
     if (enabled == mouse->relative_mode) {
         return 0;
     }
@@ -1642,6 +1687,12 @@ int SDL_ShowCursor(void)
 {
     SDL_Mouse *mouse = SDL_GetMouse();
 
+    if (mouse->warp_emulation_active) {
+        SDL_SetRelativeMouseMode(SDL_FALSE);
+        mouse->warp_emulation_active = SDL_FALSE;
+        mouse->warp_emulation_prohibited = SDL_FALSE;
+    }
+
     if (!mouse->cursor_shown) {
         mouse->cursor_shown = SDL_TRUE;
         SDL_SetCursor(NULL);

+ 3 - 0
src/events/SDL_mouse_c.h

@@ -92,6 +92,9 @@ typedef struct
     SDL_bool relative_mode_warp;
     SDL_bool relative_mode_warp_motion;
     SDL_bool relative_mode_cursor_visible;
+    SDL_bool warp_emulation_hint;
+    SDL_bool warp_emulation_active;
+    SDL_bool warp_emulation_prohibited;
     int relative_mode_clip_interval;
     SDL_bool enable_normal_speed_scale;
     float normal_speed_scale;

+ 0 - 4
src/video/wayland/SDL_waylandevents_c.h

@@ -171,10 +171,6 @@ struct SDL_WaylandInput
 
     struct SDL_WaylandTabletInput *tablet;
 
-    /* are we forcing relative mouse mode? */
-    SDL_bool cursor_visible;
-    SDL_bool relative_mode_override;
-    SDL_bool warp_emulation_prohibited;
     SDL_bool keyboard_is_virtual;
 
     /* Current SDL modifier flags */

+ 27 - 78
src/video/wayland/SDL_waylandmouse.c

@@ -624,14 +624,8 @@ static int Wayland_ShowCursor(SDL_Cursor *cursor)
             if (input->cursor_shape) {
                 Wayland_SetSystemCursorShape(input, data->cursor_data.system.id);
 
-                input->cursor_visible = SDL_TRUE;
                 input->current_cursor = data;
 
-                if (input->relative_mode_override) {
-                    Wayland_input_disable_relative_pointer(input);
-                    input->relative_mode_override = SDL_FALSE;
-                }
-
                 return 0;
             } else if (!wayland_get_system_cursor(d, data, &scale)) {
                 return -1;
@@ -662,18 +656,10 @@ static int Wayland_ShowCursor(SDL_Cursor *cursor)
         } else {
             wl_surface_damage(data->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
         }
-        wl_surface_commit(data->surface);
 
-        input->cursor_visible = SDL_TRUE;
+        wl_surface_commit(data->surface);
         input->current_cursor = data;
-
-        if (input->relative_mode_override) {
-            Wayland_input_disable_relative_pointer(input);
-            input->relative_mode_override = SDL_FALSE;
-        }
-
     } else {
-        input->cursor_visible = SDL_FALSE;
         input->current_cursor = NULL;
         wl_pointer_set_cursor(pointer, input->pointer_enter_serial, NULL, 0, 0);
     }
@@ -688,40 +674,33 @@ static int Wayland_WarpMouse(SDL_Window *window, float x, float y)
     SDL_WindowData *wind = window->internal;
     struct SDL_WaylandInput *input = d->input;
 
-    if (input->cursor_visible || (input->warp_emulation_prohibited && !d->relative_mouse_mode)) {
-        if (d->pointer_constraints) {
-            const SDL_bool toggle_lock = !wind->locked_pointer;
-
-            /* The pointer confinement protocol allows setting a hint to warp the pointer,
-             * but only when the pointer is locked.
-             *
-             * Lock the pointer, set the position hint, unlock, and hope for the best.
-             */
-            if (toggle_lock) {
-                Wayland_input_lock_pointer(input, window);
-            }
-            if (wind->locked_pointer) {
-                const wl_fixed_t f_x = wl_fixed_from_double(x / wind->pointer_scale.x);
-                const wl_fixed_t f_y = wl_fixed_from_double(y / wind->pointer_scale.y);
-                zwp_locked_pointer_v1_set_cursor_position_hint(wind->locked_pointer, f_x, f_y);
-                wl_surface_commit(wind->surface);
-            }
-            if (toggle_lock) {
-                Wayland_input_unlock_pointer(input, window);
-            }
+    if (d->pointer_constraints) {
+        const SDL_bool toggle_lock = !wind->locked_pointer;
 
-            /* NOTE: There is a pending warp event under discussion that should replace this when available.
-             * https://gitlab.freedesktop.org/wayland/wayland/-/merge_requests/340
-             */
-            SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, SDL_FALSE, x, y);
-        } else {
-            return SDL_SetError("wayland: mouse warp failed; compositor lacks support for the required zwp_pointer_confinement_v1 protocol");
+        /* The pointer confinement protocol allows setting a hint to warp the pointer,
+         * but only when the pointer is locked.
+         *
+         * Lock the pointer, set the position hint, unlock, and hope for the best.
+         */
+        if (toggle_lock) {
+            Wayland_input_lock_pointer(input, window);
+        }
+        if (wind->locked_pointer) {
+            const wl_fixed_t f_x = wl_fixed_from_double(x / wind->pointer_scale.x);
+            const wl_fixed_t f_y = wl_fixed_from_double(y / wind->pointer_scale.y);
+            zwp_locked_pointer_v1_set_cursor_position_hint(wind->locked_pointer, f_x, f_y);
+            wl_surface_commit(wind->surface);
+        }
+        if (toggle_lock) {
+            Wayland_input_unlock_pointer(input, window);
         }
-    } else if (input->warp_emulation_prohibited) {
-        return SDL_Unsupported();
-    } else if (!d->relative_mouse_mode) {
-        Wayland_input_enable_relative_pointer(input);
-        input->relative_mode_override = SDL_TRUE;
+
+        /* NOTE: There is a pending warp event under discussion that should replace this when available.
+         * https://gitlab.freedesktop.org/wayland/wayland/-/merge_requests/340
+         */
+        SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, SDL_FALSE, x, y);
+    } else {
+        return SDL_SetError("wayland: mouse warp failed; compositor lacks support for the required zwp_pointer_confinement_v1 protocol");
     }
 
     return 0;
@@ -749,29 +728,12 @@ static int Wayland_SetRelativeMouseMode(SDL_bool enabled)
     SDL_VideoData *data = vd->internal;
 
     if (enabled) {
-        /* Disable mouse warp emulation if it's enabled. */
-        if (data->input->relative_mode_override) {
-            data->input->relative_mode_override = SDL_FALSE;
-        }
-
-        /* If the app has used relative mode before, it probably shouldn't
-         * also be emulating it using repeated mouse warps, so disable
-         * mouse warp emulation by default.
-         */
-        data->input->warp_emulation_prohibited = SDL_TRUE;
         return Wayland_input_enable_relative_pointer(data->input);
     } else {
         return Wayland_input_disable_relative_pointer(data->input);
     }
 }
 
-static void SDLCALL Wayland_EmulateMouseWarpChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
-{
-    struct SDL_WaylandInput *input = (struct SDL_WaylandInput *)userdata;
-
-    input->warp_emulation_prohibited = !SDL_GetStringBoolean(hint, !input->warp_emulation_prohibited);
-}
-
 /* Wayland doesn't support getting the true global cursor position, but it can
  * be faked well enough for what most applications use it for: querying the
  * global cursor coordinates and transforming them to the window-relative
@@ -862,7 +824,6 @@ void Wayland_InitMouse(void)
     SDL_Mouse *mouse = SDL_GetMouse();
     SDL_VideoDevice *vd = SDL_GetVideoDevice();
     SDL_VideoData *d = vd->internal;
-    struct SDL_WaylandInput *input = d->input;
 
     mouse->CreateCursor = Wayland_CreateCursor;
     mouse->CreateSystemCursor = Wayland_CreateSystemCursor;
@@ -873,9 +834,6 @@ void Wayland_InitMouse(void)
     mouse->SetRelativeMouseMode = Wayland_SetRelativeMouseMode;
     mouse->GetGlobalMouseState = Wayland_GetGlobalMouseState;
 
-    input->relative_mode_override = SDL_FALSE;
-    input->cursor_visible = SDL_TRUE;
-
     SDL_HitTestResult r = SDL_HITTEST_NORMAL;
     while (r <= SDL_HITTEST_RESIZE_LEFT) {
         switch (r) {
@@ -918,26 +876,17 @@ void Wayland_InitMouse(void)
 #endif
 
     SDL_SetDefaultCursor(Wayland_CreateDefaultCursor());
-
-    SDL_AddHintCallback(SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP,
-                        Wayland_EmulateMouseWarpChanged, input);
 }
 
 void Wayland_FiniMouse(SDL_VideoData *data)
 {
-    struct SDL_WaylandInput *input = data->input;
-    int i;
-
     Wayland_FreeCursorThemes(data);
 
 #ifdef SDL_USE_LIBDBUS
     Wayland_DBusFinishCursorProperties();
 #endif
 
-    SDL_DelHintCallback(SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP,
-                        Wayland_EmulateMouseWarpChanged, input);
-
-    for (i = 0; i < SDL_arraysize(sys_cursors); i++) {
+    for (int i = 0; i < SDL_arraysize(sys_cursors); i++) {
         Wayland_FreeCursor(sys_cursors[i]);
         sys_cursors[i] = NULL;
     }

+ 94 - 10
test/testrelative.c

@@ -12,6 +12,7 @@
 
 /* Simple program:  Test relative mouse motion */
 
+#include <SDL3/SDL_test.h>
 #include <SDL3/SDL_test_common.h>
 #include <SDL3/SDL_main.h>
 
@@ -21,16 +22,47 @@
 
 static SDLTest_CommonState *state;
 static int i, done;
-static float mouseX, mouseY;
 static SDL_FRect rect;
 static SDL_Event event;
+static SDL_bool warp;
 
 static void DrawRects(SDL_Renderer *renderer)
 {
     SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
-    rect.x = mouseX;
-    rect.y = mouseY;
     SDL_RenderFillRect(renderer, &rect);
+
+    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+    if (SDL_GetRelativeMouseMode()) {
+        SDLTest_DrawString(renderer, 0.f, 0.f, "Relative Mode: Enabled");
+    } else {
+        SDLTest_DrawString(renderer, 0.f, 0.f, "Relative Mode: Disabled");
+    }
+}
+
+static void CenterMouse()
+{
+    /* Warp the mouse back to the center of the window with input focus to use the
+     * center point for calculating future motion deltas.
+     *
+     * NOTE: DO NOT DO THIS IN REAL APPS/GAMES!
+     *
+     *       This is an outdated method of handling relative pointer motion, and
+     *       may not work properly, if at all, on some platforms. It is here *only*
+     *       for testing the warp emulation code path internal to SDL.
+     *
+     *       Relative mouse mode should be used instead!
+     */
+    SDL_Window *window = SDL_GetKeyboardFocus();
+    if (window) {
+        int w, h;
+        float cx, cy;
+
+        SDL_GetWindowSize(window, &w, &h);
+        cx = (float)w / 2.f;
+        cy = (float)h / 2.f;
+
+        SDL_WarpMouseInWindow(window, cx, cy);
+    }
 }
 
 static void loop(void)
@@ -39,21 +71,46 @@ static void loop(void)
     while (SDL_PollEvent(&event)) {
         SDLTest_CommonEvent(state, &event, &done);
         switch (event.type) {
+        case SDL_EVENT_WINDOW_FOCUS_GAINED:
+            if (warp) {
+                /* This should activate relative mode for warp emulation, unless disabled via a hint. */
+                CenterMouse();
+            }
+            break;
+        case SDL_EVENT_KEY_DOWN:
+            if (event.key.key == SDLK_C) {
+                /* If warp emulation is active, showing the cursor should turn
+                 * relative mode off, and it should re-activate after a warp
+                 * when hidden again.
+                 */
+                if (SDL_CursorVisible()) {
+                    SDL_HideCursor();
+                } else {
+                    SDL_ShowCursor();
+                }
+            }
+            break;
         case SDL_EVENT_MOUSE_MOTION:
         {
-            mouseX += event.motion.xrel;
-            mouseY += event.motion.yrel;
+            rect.x += event.motion.xrel;
+            rect.y += event.motion.yrel;
+
+            if (warp) {
+                CenterMouse();
+            }
         } break;
         default:
             break;
         }
     }
+
     for (i = 0; i < state->num_windows; ++i) {
         SDL_Rect viewport;
         SDL_Renderer *renderer = state->renderers[i];
         if (state->windows[i] == NULL) {
             continue;
         }
+
         SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
         SDL_RenderClear(renderer);
 
@@ -85,7 +142,6 @@ static void loop(void)
 
 int main(int argc, char *argv[])
 {
-
     /* Enable standard application logging */
     SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
 
@@ -96,8 +152,27 @@ int main(int argc, char *argv[])
     }
 
     /* Parse commandline */
-    if (!SDLTest_CommonDefaultArgs(state, argc, argv)) {
-        return 1;
+    for (i = 1; i < argc;) {
+        int consumed;
+
+        consumed = SDLTest_CommonArg(state, i);
+        if (consumed == 0) {
+            consumed = -1;
+            if (SDL_strcasecmp(argv[i], "--warp") == 0) {
+                warp = SDL_TRUE;
+                consumed = 1;
+            }
+        }
+
+        if (consumed < 0) {
+            static const char *options[] = {
+                "[--warp]",
+                NULL
+            };
+            SDLTest_CommonLogUsage(state, argv[0], options);
+            return 1;
+        }
+        i += consumed;
     }
 
     if (!SDLTest_CommonInit(state)) {
@@ -112,8 +187,17 @@ int main(int argc, char *argv[])
         SDL_RenderClear(renderer);
     }
 
-    if (SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
-        return 3;
+    /* If warp mode is activated, the cursor will be repeatedly warped back to
+     * the center of the window to simulate the behavior of older games. The cursor
+     * is initially hidden in this case to trigger the warp emulation unless it has
+     * been explicitly disabled via a hint.
+     *
+     * Otherwise, try to activate relative mode.
+     */
+    if (warp) {
+        SDL_HideCursor();
+    } else if (SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
+        return 3; /* Relative mode failed, just exit. */
     }
 
     rect.x = DEFAULT_WINDOW_WIDTH / 2;