| /* |
| Simple DirectMedia Layer |
| Copyright (C) 1997-2026 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" |
| |
| #ifdef HAVE_GAMEINPUT_H |
| |
| #include "../../core/windows/SDL_gameinput.h" |
| extern "C" { |
| #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 |
| bool registered; |
| bool delete_requested; |
| IGameInputReading *last_mouse_reading; |
| IGameInputReading *last_keyboard_reading; |
| } GAMEINPUT_Device; |
| |
| struct WIN_GameInputData |
| { |
| IGameInput *pGameInput; |
| GameInputCallbackToken gameinput_callback_token; |
| int num_devices; |
| GAMEINPUT_Device **devices; |
| GameInputKind enabled_input; |
| SDL_Mutex *lock; |
| uint64_t timestamp_offset; |
| }; |
| |
| static bool GAMEINPUT_InternalAddOrFind(WIN_GameInputData *data, IGameInputDevice *pDevice) |
| { |
| GAMEINPUT_Device **devicelist = NULL; |
| GAMEINPUT_Device *device = NULL; |
| const GameInputDeviceInfo *info; |
| bool result = false; |
| |
| #if GAMEINPUT_API_VERSION >= 1 |
| HRESULT hr = pDevice->GetDeviceInfo(&info); |
| if (FAILED(hr)) { |
| return WIN_SetErrorFromHRESULT("IGameInputDevice_GetDeviceInfo", hr); |
| } |
| #else |
| info = pDevice->GetDeviceInfo(); |
| #endif |
| |
| 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 = false; |
| result = true; |
| 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->displayName) { |
| // This could give us a product string, but it's NULL for all the devices I've tested |
| } |
| |
| pDevice->AddRef(); |
| device->pDevice = pDevice; |
| device->instance_id = SDL_GetNextObjectID(); |
| device->info = info; |
| |
| data->devices = devicelist; |
| data->devices[data->num_devices++] = device; |
| |
| result = true; |
| } |
| done: |
| SDL_UnlockMutex(data->lock); |
| |
| return result; |
| } |
| |
| static bool GAMEINPUT_InternalRemoveByIndex(WIN_GameInputData *data, int idx) |
| { |
| GAMEINPUT_Device **devicelist = NULL; |
| GAMEINPUT_Device *device; |
| bool result = false; |
| |
| SDL_LockMutex(data->lock); |
| { |
| if (idx < 0 || idx >= data->num_devices) { |
| result = 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); |
| } |
| if (device->info->supportedInput & GameInputKindKeyboard) { |
| SDL_RemoveKeyboard(device->instance_id); |
| } |
| if (device->last_mouse_reading) { |
| device->last_mouse_reading->Release(); |
| device->last_mouse_reading = NULL; |
| } |
| if (device->last_keyboard_reading) { |
| device->last_keyboard_reading->Release(); |
| device->last_keyboard_reading = NULL; |
| } |
| } |
| device->pDevice->Release(); |
| 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 - 1); |
| SDL_memmove(&data->devices[idx], &data->devices[idx + 1], bytes); |
| } |
| } |
| |
| // decrement the count and return |
| --data->num_devices; |
| result = true; |
| } |
| done: |
| SDL_UnlockMutex(data->lock); |
| |
| return result; |
| } |
| |
| 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 = true; |
| break; |
| } |
| } |
| } |
| } |
| |
| bool WIN_InitGameInput(SDL_VideoDevice *_this) |
| { |
| WIN_GameInputData *data; |
| HRESULT hr; |
| Uint64 now; |
| uint64_t timestampUS; |
| bool result = false; |
| |
| if (_this->internal->gameinput_context) { |
| return true; |
| } |
| |
| 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; |
| } |
| |
| if (!SDL_InitGameInput(&data->pGameInput)) { |
| goto done; |
| } |
| |
| hr = data->pGameInput->RegisterDeviceCallback(NULL, |
| (GameInputKindMouse | GameInputKindKeyboard), |
| GameInputDeviceConnected, |
| GameInputBlockingEnumeration, |
| data, |
| GAMEINPUT_InternalDeviceCallback, |
| &data->gameinput_callback_token); |
| if (FAILED(hr)) { |
| WIN_SetErrorFromHRESULT("IGameInput::RegisterDeviceCallback", hr); |
| goto done; |
| } |
| |
| // Calculate the relative offset between SDL timestamps and GameInput timestamps |
| now = SDL_GetTicksNS(); |
| timestampUS = data->pGameInput->GetCurrentTimestamp(); |
| data->timestamp_offset = (SDL_NS_TO_US(now) - timestampUS); |
| |
| result = true; |
| |
| done: |
| if (!result) { |
| WIN_QuitGameInput(_this); |
| } |
| return result; |
| } |
| |
| static void GAMEINPUT_InitialMouseReading(WIN_GameInputData *data, SDL_Window *window, GAMEINPUT_Device *device, IGameInputReading *reading) |
| { |
| GameInputMouseState state; |
| if (reading->GetMouseState(&state)) { |
| Uint64 timestamp = SDL_US_TO_NS(reading->GetTimestamp() + data->timestamp_offset); |
| SDL_MouseID mouseID = device->instance_id; |
| |
| for (int i = 0; i < MAX_GAMEINPUT_BUTTONS; ++i) { |
| const GameInputMouseButtons mask = GameInputMouseButtons(1 << i); |
| bool down = ((state.buttons & mask) != 0); |
| SDL_SendMouseButton(timestamp, window, mouseID, GAMEINPUT_button_map[i], down); |
| } |
| |
| // Invalidate mouse button flags |
| window->internal->mouse_button_flags = (WPARAM)-1; |
| } |
| } |
| |
| static void GAMEINPUT_HandleMouseDelta(WIN_GameInputData *data, SDL_Window *window, GAMEINPUT_Device *device, IGameInputReading *last_reading, IGameInputReading *reading) |
| { |
| GameInputMouseState last; |
| GameInputMouseState state; |
| if (last_reading->GetMouseState(&last) && reading->GetMouseState(&state)) { |
| Uint64 timestamp = SDL_US_TO_NS(reading->GetTimestamp() + 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, true, (float)delta.positionX, (float)delta.positionY); |
| } |
| if (delta.buttons) { |
| for (int i = 0; i < MAX_GAMEINPUT_BUTTONS; ++i) { |
| const GameInputMouseButtons mask = GameInputMouseButtons(1 << i); |
| if (delta.buttons & mask) { |
| bool down = ((state.buttons & mask) != 0); |
| SDL_SendMouseButton(timestamp, window, mouseID, GAMEINPUT_button_map[i], down); |
| } |
| } |
| |
| // Invalidate mouse button flags |
| window->internal->mouse_button_flags = (WPARAM)-1; |
| } |
| 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 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 true; |
| } |
| } |
| return false; |
| } |
| |
| static void GAMEINPUT_InitialKeyboardReading(WIN_GameInputData *data, SDL_Window *window, GAMEINPUT_Device *device, IGameInputReading *reading) |
| { |
| Uint64 timestamp = SDL_US_TO_NS(reading->GetTimestamp() + 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 = reading->GetKeyState(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 bool *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, false); |
| } |
| } |
| |
| // 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]), true); |
| } |
| } |
| |
| #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)", 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(reading->GetTimestamp() + 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 = last_reading->GetKeyState(max_keys, last); |
| uint32_t num_keys = reading->GetKeyState(max_keys, keys); |
| #ifdef DEBUG_KEYS |
| SDL_Log("Timestamp: %llu", 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]), false); |
| ++index_last; |
| } |
| } else if (index_last < num_last) { |
| // This key was released |
| SDL_SendKeyboardKey(timestamp, keyboardID, last[index_last].scanCode, GetScancodeFromKeyState(&last[index_last]), false); |
| ++index_last; |
| } else { |
| // This key was pressed |
| SDL_SendKeyboardKey(timestamp, keyboardID, keys[index_keys].scanCode, GetScancodeFromKeyState(&keys[index_keys]), true); |
| ++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); |
| } |
| if (device->info->supportedInput & GameInputKindKeyboard) { |
| SDL_AddKeyboard(device->instance_id, device->name); |
| } |
| device->registered = 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 = data->pGameInput->GetNextReading(device->last_mouse_reading, GameInputKindMouse, device->pDevice, &reading))) { |
| GAMEINPUT_HandleMouseDelta(data, window, device, device->last_mouse_reading, reading); |
| device->last_mouse_reading->Release(); |
| device->last_mouse_reading = reading; |
| } |
| if (hr != GAMEINPUT_E_READING_NOT_FOUND) { |
| if (SUCCEEDED(data->pGameInput->GetCurrentReading(GameInputKindMouse, device->pDevice, &reading))) { |
| GAMEINPUT_HandleMouseDelta(data, window, device, device->last_mouse_reading, reading); |
| device->last_mouse_reading->Release(); |
| device->last_mouse_reading = reading; |
| } |
| } |
| } else { |
| if (SUCCEEDED(data->pGameInput->GetCurrentReading(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) { |
| device->last_keyboard_reading->Release(); |
| device->last_keyboard_reading = NULL; |
| } |
| } else { |
| if (device->last_keyboard_reading) { |
| HRESULT hr; |
| while (SUCCEEDED(hr = data->pGameInput->GetNextReading(device->last_keyboard_reading, GameInputKindKeyboard, device->pDevice, &reading))) { |
| GAMEINPUT_HandleKeyboardDelta(data, window, device, device->last_keyboard_reading, reading); |
| device->last_keyboard_reading->Release(); |
| device->last_keyboard_reading = reading; |
| } |
| if (hr != GAMEINPUT_E_READING_NOT_FOUND) { |
| if (SUCCEEDED(data->pGameInput->GetCurrentReading(GameInputKindKeyboard, device->pDevice, &reading))) { |
| GAMEINPUT_HandleKeyboardDelta(data, window, device, device->last_keyboard_reading, reading); |
| device->last_keyboard_reading->Release(); |
| device->last_keyboard_reading = reading; |
| } |
| } |
| } else { |
| if (SUCCEEDED(data->pGameInput->GetCurrentReading(GameInputKindKeyboard, device->pDevice, &reading))) { |
| GAMEINPUT_InitialKeyboardReading(data, window, device, reading); |
| device->last_keyboard_reading = reading; |
| } |
| } |
| } |
| } |
| } |
| } |
| SDL_UnlockMutex(data->lock); |
| } |
| |
| bool WIN_UpdateGameInputEnabled(SDL_VideoDevice *_this) |
| { |
| WIN_GameInputData *data = _this->internal->gameinput_context; |
| bool raw_mouse_enabled = _this->internal->raw_mouse_enabled; |
| bool raw_keyboard_enabled = _this->internal->raw_keyboard_enabled; |
| |
| SDL_LockMutex(data->lock); |
| { |
| data->enabled_input = (raw_mouse_enabled ? GameInputKindMouse : GameInputKindUnknown) | |
| (raw_keyboard_enabled ? GameInputKindKeyboard : GameInputKindUnknown); |
| |
| // 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) { |
| device->last_mouse_reading->Release(); |
| device->last_mouse_reading = NULL; |
| } |
| |
| if (device->last_keyboard_reading && !raw_keyboard_enabled) { |
| device->last_keyboard_reading->Release(); |
| device->last_keyboard_reading = NULL; |
| } |
| } |
| } |
| SDL_UnlockMutex(data->lock); |
| |
| return true; |
| } |
| |
| 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) { |
| #if GAMEINPUT_API_VERSION >= 1 |
| data->pGameInput->UnregisterCallback(data->gameinput_callback_token); |
| #else |
| data->pGameInput->UnregisterCallback(data->gameinput_callback_token, 10000); |
| #endif |
| data->gameinput_callback_token = 0; |
| } |
| |
| // free the list |
| while (data->num_devices > 0) { |
| GAMEINPUT_InternalRemoveByIndex(data, 0); |
| } |
| |
| SDL_QuitGameInput(); |
| data->pGameInput = NULL; |
| } |
| |
| if (data->lock) { |
| SDL_DestroyMutex(data->lock); |
| data->lock = NULL; |
| } |
| |
| SDL_free(data); |
| _this->internal->gameinput_context = NULL; |
| } |
| |
| #else // !HAVE_GAMEINPUT_H |
| |
| bool WIN_InitGameInput(SDL_VideoDevice *_this) |
| { |
| return SDL_Unsupported(); |
| } |
| |
| bool 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 |