SDL: x11: Attempt to deal with XInput2 devices with absolute coordinates.

From 5907db56f1535d1fafcb8c28591c8d0f7f880ebd Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Thu, 4 Aug 2022 02:11:21 -0400
Subject: [PATCH] x11: Attempt to deal with XInput2 devices with absolute
 coordinates.

This is untested!

Reference Issue #1836.
---
 src/video/x11/SDL_x11mouse.c   |  10 +++
 src/video/x11/SDL_x11mouse.h   |  11 +++
 src/video/x11/SDL_x11video.h   |   2 +
 src/video/x11/SDL_x11xinput2.c | 139 ++++++++++++++++++++++++++++++---
 4 files changed, 150 insertions(+), 12 deletions(-)

diff --git a/src/video/x11/SDL_x11mouse.c b/src/video/x11/SDL_x11mouse.c
index ed29288c6b2..4fb0afa1a9d 100644
--- a/src/video/x11/SDL_x11mouse.c
+++ b/src/video/x11/SDL_x11mouse.c
@@ -452,6 +452,16 @@ X11_InitMouse(_THIS)
 void
 X11_QuitMouse(_THIS)
 {
+    SDL_VideoData *data = (SDL_VideoData *) _this->driverdata;
+    SDL_XInput2DeviceInfo *i;
+    SDL_XInput2DeviceInfo *next;
+
+    for (i = data->mouse_device_info; i != NULL; i = next) {
+        next = i->next;
+        SDL_free(i);
+    }
+    data->mouse_device_info = NULL;
+
     X11_DestroyEmptyCursor();
 }
 
diff --git a/src/video/x11/SDL_x11mouse.h b/src/video/x11/SDL_x11mouse.h
index e44adacb172..da28f7bca21 100644
--- a/src/video/x11/SDL_x11mouse.h
+++ b/src/video/x11/SDL_x11mouse.h
@@ -23,6 +23,17 @@
 #ifndef SDL_x11mouse_h_
 #define SDL_x11mouse_h_
 
+typedef struct SDL_XInput2DeviceInfo
+{
+    int device_id;
+    SDL_bool relative[2];
+    double minval[2];
+    double maxval[2];
+    double prev_coords[2];
+    Time prev_time;
+    struct SDL_XInput2DeviceInfo *next;
+} SDL_XInput2DeviceInfo;
+
 extern void X11_InitMouse(_THIS);
 extern void X11_QuitMouse(_THIS);
 
diff --git a/src/video/x11/SDL_x11video.h b/src/video/x11/SDL_x11video.h
index 5360d037fe0..e2edf248ad5 100644
--- a/src/video/x11/SDL_x11video.h
+++ b/src/video/x11/SDL_x11video.h
@@ -134,6 +134,8 @@ typedef struct SDL_VideoData
     SDL_Point global_mouse_position;
     Uint32 global_mouse_buttons;
 
+    SDL_XInput2DeviceInfo *mouse_device_info;
+
     int xrandr_event_base;
 
 #if SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
diff --git a/src/video/x11/SDL_x11xinput2.c b/src/video/x11/SDL_x11xinput2.c
index 83a86997f02..c8c2a6ef2e0 100644
--- a/src/video/x11/SDL_x11xinput2.c
+++ b/src/video/x11/SDL_x11xinput2.c
@@ -167,14 +167,109 @@ X11_InitXinput2(_THIS)
     }
 #endif
 
-    if (X11_XISelectEvents(data->display, DefaultRootWindow(data->display), &eventmask,1) != Success) {
+    if (X11_XISelectEvents(data->display, DefaultRootWindow(data->display), &eventmask, 1) != Success) {
+        return;
+    }
+
+    SDL_zero(eventmask);
+    SDL_zeroa(mask);
+    eventmask.deviceid = XIAllDevices;
+    eventmask.mask_len = sizeof(mask);
+    eventmask.mask = mask;
+
+    XISetMask(mask, XI_HierarchyChanged);
+    if (X11_XISelectEvents(data->display, DefaultRootWindow(data->display), &eventmask, 1) != Success) {
         return;
     }
 #endif
 }
 
+/* xi2 device went away? take it out of the list. */
+static void
+xinput2_remove_device_info(SDL_VideoData *videodata, const int device_id)
+{
+    SDL_XInput2DeviceInfo *prev = NULL;
+    SDL_XInput2DeviceInfo *devinfo;
+
+    for (devinfo = videodata->mouse_device_info; devinfo != NULL; devinfo = devinfo->next) {
+        if (devinfo->device_id == device_id) {
+            SDL_assert((devinfo == videodata->mouse_device_info) == (prev == NULL));
+            if (prev == NULL) {
+                videodata->mouse_device_info = devinfo->next;
+            } else {
+                prev->next = devinfo->next;
+            }
+            SDL_free(devinfo);
+            return;
+        }
+        prev = devinfo;
+    }
+}
+
+static SDL_XInput2DeviceInfo *
+xinput2_get_device_info(SDL_VideoData *videodata, const int device_id)
+{
+    /* cache device info as we see new devices. */
+    SDL_XInput2DeviceInfo *prev = NULL;
+    SDL_XInput2DeviceInfo *devinfo;
+    XIDeviceInfo *xidevinfo;
+    int axis = 0;
+    int i;
+
+    for (devinfo = videodata->mouse_device_info; devinfo != NULL; devinfo = devinfo->next) {
+        if (devinfo->device_id == device_id) {
+            SDL_assert((devinfo == videodata->mouse_device_info) == (prev == NULL));
+            if (prev != NULL) {  /* move this to the front of the list, assuming we'll get more from this one. */
+                prev->next = devinfo->next;
+                devinfo->next = videodata->mouse_device_info;
+                videodata->mouse_device_info = devinfo;
+            }
+            return devinfo;
+        }
+        prev = devinfo;
+    }
+
+    /* don't know about this device yet, query and cache it. */
+    devinfo = (SDL_XInput2DeviceInfo *) SDL_calloc(1, sizeof (SDL_XInput2DeviceInfo));
+    if (!devinfo) {
+        SDL_OutOfMemory();
+        return NULL;
+    }
+
+    xidevinfo = X11_XIQueryDevice(videodata->display, device_id, &i);
+    if (!xidevinfo) {
+        SDL_free(devinfo);
+        return NULL;
+    }
+
+    devinfo->device_id = device_id;
+
+    /* !!! FIXME: this is sort of hacky because we only care about the first two axes we see, but any given
+       !!! FIXME:  axis could be relative or absolute, and they might not even be the X and Y axes!
+       !!! FIXME:  But we go on, for now. Maybe we need a more robust mouse API in SDL3... */
+    for (i = 0; i < xidevinfo->num_classes; i++) {
+        const XIValuatorClassInfo *v = (const XIValuatorClassInfo *) xidevinfo->classes[i];
+        if (v->type == XIValuatorClass) {
+            devinfo->relative[axis] = (v->mode == XIModeRelative) ? SDL_TRUE : SDL_FALSE;
+            devinfo->minval[axis] = v->min;
+            devinfo->maxval[axis] = v->max;
+            if (++axis >= 2) {
+                break;
+            }
+        }
+    }
+
+    X11_XIFreeDeviceInfo(xidevinfo);
+
+    devinfo->next = videodata->mouse_device_info;
+    videodata->mouse_device_info = devinfo;
+
+    return devinfo;
+}
+
+
 int
-X11_HandleXinput2Event(SDL_VideoData *videodata,XGenericEventCookie *cookie)
+X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie)
 {
 #if SDL_VIDEO_DRIVER_X11_XINPUT2
     if (cookie->extension != xinput2_opcode) {
@@ -184,31 +279,51 @@ X11_HandleXinput2Event(SDL_VideoData *videodata,XGenericEventCookie *cookie)
         case XI_RawMotion: {
             const XIRawEvent *rawev = (const XIRawEvent*)cookie->data;
             SDL_Mouse *mouse = SDL_GetMouse();
-            double relative_coords[2];
-            static Time prev_time = 0;
-            static double prev_rel_coords[2];
+            SDL_XInput2DeviceInfo *devinfo = xinput2_get_device_info(videodata, rawev->deviceid);
+            double coords[2];
+            double processed_coords[2];
+            int i;
 
             videodata->global_mouse_changed = SDL_TRUE;
 
-            if (!mouse->relative_mode || mouse->relative_mode_warp) {
+            if (!devinfo || !mouse->relative_mode || mouse->relative_mode_warp) {
                 return 0;
             }
 
             parse_valuators(rawev->raw_values,rawev->valuators.mask,
-                            rawev->valuators.mask_len,relative_coords,2);
+                            rawev->valuators.mask_len,coords,2);
 
-            if ((rawev->time == prev_time) && (relative_coords[0] == prev_rel_coords[0]) && (relative_coords[1] == prev_rel_coords[1])) {
+            if ((rawev->time == devinfo->prev_time) && (coords[0] == devinfo->prev_coords[0]) && (coords[1] == devinfo->prev_coords[1])) {
                 return 0;  /* duplicate event, drop it. */
             }
 
-            SDL_SendMouseMotion(mouse->focus,mouse->mouseID,1,(int)relative_coords[0],(int)relative_coords[1]);
-            prev_rel_coords[0] = relative_coords[0];
-            prev_rel_coords[1] = relative_coords[1];
-            prev_time = rawev->time;
+            for (i = 0; i < 2; i++) {
+                if (devinfo->relative[i]) {
+                    processed_coords[i] = coords[i];
+                } else {
+                    processed_coords[i] = devinfo->prev_coords[i] - coords[i];    /* convert absolute to relative */
+                }
+            }
+
+            SDL_SendMouseMotion(mouse->focus, mouse->mouseID, 1, (int) processed_coords[0], (int) processed_coords[1]);
+            devinfo->prev_coords[0] = coords[0];
+            devinfo->prev_coords[1] = coords[1];
+            devinfo->prev_time = rawev->time;
             return 1;
         }
         break;
 
+        case XI_HierarchyChanged: {
+            const XIHierarchyEvent *hierev = (const XIHierarchyEvent *) cookie->data;
+            int i;
+            for (i = 0; i < hierev->num_info; i++) {
+                if (hierev->info[i].flags & XISlaveRemoved) {
+                    xinput2_remove_device_info(videodata, hierev->info[i].deviceid);
+                }
+            }
+        }
+        break;
+
         case XI_RawButtonPress:
         case XI_RawButtonRelease:
 #if SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH