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.
This commit is contained in:
William Horvath
2026-03-20 06:23:45 -07:00
committed by Sam Lantinga
parent 706e7f9043
commit 983e984afc
4 changed files with 200 additions and 114 deletions

View File

@@ -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

View File

@@ -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);

View File

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

View File

@@ -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;