SDL: Added support for raw mouse and keyboard using GameInput on Windows

From 001dbc5da8a6cbce92b8722d91c2a9400ae7297f Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 7 Aug 2024 06:48:36 -0700
Subject: [PATCH] Added support for raw mouse and keyboard using GameInput on
 Windows

Fixes https://github.com/libsdl-org/SDL/issues/10442
---
 VisualC-GDK/SDL/SDL.vcxproj              |   2 +
 VisualC-GDK/SDL/SDL.vcxproj.filters      |   2 +
 VisualC/SDL/SDL.vcxproj                  |   2 +
 VisualC/SDL/SDL.vcxproj.filters          |   6 +
 include/SDL3/SDL_hints.h                 |  14 +
 src/video/windows/SDL_windowsevents.c    |   8 +-
 src/video/windows/SDL_windowsgameinput.c | 625 +++++++++++++++++++++++
 src/video/windows/SDL_windowsgameinput.h |  29 ++
 src/video/windows/SDL_windowsrawinput.c  |  26 +-
 src/video/windows/SDL_windowsvideo.c     |  24 +-
 src/video/windows/SDL_windowsvideo.h     |   3 +
 11 files changed, 729 insertions(+), 12 deletions(-)
 create mode 100644 src/video/windows/SDL_windowsgameinput.c
 create mode 100644 src/video/windows/SDL_windowsgameinput.h

diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index 77fc0785a83a7..8d038f6d3153c 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/VisualC-GDK/SDL/SDL.vcxproj
@@ -573,6 +573,7 @@
     <ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowskeyboard.h" />
+    <ClInclude Include="..\..\src\video\windows\SDL_windowsgameinput.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmessagebox.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmodes.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmouse.h" />
@@ -850,6 +851,7 @@
     <ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowskeyboard.c" />
+    <ClCompile Include="..\..\src\video\windows\SDL_windowsgameinput.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmessagebox.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmodes.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmouse.c" />
diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index f3cd8ca92b09f..fbd091f59066e 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/VisualC-GDK/SDL/SDL.vcxproj.filters
@@ -217,6 +217,7 @@
     <ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowskeyboard.c" />
+    <ClCompile Include="..\..\src\video\windows\SDL_windowsgameinput.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmessagebox.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmodes.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmouse.c" />
@@ -447,6 +448,7 @@
     <ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowskeyboard.h" />
+    <ClInclude Include="..\..\src\video\windows\SDL_windowsgameinput.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmessagebox.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmodes.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmouse.h" />
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index 4cb8de774c47f..f8111cf344d9d 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -479,6 +479,7 @@
     <ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowskeyboard.h" />
+    <ClInclude Include="..\..\src\video\windows\SDL_windowsgameinput.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmessagebox.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmodes.h" />
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmouse.h" />
@@ -708,6 +709,7 @@
     <ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowskeyboard.c" />
+    <ClCompile Include="..\..\src\video\windows\SDL_windowsgameinput.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmessagebox.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmodes.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmouse.c" />
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index d58f179c076ba..57b5a527a7050 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/VisualC/SDL/SDL.vcxproj.filters
@@ -684,6 +684,9 @@
     <ClInclude Include="..\..\src\video\windows\SDL_windowskeyboard.h">
       <Filter>video\windows</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\video\windows\SDL_windowsgameinput.h">
+      <Filter>video\windows</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\src\video\windows\SDL_windowsmessagebox.h">
       <Filter>video\windows</Filter>
     </ClInclude>
@@ -1340,6 +1343,9 @@
     <ClCompile Include="..\..\src\video\windows\SDL_windowskeyboard.c">
       <Filter>video\windows</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\video\windows\SDL_windowsgameinput.c">
+      <Filter>video\windows</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\video\windows\SDL_windowsmessagebox.c">
       <Filter>video\windows</Filter>
     </ClCompile>
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 7f4391c4ccc9f..d8bbfb47283a1 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -3823,6 +3823,20 @@ extern "C" {
  */
 #define SDL_HINT_WINDOWS_ENABLE_MESSAGELOOP "SDL_WINDOWS_ENABLE_MESSAGELOOP"
 
+/**
+ * A variable controlling whether GameInput is used for raw keyboard and mouse on Windows.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": GameInput is not used for raw keyboard and mouse events.
+ * - "1": GameInput is used for raw keyboard and mouse events, if available. (default)
+ *
+ * This hint should be set before SDL is initialized.
+ *
+ * \since This hint is available since SDL 3.0.0.
+ */
+#define SDL_HINT_WINDOWS_GAMEINPUT   "SDL_WINDOWS_GAMEINPUT"
+
 /**
  * A variable controlling whether raw keyboard events are used on Windows.
  *
diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index c5d36039726af..367f8f027399d 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -2233,6 +2233,10 @@ void WIN_PumpEvents(SDL_VideoDevice *_this)
     SDL_Window *focusWindow;
 #endif
 
+    if (_this->internal->gameinput_context) {
+        WIN_UpdateGameInput(_this);
+    }
+
     if (g_WindowsEnableMessageLoop) {
         SDL_processing_messages = SDL_TRUE;
 
@@ -2310,7 +2314,9 @@ void WIN_PumpEvents(SDL_VideoDevice *_this)
     /* Update mouse capture */
     WIN_UpdateMouseCapture();
 
-    WIN_CheckKeyboardAndMouseHotplug(_this, SDL_FALSE);
+    if (!_this->internal->gameinput_context) {
+        WIN_CheckKeyboardAndMouseHotplug(_this, SDL_FALSE);
+    }
 
     WIN_UpdateIMECandidates(_this);
 
diff --git a/src/video/windows/SDL_windowsgameinput.c b/src/video/windows/SDL_windowsgameinput.c
new file mode 100644
index 0000000000000..d92eb27cdd7ed
--- /dev/null
+++ b/src/video/windows/SDL_windowsgameinput.c
@@ -0,0 +1,625 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 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"
+
+#include "SDL_windowsvideo.h"
+
+#if defined(__has_include) && __has_include(<GameInput.h>)
+#define HAVE_GAMEINPUT_H
+#endif
+
+#ifdef HAVE_GAMEINPUT_H
+
+#include <stdbool.h>
+#define COBJMACROS
+#include <GameInput.h>
+
+#include "../../events/SDL_mouse_c.h"
+#include "../../events/SDL_keyboard_c.h"
+#include "../../events/scancodes_windows.h"
+
+
+#define MAX_GAMEINPUT_BUTTONS   7   // GameInputMouseWheelTiltRight is the highest button
+
+static const Uint8 GAMEINPUT_button_map[MAX_GAMEINPUT_BUTTONS] = {
+    SDL_BUTTON_LEFT,
+    SDL_BUTTON_RIGHT,
+    SDL_BUTTON_MIDDLE,
+    SDL_BUTTON_X1,
+    SDL_BUTTON_X2,
+    6,
+    7
+};
+
+typedef struct GAMEINPUT_Device
+{
+    IGameInputDevice *pDevice;
+    const GameInputDeviceInfo *info;
+    char *name;
+    Uint32 instance_id; /* generated by SDL */
+    SDL_bool registered;
+    SDL_bool delete_requested;
+    IGameInputReading *last_mouse_reading;
+    IGameInputReading *last_keyboard_reading;
+} GAMEINPUT_Device;
+
+struct WIN_GameInputData
+{
+    void *hGameInputDLL;
+    IGameInput *pGameInput;
+    GameInputCallbackToken gameinput_callback_token;
+    int num_devices;
+    GAMEINPUT_Device **devices;
+    GameInputKind enabled_input;
+    SDL_Mutex *lock;
+    uint64_t timestamp_offset;
+};
+
+static int GAMEINPUT_InternalAddOrFind(WIN_GameInputData *data, IGameInputDevice *pDevice)
+{
+    GAMEINPUT_Device **devicelist = NULL;
+    GAMEINPUT_Device *device = NULL;
+    const GameInputDeviceInfo *info;
+    int retval = -1;
+
+    info = IGameInputDevice_GetDeviceInfo(pDevice);
+
+    SDL_LockMutex(data->lock);
+    {
+        for (int i = 0; i < data->num_devices; ++i) {
+            device = data->devices[i];
+            if (device && device->pDevice == pDevice) {
+                /* we're already added */
+                device->delete_requested = SDL_FALSE;
+                retval = 0;
+                goto done;
+            }
+        }
+
+        device = (GAMEINPUT_Device *)SDL_calloc(1, sizeof(*device));
+        if (!device) {
+            goto done;
+        }
+
+        devicelist = (GAMEINPUT_Device **)SDL_realloc(data->devices, (data->num_devices + 1) * sizeof(*devicelist));
+        if (!devicelist) {
+            SDL_free(device);
+            goto done;
+        }
+
+        if (info->deviceStrings) {
+            /* In theory we could get the manufacturer and product strings here, but they're NULL for all the devices I've tested */
+        }
+
+        if (info->displayName) {
+            /* This could give us a product string, but it's NULL for all the devices I've tested */
+        }
+
+        IGameInputDevice_AddRef(pDevice);
+        device->pDevice = pDevice;
+        device->instance_id = SDL_GetNextObjectID();
+        device->info = info;
+
+        data->devices = devicelist;
+        data->devices[data->num_devices++] = device;
+
+        retval = 0;
+    }
+done:
+    SDL_UnlockMutex(data->lock);
+
+    return retval;
+}
+
+static int GAMEINPUT_InternalRemoveByIndex(WIN_GameInputData *data, int idx)
+{
+    GAMEINPUT_Device **devicelist = NULL;
+    GAMEINPUT_Device *device;
+    int retval = -1;
+
+    SDL_LockMutex(data->lock);
+    {
+        if (idx < 0 || idx >= data->num_devices) {
+            retval = SDL_SetError("GAMEINPUT_InternalRemoveByIndex argument idx %d is out of range", idx);
+            goto done;
+        }
+
+        device = data->devices[idx];
+        if (device) {
+            if (device->registered) {
+                if (device->info->supportedInput & GameInputKindMouse) {
+                    SDL_RemoveMouse(device->instance_id, SDL_TRUE);
+                }
+                if (device->info->supportedInput & GameInputKindKeyboard) {
+                    SDL_RemoveKeyboard(device->instance_id, SDL_TRUE);
+                }
+                if (device->last_mouse_reading) {
+                    IGameInputReading_Release(device->last_mouse_reading);
+                    device->last_mouse_reading = NULL;
+                }
+                if (device->last_keyboard_reading) {
+                    IGameInputReading_Release(device->last_keyboard_reading);
+                    device->last_keyboard_reading = NULL;
+                }
+            }
+            IGameInputDevice_Release(device->pDevice);
+            SDL_free(device->name);
+            SDL_free(device);
+        }
+        data->devices[idx] = NULL;
+
+        if (data->num_devices == 1) {
+            /* last element in the list, free the entire list then */
+            SDL_free(data->devices);
+            data->devices = NULL;
+        } else {
+            if (idx != data->num_devices - 1) {
+                size_t bytes = sizeof(*devicelist) * (data->num_devices - idx);
+                SDL_memmove(&data->devices[idx], &data->devices[idx + 1], bytes);
+            }
+        }
+
+        /* decrement the count and return */
+        retval = data->num_devices--;
+    }
+done:
+    SDL_UnlockMutex(data->lock);
+
+    return retval;
+}
+
+static void CALLBACK GAMEINPUT_InternalDeviceCallback(
+    _In_ GameInputCallbackToken callbackToken,
+    _In_ void* context,
+    _In_ IGameInputDevice *pDevice,
+    _In_ uint64_t timestamp,
+    _In_ GameInputDeviceStatus currentStatus,
+    _In_ GameInputDeviceStatus previousStatus)
+{
+    WIN_GameInputData *data = (WIN_GameInputData *)context;
+    int idx = 0;
+    GAMEINPUT_Device *device = NULL;
+
+    if (!pDevice) {
+        /* This should never happen, but ignore it if it does */
+        return;
+    }
+
+    if (currentStatus & GameInputDeviceConnected) {
+        GAMEINPUT_InternalAddOrFind(data, pDevice);
+    } else {
+        for (idx = 0; idx < data->num_devices; ++idx) {
+            device = data->devices[idx];
+            if (device && device->pDevice == pDevice) {
+                /* will be deleted on the next Detect call */
+                device->delete_requested = SDL_TRUE;
+                break;
+            }
+        }
+    }
+}
+
+int WIN_InitGameInput(SDL_VideoDevice *_this)
+{
+    WIN_GameInputData *data;
+    HRESULT hr;
+    int retval = -1;
+
+    if (_this->internal->gameinput_context) {
+        return 0;
+    }
+
+    data = (WIN_GameInputData *)SDL_calloc(1, sizeof(*data));
+    if (!data) {
+        goto done;
+    }
+    _this->internal->gameinput_context = data;
+
+    data->lock = SDL_CreateMutex();
+    if (!data->lock) {
+        goto done;
+    }
+
+    data->hGameInputDLL = SDL_LoadObject("gameinput.dll");
+    if (!data->hGameInputDLL) {
+        goto done;
+    }
+
+    typedef HRESULT (WINAPI *GameInputCreate_t)(IGameInput * *gameInput);
+    GameInputCreate_t GameInputCreateFunc = (GameInputCreate_t)SDL_LoadFunction(data->hGameInputDLL, "GameInputCreate");
+    if (!GameInputCreateFunc) {
+        goto done;
+    }
+
+    hr = GameInputCreateFunc(&data->pGameInput);
+    if (FAILED(hr)) {
+        SDL_SetError("GameInputCreate failure with HRESULT of %08X", hr);
+        goto done;
+    }
+
+    hr = IGameInput_RegisterDeviceCallback(data->pGameInput,
+                                           NULL,
+                                           (GameInputKindMouse | GameInputKindKeyboard),
+                                           GameInputDeviceConnected,
+                                           GameInputBlockingEnumeration,
+                                           data,
+                                           GAMEINPUT_InternalDeviceCallback,
+                                           &data->gameinput_callback_token);
+    if (FAILED(hr)) {
+        SDL_SetError("IGameInput::RegisterDeviceCallback failure with HRESULT of %08X", hr);
+        goto done;
+    }
+
+    // Calculate the relative offset between SDL timestamps and GameInput timestamps
+    Uint64 now = SDL_GetTicksNS();
+    uint64_t timestampUS = IGameInput_GetCurrentTimestamp(data->pGameInput);
+    data->timestamp_offset = (SDL_NS_TO_US(now) - timestampUS);
+
+    retval = 0;
+
+done:
+    if (retval < 0) {
+        WIN_QuitGameInput(_this);
+    }
+    return retval;
+}
+
+static void GAMEINPUT_InitialMouseReading(WIN_GameInputData *data, SDL_Window *window, GAMEINPUT_Device *device, IGameInputReading *reading)
+{
+    GameInputMouseState state;
+    if (SUCCEEDED(IGameInputReading_GetMouseState(reading, &state))) {
+        Uint64 timestamp = SDL_US_TO_NS(IGameInputReading_GetTimestamp(reading) + data->timestamp_offset);
+        SDL_MouseID mouseID = device->instance_id;
+
+        for (int i = 0; i < MAX_GAMEINPUT_BUTTONS; ++i) {
+            const GameInputMouseButtons mask = (1 << i);
+            SDL_SendMouseButton(timestamp, window, mouseID, (state.buttons & mask) ? SDL_PRESSED : SDL_RELEASED, GAMEINPUT_button_map[i]);
+        }
+    }
+}
+
+static void GAMEINPUT_HandleMouseDelta(WIN_GameInputData *data, SDL_Window *window, GAMEINPUT_Device *device, IGameInputReading *last_reading, IGameInputReading *reading)
+{
+    GameInputMouseState last;
+    GameInputMouseState state;
+    if (SUCCEEDED(IGameInputReading_GetMouseState(last_reading, &last)) &&
+        SUCCEEDED(IGameInputReading_GetMouseState(reading, &state))) {
+        Uint64 timestamp = SDL_US_TO_NS(IGameInputReading_GetTimestamp(reading) + data->timestamp_offset);
+        SDL_MouseID mouseID = device->instance_id;
+
+        GameInputMouseState delta;
+        delta.buttons = (state.buttons ^ last.buttons);
+        delta.positionX = (state.positionX - last.positionX);
+        delta.positionY = (state.positionY - last.positionY);
+        delta.wheelX = (state.wheelX - last.wheelX);
+        delta.wheelY = (state.wheelY - last.wheelY);
+
+        if (delta.positionX || delta.positionY) {
+            SDL_SendMouseMotion(timestamp, window, mouseID, SDL_TRUE, (float)delta.positionX, (float)delta.positionY);
+        }
+        if (delta.buttons) {
+            for (int i = 0; i < MAX_GAMEINPUT_BUTTONS; ++i) {
+                const GameInputMouseButtons mask = (1 << i); 
+                if (delta.buttons & mask) {
+                    SDL_SendMouseButton(timestamp, window, mouseID, (state.buttons & mask) ? SDL_PRESSED : SDL_RELEASED, GAMEINPUT_button_map[i]);
+                }
+            }
+        }
+        if (delta.wheelX || delta.wheelY) {
+            float fAmountX = (float)delta.wheelX / WHEEL_DELTA;
+            float fAmountY = (float)delta.wheelY / WHEEL_DELTA;
+            SDL_SendMouseWheel(timestamp, SDL_GetMouseFocus(), device->instance_id, fAmountX, fAmountY, SDL_MOUSEWHEEL_NORMAL);
+        }
+    }
+}
+
+static SDL_Scancode GetScancodeFromKeyState(const GameInputKeyState *state)
+{
+    Uint8 index = (Uint8)(state->scanCode & 0xFF);
+    if ((state->scanCode & 0xFF00) == 0xE000) {
+        index |= 0x80;
+    }
+    return windows_scancode_table[index];
+}
+
+static SDL_bool KeysHaveScancode(const GameInputKeyState *keys, uint32_t count, SDL_Scancode scancode)
+{
+    for (uint32_t i = 0; i < count; ++i) {
+        if (GetScancodeFromKeyState(&keys[i]) == scancode) {
+            return SDL_TRUE;
+        }
+    }
+    return SDL_FALSE;
+}
+
+static void GAMEINPUT_InitialKeyboardReading(WIN_GameInputData *data, SDL_Window *window, GAMEINPUT_Device *device, IGameInputReading *reading)
+{
+    Uint64 timestamp = SDL_US_TO_NS(IGameInputReading_GetTimestamp(reading) + data->timestamp_offset);
+    SDL_KeyboardID keyboardID = device->instance_id;
+
+    uint32_t max_keys = device->info->keyboardInfo->maxSimultaneousKeys;
+    GameInputKeyState *keys = SDL_stack_alloc(GameInputKeyState, max_keys);
+    if (!keys) {
+        return;
+    }
+
+    uint32_t num_keys = IGameInputReading_GetKeyState(reading, max_keys, keys);
+    if (!num_keys) {
+        // FIXME: We probably need to track key state by keyboardID
+        SDL_ResetKeyboard();
+        return;
+    }
+
+    // Go through and send key up events for any key that's not held down
+    int num_scancodes;
+    const Uint8 *keyboard_state = SDL_GetKeyboardState(&num_scancodes);
+    for (int i = 0; i < num_scancodes; ++i) {
+        if (keyboard_state[i] && !KeysHaveScancode(keys, num_keys, (SDL_Scancode)i)) {
+            SDL_SendKeyboardKey(timestamp, keyboardID, keys[i].scanCode, (SDL_Scancode)i, SDL_RELEASED);
+        }
+    }
+
+    // Go through and send key down events for any key that's held down
+    for (uint32_t i = 0; i < num_keys; ++i) {
+        SDL_SendKeyboardKey(timestamp, keyboardID, keys[i].scanCode, GetScancodeFromKeyState(&keys[i]), SDL_PRESSED);
+    }
+}
+
+#ifdef DEBUG_KEYS
+static void DumpKeys(const char *prefix, GameInputKeyState *keys, uint32_t count)
+{
+    SDL_Log("%s", prefix);
+    for (uint32_t i = 0; i < count; ++i) {
+        char str[5];
+        *SDL_UCS4ToUTF8(keys[i].codePoint, str) = '\0';
+        SDL_Log("    Key 0x%.2x (%s)\n", keys[i].scanCode, str);
+    }
+}
+#endif // DEBUG_KEYS
+
+static void GAMEINPUT_HandleKeyboardDelta(WIN_GameInputData *data, SDL_Window *window, GAMEINPUT_Device *device, IGameInputReading *last_reading, IGameInputReading *reading)
+{
+    Uint64 timestamp = SDL_US_TO_NS(IGameInputReading_GetTimestamp(reading) + data->timestamp_offset);
+    SDL_KeyboardID keyboardID = device->instance_id;
+
+    uint32_t max_keys = device->info->keyboardInfo->maxSimultaneousKeys;
+    GameInputKeyState *last = SDL_stack_alloc(GameInputKeyState, max_keys);
+    GameInputKeyState *keys = SDL_stack_alloc(GameInputKeyState, max_keys);
+    if (!last || !keys) {
+        return;
+    }
+
+    uint32_t index_last = 0;
+    uint32_t index_keys = 0;
+    uint32_t num_last = IGameInputReading_GetKeyState(last_reading, max_keys, last);
+    uint32_t num_keys = IGameInputReading_GetKeyState(reading, max_keys, keys);
+#ifdef DEBUG_KEYS
+    SDL_Log("Timestamp: %llu\n", timestamp);
+    DumpKeys("Last keys:", last, num_last);
+    DumpKeys("New keys:", keys, num_keys);
+#endif
+    while (index_last < num_last || index_keys < num_keys) {
+        if (index_last < num_last && index_keys < num_keys) {
+            if (last[index_last].scanCode == keys[index_keys].scanCode) {
+                // No change
+                ++index_last;
+                ++index_keys;
+            } else {
+                // This key was released
+                SDL_SendKeyboardKey(timestamp, keyboardID, last[index_last].scanCode, GetScancodeFromKeyState(&last[index_last]), SDL_RELEASED);
+                ++index_last;
+            }
+        } else if (index_last < num_last) {
+            // This key was released
+            SDL_SendKeyboardKey(timestamp, keyboardID, last[index_last].scanCode, GetScancodeFromKeyState(&last[index_last]), SDL_RELEASED);
+            ++index_last;
+        } else {
+            // This key was pressed
+            SDL_SendKeyboardKey(timestamp, keyboardID, keys[index_keys].scanCode, GetScancodeFromKeyState(&keys[index_keys]), SDL_PRESSED);
+            ++index_keys;
+        }
+    }
+}
+
+void WIN_UpdateGameInput(SDL_VideoDevice *_this)
+{
+    WIN_GameInputData *data = _this->internal->gameinput_context;
+
+    SDL_LockMutex(data->lock);
+    {
+        // Key events and relative mouse motion both go to the window with keyboard focus
+        SDL_Window *window = SDL_GetKeyboardFocus();
+
+        for (int i = 0; i < data->num_devices; ++i) {
+            GAMEINPUT_Device *device = data->devices[i];
+            IGameInputReading *reading;
+
+            if (!device->registered) {
+                if (device->info->supportedInput & GameInputKindMouse) {
+                    SDL_AddMouse(device->instance_id, device->name, SDL_TRUE);
+                }
+                if (device->info->supportedInput & GameInputKindKeyboard) {
+                    SDL_AddKeyboard(device->instance_id, device->name, SDL_TRUE);
+                }
+                device->registered = SDL_TRUE;
+            }
+
+            if (device->delete_requested) {
+                GAMEINPUT_InternalRemoveByIndex(data, i--);
+                continue;
+            }
+
+            if (!(device->info->supportedInput & data->enabled_input)) {
+                continue;
+            }
+
+            if (!window) {
+                continue;
+            }
+
+            if (data->enabled_input & GameInputKindMouse) {
+                if (device->last_mouse_reading) {
+                    HRESULT hr;
+                    while (SUCCEEDED(hr = IGameInput_GetNextReading(data->pGameInput, device->last_mouse_reading, GameInputKindMouse, device->pDevice, &reading))) {
+                        GAMEINPUT_HandleMouseDelta(data, window, device, device->last_mouse_reading, reading);
+                        IGameInputReading_Release(device->last_mouse_reading);
+                        device->last_mouse_reading = reading;
+                    }
+                    if (hr != GAMEINPUT_E_READING_NOT_FOUND) {
+                        // The last reading is too old, resynchronize
+                        IGameInputReading_Release(device->last_mouse_reading);
+                        device->last_mouse_reading = NULL;
+                    }
+                }
+                if (!device->last_mouse_reading) {
+                    if (SUCCEEDED(IGameInput_GetCurrentReading(data->pGameInput, GameInputKindMouse, device->pDevice, &reading))) {
+                        GAMEINPUT_InitialMouseReading(data, window, device, reading);
+                        device->last_mouse_reading = reading;
+                    }
+                }
+            }
+
+            if (data->enabled_input & GameInputKindKeyboard) {
+                if (window->text_input_active) {
+                    // Reset raw input while text input is active
+                    if (device->last_keyboard_reading) {
+                        IGameInputReading_Release(device->last_keyboard_reading);
+                        device->last_keyboard_reading = NULL;
+                    }
+                } else {
+                    if (device->last_keyboard_reading) {
+                        HRESULT hr;
+                        while (SUCCEEDED(hr = IGameInput_GetNextReading(data->pGameInput, device->last_keyboard_reading, GameInputKindKeyboard, device->pDevice, &reading))) {
+                            GAMEINPUT_HandleKeyboardDelta(data, window, device, device->last_keyboard_reading, reading);
+                            IGameInputReading_Release(device->last_keyboard_reading);
+                            device->last_keyboard_reading = reading;
+                        }
+                        if (hr != GAMEINPUT_E_READING_NOT_FOUND) {
+                            // The last reading is too old, resynchronize
+                            IGameInputReading_Release(device->last_keyboard_reading);
+                            device->last_keyboard_reading = NULL;
+                        }
+                    }
+                    if (!device->last_keyboard_reading) {
+                        if (SUCCEEDED(IGameInput_GetCurrentReading(data->pGameInput, GameInputKindKeyboard, device->pDevice, &reading))) {
+                            GAMEINPUT_InitialKeyboardReading(data, window, device, reading);
+                            device->last_keyboard_reading = reading;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    SDL_UnlockMutex(data->lock);
+}
+
+int WIN_UpdateGameInputEnabled(SDL_VideoDevice *_this)
+{
+    WIN_GameInputData *data = _this->internal->gameinput_context;
+    SDL_bool raw_mouse_enabled = _this->internal->raw_mouse_enabled;
+    SDL_bool raw_keyboard_enabled = _this->internal->raw_keyboard_enabled;
+
+    SDL_LockMutex(data->lock);
+    {
+        data->enabled_input = (raw_mouse_enabled ? GameInputKindMouse : 0) |
+                             (raw_keyboard_enabled ? GameInputKindKeyboard : 0);
+
+        // Reset input if not enabled
+        for (int i = 0; i < data->num_devices; ++i) {
+            GAMEINPUT_Device *device = data->devices[i];
+
+            if (device->last_mouse_reading && !raw_mouse_enabled) {
+                IGameInputReading_Release(device->last_mouse_reading);
+                device->last_mouse_reading = NULL;
+            }
+
+            if (device->last_keyboard_reading && !raw_keyboard_enabled) {
+                IGameInputReading_Release(device->last_keyboard_reading);
+                device->last_keyboard_reading = NULL;
+            }
+        }
+    }
+    SDL_UnlockMutex(data->lock);
+
+    return 0;
+}
+
+void WIN_QuitGameInput(SDL_VideoDevice *_this)
+{
+    WIN_GameInputData *data = _this->internal->gameinput_context;
+
+    if (!data) {
+        return;
+    }
+
+    if (data->pGameInput) {
+        /* free the callback */
+        if (data->gameinput_callback_token != GAMEINPUT_INVALID_CALLBACK_TOKEN_VALUE) {
+            IGameInput_UnregisterCallback(data->pGameInput, data->gameinput_callback_token, /*timeoutInUs:*/ 10000);
+            data->gameinput_callback_token = GAMEINPUT_INVALID_CALLBACK_TOKEN_VALUE;
+        }
+
+        /* free the list */
+        while (data->num_devices > 0) {
+            GAMEINPUT_InternalRemoveByIndex(data, 0);
+        }
+
+        IGameInput_Release(data->pGameInput);
+        data->pGameInput = NULL;
+    }
+
+    if (data->hGameInputDLL) {
+        SDL_UnloadObject(data->hGameInputDLL);
+        data->hGameInputDLL = NULL;
+    }
+
+    if (data->lock) {
+        SDL_DestroyMutex(data->lock);
+        data->lock = NULL;
+    }
+
+    SDL_free(data);
+    _this->internal->gameinput_context = NULL;
+}
+
+#else /* !HAVE_GAMEINPUT_H */
+
+int WIN_InitGameInput(SDL_VideoDevice* _this)
+{
+    return SDL_Unsupported();
+}
+
+int WIN_UpdateGameInputEnabled(SDL_VideoDevice *_this)
+{
+    return SDL_Unsupported();
+}
+
+void WIN_UpdateGameInput(SDL_VideoDevice* _this)
+{
+    return;
+}
+
+void WIN_QuitGameInput(SDL_VideoDevice* _this)
+{
+    return;
+}
+
+#endif /* HAVE_GAMEINPUT_H */
diff --git a/src/video/windows/SDL_windowsgameinput.h b/src/video/windows/SDL_windowsgameinput.h
new file mode 100644
index 0000000000000..58ef19a367e4c
--- /dev/null
+++ b/src/video/windows/SDL_windowsgameinput.h
@@ -0,0 +1,29 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 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. Altere

(Patch may be truncated, please check the link at the top of this post.)