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.
This commit is contained in:
Frank Praznik 2024-07-24 17:43:02 -04:00
parent 92667e3066
commit 66eb2ea443
8 changed files with 212 additions and 122 deletions

View File

@ -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

View File

@ -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()

View File

@ -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.

View File

@ -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);

View File

@ -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;

View File

@ -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 */

View File

@ -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;
}

View File

@ -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;