SDL: Added support for using XTest to warp the mouse

From 794ff283e26bedd63e0737b51b1cd2def3676ce3 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 5 Mar 2025 12:39:06 -0800
Subject: [PATCH] Added support for using XTest to warp the mouse

---
 CMakeLists.txt                                |  1 +
 cmake/sdlchecks.cmake                         | 14 +++-
 docs/README-linux.md                          |  4 +-
 include/build_config/SDL_build_config.h.cmake |  2 +
 src/video/x11/SDL_x11dyn.c                    |  6 +-
 src/video/x11/SDL_x11mouse.c                  |  9 ++
 src/video/x11/SDL_x11sym.h                    |  6 ++
 src/video/x11/SDL_x11video.c                  |  9 +-
 src/video/x11/SDL_x11xtest.c                  | 83 +++++++++++++++++++
 src/video/x11/SDL_x11xtest.h                  | 31 +++++++
 10 files changed, 159 insertions(+), 6 deletions(-)
 create mode 100644 src/video/x11/SDL_x11xtest.c
 create mode 100644 src/video/x11/SDL_x11xtest.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 67389175e2200..ed3a2d2ef1e9f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -357,6 +357,7 @@ dep_option(SDL_X11_XRANDR          "Enable Xrandr support" "${SDL_X11_XRANDR_DEF
 dep_option(SDL_X11_XSCRNSAVER      "Enable Xscrnsaver support" ON SDL_X11 OFF)
 dep_option(SDL_X11_XSHAPE          "Enable XShape support" ON SDL_X11 OFF)
 dep_option(SDL_X11_XSYNC           "Enable Xsync support" ON SDL_X11 OFF)
+dep_option(SDL_X11_XTEST           "Enable XTest support" ON SDL_X11 OFF)
 dep_option(SDL_WAYLAND             "Use Wayland video driver" ${UNIX_SYS} "SDL_VIDEO" OFF)
 dep_option(SDL_WAYLAND_SHARED      "Dynamically load Wayland support" ON "SDL_WAYLAND;SDL_DEPS_SHARED" OFF)
 dep_option(SDL_WAYLAND_LIBDECOR    "Use client-side window decorations on Wayland" ON "SDL_WAYLAND" OFF)
diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake
index d95cbfa3a1a8a..e5670305fd5fc 100644
--- a/cmake/sdlchecks.cmake
+++ b/cmake/sdlchecks.cmake
@@ -274,10 +274,11 @@ macro(CheckX11)
     set(Xrandr_PKG_CONFIG_SPEC xrandr)
     set(Xrender_PKG_CONFIG_SPEC xrender)
     set(Xss_PKG_CONFIG_SPEC xscrnsaver)
+    set(Xtst_PKG_CONFIG_SPEC xtst)
 
     find_package(X11)
 
-    foreach(_LIB X11 Xext Xcursor Xi Xfixes Xrandr Xrender Xss)
+    foreach(_LIB X11 Xext Xcursor Xi Xfixes Xrandr Xrender Xss Xtst)
       get_filename_component(_libdir "${X11_${_LIB}_LIB}" DIRECTORY)
       FindLibraryAndSONAME("${_LIB}" LIBDIRS ${_libdir})
     endforeach()
@@ -310,6 +311,7 @@ macro(CheckX11)
     find_file(HAVE_XSYNC_H NAMES "X11/extensions/sync.h" HINTS "${X11_INCLUDEDIR}")
     find_file(HAVE_XSS_H NAMES "X11/extensions/scrnsaver.h" HINTS "${X11_INCLUDEDIR}")
     find_file(HAVE_XSHAPE_H NAMES "X11/extensions/shape.h" HINTS "${X11_INCLUDEDIR}")
+    find_file(HAVE_XTEST_H NAMES "X11/extensions/XTest.h" HINTS "${X11_INCLUDEDIR}")
     find_file(HAVE_XDBE_H NAMES "X11/extensions/Xdbe.h" HINTS "${X11_INCLUDEDIR}")
     find_file(HAVE_XEXT_H NAMES "X11/extensions/Xext.h" HINTS "${X11_INCLUDEDIR}")
 
@@ -472,6 +474,16 @@ macro(CheckX11)
         set(SDL_VIDEO_DRIVER_X11_XSHAPE 1)
         set(HAVE_X11_XSHAPE TRUE)
       endif()
+
+      if(SDL_X11_XTEST AND HAVE_XTEST_H AND XTST_LIB)
+        if(HAVE_X11_SHARED)
+          set(SDL_VIDEO_DRIVER_X11_DYNAMIC_XTEST "\"${XTST_LIB_SONAME}\"")
+        else()
+          sdl_link_dependency(xtst LIBS X11::Xtst CMAKE_MODULE X11 PKG_CONFIG_SPECS ${Xtst_PKG_CONFIG_SPEC})
+        endif()
+        set(SDL_VIDEO_DRIVER_X11_XTEST 1)
+        set(HAVE_X11_XTEST TRUE)
+      endif()
     endif()
   endif()
   if(NOT HAVE_X11)
diff --git a/docs/README-linux.md b/docs/README-linux.md
index 3f2d2c055880e..8399881cd8aaa 100644
--- a/docs/README-linux.md
+++ b/docs/README-linux.md
@@ -17,7 +17,7 @@ Ubuntu 18.04, all available features enabled:
     sudo apt-get install build-essential git make \
     pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
     libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
-    libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev \
+    libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
     libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
     libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev
 
@@ -46,7 +46,7 @@ openSUSE Tumbleweed:
     libgbm-devel pipewire-devel libpulse-devel sndio-devel Mesa-libEGL-devel
 
 Arch:
-    sudo pacman -S alsa-lib cmake hidapi ibus jack libdecor libgl libpulse libusb libx11 libxcursor libxext libxinerama libxkbcommon libxrandr libxrender libxss mesa ninja pipewire sndio vulkan-driver vulkan-headers wayland wayland-protocols
+    sudo pacman -S alsa-lib cmake hidapi ibus jack libdecor libgl libpulse libusb libx11 libxcursor libxext libxinerama libxkbcommon libxrandr libxrender libxss libxtst mesa ninja pipewire sndio vulkan-driver vulkan-headers wayland wayland-protocols
 
 
 Joystick does not work
diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake
index 6ce491e7d2fb9..69117559cbc32 100644
--- a/include/build_config/SDL_build_config.h.cmake
+++ b/include/build_config/SDL_build_config.h.cmake
@@ -390,6 +390,7 @@
 #cmakedefine SDL_VIDEO_DRIVER_X11_DYNAMIC_XINPUT2 @SDL_VIDEO_DRIVER_X11_DYNAMIC_XINPUT2@
 #cmakedefine SDL_VIDEO_DRIVER_X11_DYNAMIC_XRANDR @SDL_VIDEO_DRIVER_X11_DYNAMIC_XRANDR@
 #cmakedefine SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS @SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS@
+#cmakedefine SDL_VIDEO_DRIVER_X11_DYNAMIC_XTEST @SDL_VIDEO_DRIVER_X11_DYNAMIC_XTEST@
 #cmakedefine SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM 1
 #cmakedefine SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS 1
 #cmakedefine SDL_VIDEO_DRIVER_X11_XCURSOR 1
@@ -401,6 +402,7 @@
 #cmakedefine SDL_VIDEO_DRIVER_X11_XSCRNSAVER 1
 #cmakedefine SDL_VIDEO_DRIVER_X11_XSHAPE 1
 #cmakedefine SDL_VIDEO_DRIVER_X11_XSYNC 1
+#cmakedefine SDL_VIDEO_DRIVER_X11_XTEST 1
 #cmakedefine SDL_VIDEO_DRIVER_QNX 1
 
 #cmakedefine SDL_VIDEO_RENDER_D3D 1
diff --git a/src/video/x11/SDL_x11dyn.c b/src/video/x11/SDL_x11dyn.c
index 7c48ed5ce6158..fff97cab64270 100644
--- a/src/video/x11/SDL_x11dyn.c
+++ b/src/video/x11/SDL_x11dyn.c
@@ -56,6 +56,9 @@ typedef struct
 #ifndef SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS
 #define SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS NULL
 #endif
+#ifndef SDL_VIDEO_DRIVER_X11_DYNAMIC_XTEST
+#define SDL_VIDEO_DRIVER_X11_DYNAMIC_XTEST NULL
+#endif
 
 static x11dynlib x11libs[] = {
     { NULL, SDL_VIDEO_DRIVER_X11_DYNAMIC },
@@ -64,7 +67,8 @@ static x11dynlib x11libs[] = {
     { NULL, SDL_VIDEO_DRIVER_X11_DYNAMIC_XINPUT2 },
     { NULL, SDL_VIDEO_DRIVER_X11_DYNAMIC_XFIXES },
     { NULL, SDL_VIDEO_DRIVER_X11_DYNAMIC_XRANDR },
-    { NULL, SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS }
+    { NULL, SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS },
+    { NULL, SDL_VIDEO_DRIVER_X11_DYNAMIC_XTEST }
 };
 
 static void *X11_GetSym(const char *fnname, int *pHasModule)
diff --git a/src/video/x11/SDL_x11mouse.c b/src/video/x11/SDL_x11mouse.c
index 5c72dbfae8824..343d8654457b6 100644
--- a/src/video/x11/SDL_x11mouse.c
+++ b/src/video/x11/SDL_x11mouse.c
@@ -26,6 +26,7 @@
 #include "SDL_x11video.h"
 #include "SDL_x11mouse.h"
 #include "SDL_x11xinput2.h"
+#include "SDL_x11xtest.h"
 #include "../SDL_video_c.h"
 #include "../../events/SDL_mouse_c.h"
 
@@ -367,6 +368,10 @@ static bool X11_WarpMouse(SDL_Window *window, float x, float y)
 {
     SDL_WindowData *data = window->internal;
 
+    if (X11_WarpMouseXTest(SDL_GetVideoDevice(), window, x, y)) {
+        return true;
+    }
+
 #ifdef SDL_VIDEO_DRIVER_X11_XFIXES
     // If we have no barrier, we need to warp
     if (data->pointer_barrier_active == false) {
@@ -380,6 +385,10 @@ static bool X11_WarpMouse(SDL_Window *window, float x, float y)
 
 static bool X11_WarpMouseGlobal(float x, float y)
 {
+    if (X11_WarpMouseXTest(SDL_GetVideoDevice(), NULL, x, y)) {
+        return true;
+    }
+
     X11_WarpMouseInternal(DefaultRootWindow(GetDisplay()), x, y);
     return true;
 }
diff --git a/src/video/x11/SDL_x11sym.h b/src/video/x11/SDL_x11sym.h
index 68d70cd27a91d..8e084d7ed6192 100644
--- a/src/video/x11/SDL_x11sym.h
+++ b/src/video/x11/SDL_x11sym.h
@@ -182,6 +182,12 @@ SDL_X11_SYM(Status, XSyncDestroyCounter, (Display* a, XSyncCounter b), (a, b), r
 SDL_X11_SYM(Status, XSyncSetCounter, (Display* a, XSyncCounter b, XSyncValue c), (a, b, c), return)
 #endif
 
+#ifdef SDL_VIDEO_DRIVER_X11_XTEST
+SDL_X11_MODULE(XTEST)
+SDL_X11_SYM(Status, XTestQueryExtension, (Display* a, int* b, int* c), (a, b, c), return)
+SDL_X11_SYM(int, XTestFakeMotionEvent, (Display* a, int b, int c, int d, unsigned long e), (a, b, c, d, e), return)
+#endif
+
 #ifdef SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS
 SDL_X11_SYM(Bool,XGetEventData,(Display* a,XGenericEventCookie* b),(a,b),return)
 SDL_X11_SYM(void,XFreeEventData,(Display* a,XGenericEventCookie* b),(a,b),)
diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c
index 75862db2215ff..a03b97fbf9af0 100644
--- a/src/video/x11/SDL_x11video.c
+++ b/src/video/x11/SDL_x11video.c
@@ -39,6 +39,7 @@
 #include "SDL_x11messagebox.h"
 #include "SDL_x11shape.h"
 #include "SDL_x11xsync.h"
+#include "SDL_x11xtest.h"
 
 #ifdef SDL_VIDEO_OPENGL_EGL
 #include "SDL_x11opengles.h"
@@ -443,13 +444,17 @@ static bool X11_VideoInit(SDL_VideoDevice *_this)
 
 #ifdef SDL_VIDEO_DRIVER_X11_XFIXES
     X11_InitXfixes(_this);
-#endif // SDL_VIDEO_DRIVER_X11_XFIXES
+#endif
 
     X11_InitXsettings(_this);
 
 #ifdef SDL_VIDEO_DRIVER_X11_XSYNC
     X11_InitXsync(_this);
-#endif /* SDL_VIDEO_DRIVER_X11_XSYNC */
+#endif
+
+#ifdef SDL_VIDEO_DRIVER_X11_XTEST
+    X11_InitXTest(_this);
+#endif
 
 #ifndef X_HAVE_UTF8_STRING
 #warning X server does not support UTF8_STRING, a feature introduced in 2000! This is likely to become a hard error in a future libSDL3.
diff --git a/src/video/x11/SDL_x11xtest.c b/src/video/x11/SDL_x11xtest.c
new file mode 100644
index 0000000000000..691e5eb32cf6f
--- /dev/null
+++ b/src/video/x11/SDL_x11xtest.c
@@ -0,0 +1,83 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#if defined(SDL_VIDEO_DRIVER_X11)
+
+#include "SDL_x11video.h"
+#include "SDL_x11xtest.h"
+
+static bool xtest_initialized = false;
+
+void X11_InitXTest(SDL_VideoDevice *_this)
+{
+#ifdef SDL_VIDEO_DRIVER_X11_XTEST
+    Display *display = _this->internal->display;
+    int event, error;
+    int opcode;
+
+    if (!SDL_X11_HAVE_XTEST ||
+        !X11_XQueryExtension(display, "XTEST", &opcode, &event, &error)) {
+        return;
+    }
+
+    xtest_initialized = true;
+#endif
+}
+
+bool X11_XTestIsInitialized(void)
+{
+    return xtest_initialized;
+}
+
+bool X11_WarpMouseXTest(SDL_VideoDevice *_this, SDL_Window *window, float x, float y)
+{
+#ifdef SDL_VIDEO_DRIVER_X11_XTEST
+    if (!X11_XTestIsInitialized()) {
+        return false;
+    }
+
+    Display *display = _this->internal->display;
+    SDL_DisplayData *displaydata = window ? SDL_GetDisplayDriverDataForWindow(window) : SDL_GetDisplayDriverData(SDL_GetPrimaryDisplay());
+    if (!displaydata) {
+        return false;
+    }
+
+    int motion_x = (int)SDL_roundf(x);
+    int motion_y = (int)SDL_roundf(y);
+    if (window) {
+        motion_x += window->x;
+        motion_y += window->y;
+    }
+
+    if (!X11_XTestFakeMotionEvent(display, displaydata->screen, motion_x, motion_y, CurrentTime)) {
+        return false;
+    }
+    X11_XSync(display, False);
+
+    return true;
+#else
+    return false;
+#endif
+}
+
+#endif // SDL_VIDEO_DRIVER_X11
diff --git a/src/video/x11/SDL_x11xtest.h b/src/video/x11/SDL_x11xtest.h
new file mode 100644
index 0000000000000..c4ff07034fc56
--- /dev/null
+++ b/src/video/x11/SDL_x11xtest.h
@@ -0,0 +1,31 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#ifndef SDL_x11xtest_h_
+#define SDL_x11xtest_h_
+
+extern void X11_InitXTest(SDL_VideoDevice *_this);
+extern bool X11_XTestIsInitialized(void);
+extern bool X11_WarpMouseXTest(SDL_VideoDevice *_this, SDL_Window *window, float x, float y);
+
+#endif // SDL_x11xtest_h_