From 983e984afc742eec445431365ae87395298bf74f Mon Sep 17 00:00:00 2001 From: William Horvath Date: Fri, 20 Mar 2026 06:23:45 -0700 Subject: [PATCH] windowsrawinput: Unregister/register raw input devices as necessary. The thread is still created once and only shut down on WIN_VideoQuit, but the thread will unregister the devices when they're disabled to avoid wasting time processing raw input that won't be appreciated by anyone. This also allows RIDEV_NOLEGACY to be set on the devices, otherwise they swallow up the WM_MOUSE* events that would be going to the main thread when relative mode is disabled. That's out of scope for this change, though. --- src/video/windows/SDL_windowsrawinput.c | 307 +++++++++++++++--------- src/video/windows/SDL_windowsrawinput.h | 2 +- src/video/windows/SDL_windowsvideo.c | 4 +- src/video/windows/SDL_windowsvideo.h | 1 - 4 files changed, 200 insertions(+), 114 deletions(-) diff --git a/src/video/windows/SDL_windowsrawinput.c b/src/video/windows/SDL_windowsrawinput.c index 9bbb07d861..e9b4aad3d9 100644 --- a/src/video/windows/SDL_windowsrawinput.c +++ b/src/video/windows/SDL_windowsrawinput.c @@ -40,7 +40,7 @@ typedef struct { bool done; - Uint32 flags; + SDL_AtomicU32 flags; // Thread sets this to the actually-set flags if updating state failed HANDLE ready_event; HANDLE signal_event; HANDLE thread; @@ -48,13 +48,102 @@ typedef struct static RawInputThreadData thread_data = { 0 }; +typedef enum +{ + RINP_QUIT, + RINP_UPDATE, + RINP_CONTINUE +} RawInputIterateResult; + +static RawInputIterateResult IterateRawInputThread(void) +{ + // The high-order word of GetQueueStatus() will let us know if there's immediate raw input to be processed. + // A (necessary!) side effect is that it also marks the message queue bits as stale, + // so MsgWaitForMultipleObjects will block. + + // Any pending flag update won't be processed until the queue drains, but this is + // at most one poll cycle since GetQueueStatus clears the wake bits. + if (HIWORD(GetQueueStatus(QS_RAWINPUT)) & QS_RAWINPUT) { + return thread_data.done ? RINP_QUIT : RINP_CONTINUE; + } + + static const DWORD WAIT_SIGNAL = WAIT_OBJECT_0; + static const DWORD WAIT_INPUT = WAIT_OBJECT_0 + 1; + + // There wasn't anything, so we'll wait until new data (or signal_event) wakes us up. + DWORD wait_status = MsgWaitForMultipleObjects(1, &thread_data.signal_event, FALSE, INFINITE, QS_RAWINPUT); + if (wait_status == WAIT_SIGNAL) { + // signal_event can only be set if we need to update or quit. + return thread_data.done ? RINP_QUIT : RINP_UPDATE; + } else if (wait_status == WAIT_INPUT) { + return RINP_CONTINUE; + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "Raw input thread exiting, unexpected wait result: %lu", wait_status); + return RINP_QUIT; + } +} + +static bool UpdateRawInputDeviceFlags(HWND window, Uint32 last_flags, Uint32 new_flags) +{ + // We had nothing enabled, and we're trying to stop everything. Nothing to do. + if (last_flags == new_flags) { + return true; + } + + RAWINPUTDEVICE devices[2] = { 0 }; + UINT count = 0; + + if ((new_flags & ENABLE_RAW_MOUSE_INPUT) != (last_flags & ENABLE_RAW_MOUSE_INPUT)) { + devices[count].usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP; + devices[count].usUsage = USB_USAGE_GENERIC_MOUSE; + + if (new_flags & ENABLE_RAW_MOUSE_INPUT) { + devices[count].dwFlags = 0; + devices[count].hwndTarget = window; + } else { + devices[count].dwFlags = RIDEV_REMOVE; + devices[count].hwndTarget = NULL; + } + + ++count; + } + + const Uint32 old_kb_flags = last_flags & (ENABLE_RAW_KEYBOARD_INPUT | RAW_KEYBOARD_FLAG_NOHOTKEYS | RAW_KEYBOARD_FLAG_INPUTSINK); + const Uint32 new_kb_flags = new_flags & (ENABLE_RAW_KEYBOARD_INPUT | RAW_KEYBOARD_FLAG_NOHOTKEYS | RAW_KEYBOARD_FLAG_INPUTSINK); + + if (old_kb_flags != new_kb_flags) { + devices[count].usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP; + devices[count].usUsage = USB_USAGE_GENERIC_KEYBOARD; + + if (new_kb_flags & ENABLE_RAW_KEYBOARD_INPUT) { + devices[count].dwFlags = 0; + devices[count].hwndTarget = window; + if (new_kb_flags & RAW_KEYBOARD_FLAG_NOHOTKEYS) { + devices[count].dwFlags |= RIDEV_NOHOTKEYS; + } + + if (new_kb_flags & RAW_KEYBOARD_FLAG_INPUTSINK) { + devices[count].dwFlags |= RIDEV_INPUTSINK; + } + } else { + devices[count].dwFlags = RIDEV_REMOVE; + devices[count].hwndTarget = NULL; + } + + ++count; + } + + if (RegisterRawInputDevices(devices, count, sizeof(devices[0]))) { + return true; + } + return false; +} + static DWORD WINAPI WIN_RawInputThread(LPVOID param) { SDL_VideoDevice *_this = SDL_GetVideoDevice(); - RawInputThreadData *data = (RawInputThreadData *)param; - RAWINPUTDEVICE devices[2]; + Uint32 last_flags = 0; HWND window; - UINT count = 0; SDL_SYS_SetupThread("SDLRawInput"); @@ -63,31 +152,9 @@ static DWORD WINAPI WIN_RawInputThread(LPVOID param) return 0; } - SDL_zeroa(devices); - - if (data->flags & ENABLE_RAW_MOUSE_INPUT) { - devices[count].usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP; - devices[count].usUsage = USB_USAGE_GENERIC_MOUSE; - devices[count].dwFlags = 0; - devices[count].hwndTarget = window; - ++count; - } - - if (data->flags & ENABLE_RAW_KEYBOARD_INPUT) { - devices[count].usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP; - devices[count].usUsage = USB_USAGE_GENERIC_KEYBOARD; - devices[count].dwFlags = 0; - if (data->flags & RAW_KEYBOARD_FLAG_NOHOTKEYS) { - devices[count].dwFlags |= RIDEV_NOHOTKEYS; - } - if (data->flags & RAW_KEYBOARD_FLAG_INPUTSINK) { - devices[count].dwFlags |= RIDEV_INPUTSINK; - } - devices[count].hwndTarget = window; - ++count; - } - - if (!RegisterRawInputDevices(devices, count, sizeof(devices[0]))) { + // This doesn't *really* need to be atomic, because the parent is waiting for us + last_flags = SDL_GetAtomicU32(&thread_data.flags); + if (!UpdateRawInputDeviceFlags(window, 0, last_flags)) { DestroyWindow(window); return 0; } @@ -96,24 +163,38 @@ static DWORD WINAPI WIN_RawInputThread(LPVOID param) SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); // Tell the parent we're ready to go! - SetEvent(data->ready_event); + SetEvent(thread_data.ready_event); + + RawInputIterateResult iter_result; Uint64 idle_begin = SDL_GetTicksNS(); - while (!data->done && - // The high-order word of GetQueueStatus() will let us know if there's currently raw input to be processed. - // If there isn't, then we'll wait for new data to arrive with MsgWaitForMultipleObjects(). - ((HIWORD(GetQueueStatus(QS_RAWINPUT)) & QS_RAWINPUT) || - (MsgWaitForMultipleObjects(1, &data->signal_event, FALSE, INFINITE, QS_RAWINPUT) == WAIT_OBJECT_0 + 1))) { + while ((iter_result = IterateRawInputThread()) != RINP_QUIT) { + switch (iter_result) { + case RINP_QUIT: // Unreachable + break; + case RINP_UPDATE: + { + const Uint32 new_flags = SDL_GetAtomicU32(&thread_data.flags); + if (!UpdateRawInputDeviceFlags(window, last_flags, new_flags)) { + // Revert the shared flags so the main thread can detect the failure + SDL_SetAtomicU32(&thread_data.flags, last_flags); + } else { + last_flags = new_flags; + } + } break; + case RINP_CONTINUE: + { + Uint64 idle_end = SDL_GetTicksNS(); + Uint64 idle_time = idle_end - idle_begin; + Uint64 usb_8khz_interval = SDL_US_TO_NS(125); + Uint64 poll_start = idle_time < usb_8khz_interval ? _this->internal->last_rawinput_poll : idle_end; - Uint64 idle_end = SDL_GetTicksNS(); - Uint64 idle_time = idle_end - idle_begin; - Uint64 usb_8khz_interval = SDL_US_TO_NS(125); - Uint64 poll_start = idle_time < usb_8khz_interval ? _this->internal->last_rawinput_poll : idle_end; + WIN_PollRawInput(_this, poll_start); - WIN_PollRawInput(_this, poll_start); - - // Reset idle_begin for the next go-around - idle_begin = SDL_GetTicksNS(); + // Reset idle_begin for the next go-around + idle_begin = SDL_GetTicksNS(); + } break; + } } if (_this->internal->raw_input_fake_pen_id) { @@ -121,48 +202,84 @@ static DWORD WINAPI WIN_RawInputThread(LPVOID param) _this->internal->raw_input_fake_pen_id = 0; } - devices[0].dwFlags |= RIDEV_REMOVE; - devices[0].hwndTarget = NULL; - devices[1].dwFlags |= RIDEV_REMOVE; - devices[1].hwndTarget = NULL; - RegisterRawInputDevices(devices, count, sizeof(devices[0])); + // Reset this here, since if we're exiting due to failure, WIN_UpdateRawInputEnabled would see a stale value. + SDL_SetAtomicU32(&thread_data.flags, 0); + + UpdateRawInputDeviceFlags(NULL, last_flags, 0); DestroyWindow(window); return 0; } -static void CleanupRawInputThreadData(RawInputThreadData *data) +static void CleanupRawInputThreadData(void) { - if (data->thread) { - data->done = true; - SetEvent(data->signal_event); - WaitForSingleObject(data->thread, 3000); - CloseHandle(data->thread); - data->thread = NULL; + if (thread_data.thread) { + thread_data.done = true; + SetEvent(thread_data.signal_event); + WaitForSingleObject(thread_data.thread, 3000); + CloseHandle(thread_data.thread); } - if (data->ready_event) { - CloseHandle(data->ready_event); - data->ready_event = NULL; + if (thread_data.ready_event) { + CloseHandle(thread_data.ready_event); } - if (data->signal_event) { - CloseHandle(data->signal_event); - data->signal_event = NULL; + if (thread_data.signal_event) { + CloseHandle(thread_data.signal_event); } + + thread_data = (RawInputThreadData){ 0 }; } -bool WIN_SetRawInputEnabled(SDL_VideoDevice *_this, Uint32 flags) +// Computes the desired raw input flags from SDL_VideoData and ensures the +// raw input thread's device registrations match. +// Creates the thread on first use, only WIN_QuitRawInput actually shuts it down. +static bool WIN_UpdateRawInputEnabled(SDL_VideoDevice *_this) { bool result = false; + SDL_VideoData *data = _this->internal; + Uint32 desired_flags = 0; - CleanupRawInputThreadData(&thread_data); + if (data->raw_mouse_enabled) { + desired_flags |= ENABLE_RAW_MOUSE_INPUT; + } + if (data->raw_keyboard_enabled) { + desired_flags |= ENABLE_RAW_KEYBOARD_INPUT; + } + if (data->raw_keyboard_flag_nohotkeys) { + desired_flags |= RAW_KEYBOARD_FLAG_NOHOTKEYS; + } + if (data->raw_keyboard_flag_inputsink) { + desired_flags |= RAW_KEYBOARD_FLAG_INPUTSINK; + } - if (flags) { - HANDLE handles[2]; + if (desired_flags == SDL_GetAtomicU32(&thread_data.flags)) { + result = true; + goto done; + } - thread_data.flags = flags; + // If the thread exited unexpectedly (e.g. MsgWaitForMultipleObjects failed), + // the handle is stale. Clean it up so the creation path below can recover. + if (thread_data.thread && WaitForSingleObject(thread_data.thread, 0) != WAIT_TIMEOUT) { + CleanupRawInputThreadData(); + } + + // The thread will read from this to update its flags + SDL_SetAtomicU32(&thread_data.flags, desired_flags); + + if (thread_data.thread) { + // Thread is already running. Fire (the event) and forget, it'll read the atomic flags on wakeup. + + // If RegisterRawInputDevices fails, the thread reverts the atomic and the next call + // to this function will see the mismatch and retry. + SDL_assert(thread_data.signal_event); + SetEvent(thread_data.signal_event); + result = true; + } else if (desired_flags) { + HANDLE wait_handles[2]; + + // Thread isn't running, spin it up thread_data.ready_event = CreateEvent(NULL, FALSE, FALSE, NULL); if (!thread_data.ready_event) { WIN_SetError("CreateEvent"); @@ -176,63 +293,31 @@ bool WIN_SetRawInputEnabled(SDL_VideoDevice *_this, Uint32 flags) goto done; } - thread_data.thread = CreateThread(NULL, 0, WIN_RawInputThread, &thread_data, 0, NULL); + thread_data.thread = CreateThread(NULL, 0, WIN_RawInputThread, NULL, 0, NULL); if (!thread_data.thread) { WIN_SetError("CreateThread"); goto done; } - // Wait for the thread to signal ready or exit - handles[0] = thread_data.ready_event; - handles[1] = thread_data.thread; - if (WaitForMultipleObjects(2, handles, FALSE, INFINITE) != WAIT_OBJECT_0) { + // Wait for the thread to complete initial setup or exit + wait_handles[0] = thread_data.ready_event; + wait_handles[1] = thread_data.thread; + if (WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE) != WAIT_OBJECT_0) { SDL_SetError("Couldn't set up raw input handling"); goto done; } result = true; } else { + // Thread isn't running and we tried to disable raw input, nothing to do result = true; } - done: if (!result) { - CleanupRawInputThreadData(&thread_data); + CleanupRawInputThreadData(); } return result; } -static bool WIN_UpdateRawInputEnabled(SDL_VideoDevice *_this) -{ - SDL_VideoData *data = _this->internal; - Uint32 flags = 0; - if (data->raw_mouse_enabled) { - flags |= ENABLE_RAW_MOUSE_INPUT; - } - if (data->raw_keyboard_enabled) { - flags |= ENABLE_RAW_KEYBOARD_INPUT; - } - if (data->raw_keyboard_flag_nohotkeys) { - flags |= RAW_KEYBOARD_FLAG_NOHOTKEYS; - } - if (data->raw_keyboard_flag_inputsink) { - flags |= RAW_KEYBOARD_FLAG_INPUTSINK; - } - - // Leave the thread running, as it takes several ms to shut it down and spin it up. - // We'll continue processing them so they don't back up in the thread event queue, - // but we won't deliver raw events in the application. - flags |= (data->raw_input_enabled & (ENABLE_RAW_MOUSE_INPUT | ENABLE_RAW_KEYBOARD_INPUT)); - - if (flags != data->raw_input_enabled) { - if (WIN_SetRawInputEnabled(_this, flags)) { - data->raw_input_enabled = flags; - } else { - return false; - } - } - return true; -} - bool WIN_SetRawMouseEnabled(SDL_VideoDevice *_this, bool enabled) { SDL_VideoData *data = _this->internal; @@ -306,13 +391,13 @@ bool WIN_SetRawKeyboardFlag_Inputsink(SDL_VideoDevice *_this, bool enabled) return WIN_SetRawKeyboardFlag(_this, INPUTSINK, enabled); } -#else - -bool WIN_SetRawInputEnabled(SDL_VideoDevice *_this, Uint32 flags) +void WIN_QuitRawInput(SDL_VideoDevice *_this) { - return SDL_Unsupported(); + CleanupRawInputThreadData(); } +#else + bool WIN_SetRawMouseEnabled(SDL_VideoDevice *_this, bool enabled) { return SDL_Unsupported(); @@ -333,6 +418,10 @@ bool WIN_SetRawKeyboardFlag_Inputsink(SDL_VideoDevice *_this, bool enabled) return SDL_Unsupported(); } +void WIN_QuitRawInput(SDL_VideoDevice *_this) +{ +} + #endif // !SDL_PLATFORM_XBOXONE && !SDL_PLATFORM_XBOXSERIES #endif // SDL_VIDEO_DRIVER_WINDOWS diff --git a/src/video/windows/SDL_windowsrawinput.h b/src/video/windows/SDL_windowsrawinput.h index 1adeb45f05..d5e6caa05a 100644 --- a/src/video/windows/SDL_windowsrawinput.h +++ b/src/video/windows/SDL_windowsrawinput.h @@ -23,7 +23,7 @@ #ifndef SDL_windowsrawinput_h_ #define SDL_windowsrawinput_h_ -extern bool WIN_SetRawInputEnabled(SDL_VideoDevice *_this, Uint32 flags); +extern void WIN_QuitRawInput(SDL_VideoDevice *_this); extern bool WIN_SetRawMouseEnabled(SDL_VideoDevice *_this, bool enabled); extern bool WIN_SetRawKeyboardEnabled(SDL_VideoDevice *_this, bool enabled); extern bool WIN_SetRawKeyboardFlag_NoHotkeys(SDL_VideoDevice *_this, bool enabled); diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c index 019a23c2d6..3a1527a443 100644 --- a/src/video/windows/SDL_windowsvideo.c +++ b/src/video/windows/SDL_windowsvideo.c @@ -671,9 +671,7 @@ void WIN_VideoQuit(SDL_VideoDevice *_this) SDL_RemoveHintCallback(SDL_HINT_WINDOWS_ENABLE_MENU_MNEMONICS, UpdateWindowsEnableMenuMnemonics, NULL); SDL_RemoveHintCallback(SDL_HINT_WINDOW_FRAME_USABLE_WHILE_CURSOR_HIDDEN, UpdateWindowFrameUsableWhileCursorHidden, NULL); - WIN_SetRawMouseEnabled(_this, false); - WIN_SetRawKeyboardEnabled(_this, false); - WIN_SetRawInputEnabled(_this, 0); + WIN_QuitRawInput(_this); WIN_QuitGameInput(_this); #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) diff --git a/src/video/windows/SDL_windowsvideo.h b/src/video/windows/SDL_windowsvideo.h index fb34acfb23..b3240d3cb9 100644 --- a/src/video/windows/SDL_windowsvideo.h +++ b/src/video/windows/SDL_windowsvideo.h @@ -596,7 +596,6 @@ struct SDL_VideoData bool raw_keyboard_flag_nohotkeys; bool raw_keyboard_flag_inputsink; bool pending_E1_key_sequence; - Uint32 raw_input_enabled; SDL_PenID raw_input_fake_pen_id; WIN_GameInputData *gameinput_context;