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