From 66eb2ea443bc441a1b0ccd579d718d1a418a9a04 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Wed, 24 Jul 2024 17:43:02 -0400 Subject: [PATCH] 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. --- build-scripts/SDL_migration.cocci | 4 + docs/README-migration.md | 1 + include/SDL3/SDL_hints.h | 58 ++++++------- src/events/SDL_mouse.c | 55 ++++++++++++- src/events/SDL_mouse_c.h | 3 + src/video/wayland/SDL_waylandevents_c.h | 4 - src/video/wayland/SDL_waylandmouse.c | 105 ++++++------------------ test/testrelative.c | 104 ++++++++++++++++++++--- 8 files changed, 212 insertions(+), 122 deletions(-) diff --git a/build-scripts/SDL_migration.cocci b/build-scripts/SDL_migration.cocci index 1ad2294cf1..b5c34a03b2 100644 --- a/build-scripts/SDL_migration.cocci +++ b/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 diff --git a/docs/README-migration.md b/docs/README-migration.md index 53bc9ef38d..879c2df00e 100644 --- a/docs/README-migration.md +++ b/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() diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index a42aff0636..43e4127d65 100644 --- a/include/SDL3/SDL_hints.h +++ b/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. diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c index 9006da3517..b7329f959f 100644 --- a/src/events/SDL_mouse.c +++ b/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); diff --git a/src/events/SDL_mouse_c.h b/src/events/SDL_mouse_c.h index 5f950d6cbd..6959ba697a 100644 --- a/src/events/SDL_mouse_c.h +++ b/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; diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index f1eff1fd82..d32e8cd0a4 100644 --- a/src/video/wayland/SDL_waylandevents_c.h +++ b/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 */ diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c index 6a2640ef1e..dda2917c38 100644 --- a/src/video/wayland/SDL_waylandmouse.c +++ b/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; 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; + 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); - } - - /* 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); } - } 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; + 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); + } + + /* 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; } diff --git a/test/testrelative.c b/test/testrelative.c index 349ba5b426..1671217dd3 100644 --- a/test/testrelative.c +++ b/test/testrelative.c @@ -12,6 +12,7 @@ /* Simple program: Test relative mouse motion */ +#include #include #include @@ -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;