SDL: x11: Use XInput2 events to pass through the keyboard ID to core key events

From 75a65e05e1a5d6b2199eefc3edc1f9ed9bd684fa Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Wed, 15 Apr 2026 11:24:20 -0400
Subject: [PATCH] x11: Use XInput2 events to pass through the keyboard ID to
 core key events

XInput2 keyboard handling has limitations: system keys that shouldn't be passed through when the keyboard isn't grabbed can be seen, and the text input system needs key events to flow through the X server to function properly (passing synthesized events through the filter function is not sufficient and doesn't work with non-Latin character sets).

The primary bit of information missing from the core X key events that XInput2 provides is the source device, so use the XInput2 slave keyboard device events to store that value, and apply it to core X key events with the same serial. XInput2 events always arrive before core events so this works universally.
---
 src/video/x11/SDL_x11events.c  |  8 ++++++--
 src/video/x11/SDL_x11video.h   |  2 ++
 src/video/x11/SDL_x11xinput2.c | 15 +++++++++++++++
 3 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index 15af66124faad..961c8b08e26e4 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -1834,12 +1834,16 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
     case KeyPress:
     case KeyRelease:
     {
+        SDL_KeyboardID keyboardID = SDL_GLOBAL_KEYBOARD_ID;
         if (data->xinput2_keyboard_enabled) {
-            // This input is being handled by XInput2
+            // This input is being handled by XInput2.
             break;
+        } else if (xevent->xkey.serial == videodata->xinput_last_key_serial) {
+            // Use the device ID from the XInput2 event if the serials match.
+            keyboardID = videodata->xinput_last_keyboard_device;
         }
 
-        X11_HandleKeyEvent(_this, data, SDL_GLOBAL_KEYBOARD_ID, xevent);
+        X11_HandleKeyEvent(_this, data, keyboardID, xevent);
     } break;
 
     case MotionNotify:
diff --git a/src/video/x11/SDL_x11video.h b/src/video/x11/SDL_x11video.h
index 01c2e5b9fb1ff..7bc858d98bded 100644
--- a/src/video/x11/SDL_x11video.h
+++ b/src/video/x11/SDL_x11video.h
@@ -140,6 +140,8 @@ struct SDL_VideoData
 
     SDL_XInput2DeviceInfo *mouse_device_info;
     unsigned long xinput_last_button_serial;
+    unsigned long xinput_last_key_serial;
+    int xinput_last_keyboard_device;
     int xinput_master_pointer_device;
     bool xinput_hierarchy_changed;
 
diff --git a/src/video/x11/SDL_x11xinput2.c b/src/video/x11/SDL_x11xinput2.c
index d6c704e3f6dbf..3a94c6a1f0ace 100644
--- a/src/video/x11/SDL_x11xinput2.c
+++ b/src/video/x11/SDL_x11xinput2.c
@@ -303,6 +303,12 @@ bool X11_InitXinput2(SDL_VideoDevice *_this)
     eventmask.mask_len = sizeof(mask);
     eventmask.mask = mask;
 
+#ifndef USE_XINPUT2_KEYBOARD
+    // If not using the full keyboard handling, register for keypresses to get the event source devices.
+    XISetMask(mask, XI_KeyPress);
+    XISetMask(mask, XI_KeyRelease);
+#endif
+
     XISetMask(mask, XI_HierarchyChanged);
     X11_XISelectEvents(data->display, DefaultRootWindow(data->display), &eventmask, 1);
 
@@ -535,6 +541,8 @@ void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie)
     case XI_KeyRelease:
     {
         const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
+
+#ifdef XINPUT2_USE_KEYBOARD
         SDL_WindowData *windowdata = X11_FindWindow(videodata, xev->event);
         XEvent xevent;
 
@@ -564,6 +572,13 @@ void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie)
         xevent.xkey.same_screen = 1;
 
         X11_HandleKeyEvent(_this, windowdata, (SDL_KeyboardID)xev->sourceid, &xevent);
+#else
+        /* Keys are handled through core X events, however, note the device ID and
+         * associated serial, so that the source device ID can be passed through.
+         */
+        videodata->xinput_last_key_serial = xev->serial;
+        videodata->xinput_last_keyboard_device = xev->sourceid;
+#endif
     } break;
 
     case XI_RawButtonPress: