Browse Source

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.
William Horvath 2 days ago
parent
commit
983e984afc

+ 195 - 106
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,46 +48,113 @@ typedef struct
 
 static RawInputThreadData thread_data = { 0 };
 
-static DWORD WINAPI WIN_RawInputThread(LPVOID param)
+typedef enum
 {
-    SDL_VideoDevice *_this = SDL_GetVideoDevice();
-    RawInputThreadData *data = (RawInputThreadData *)param;
-    RAWINPUTDEVICE devices[2];
-    HWND window;
-    UINT count = 0;
+    RINP_QUIT,
+    RINP_UPDATE,
+    RINP_CONTINUE
+} RawInputIterateResult;
 
-    SDL_SYS_SetupThread("SDLRawInput");
+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;
+    }
 
-    window = CreateWindowEx(0, TEXT("Message"), NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
-    if (!window) {
-        return 0;
+    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;
     }
+}
 
-    SDL_zeroa(devices);
+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;
+    }
 
-    if (data->flags & ENABLE_RAW_MOUSE_INPUT) {
+    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;
-        devices[count].dwFlags = 0;
-        devices[count].hwndTarget = window;
+
+        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;
     }
 
-    if (data->flags & ENABLE_RAW_KEYBOARD_INPUT) {
+    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;
-        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;
+
+        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;
         }
-        devices[count].hwndTarget = window;
+
         ++count;
     }
 
-    if (!RegisterRawInputDevices(devices, count, sizeof(devices[0]))) {
+    if (RegisterRawInputDevices(devices, count, sizeof(devices[0]))) {
+        return true;
+    }
+    return false;
+}
+
+static DWORD WINAPI WIN_RawInputThread(LPVOID param)
+{
+    SDL_VideoDevice *_this = SDL_GetVideoDevice();
+    Uint32 last_flags = 0;
+    HWND window;
+
+    SDL_SYS_SetupThread("SDLRawInput");
+
+    window = CreateWindowEx(0, TEXT("Message"), NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
+    if (!window) {
+        return 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);
 
-    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))) {
-
-        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);
+    RawInputIterateResult iter_result;
 
-        // Reset idle_begin for the next go-around
-        idle_begin = SDL_GetTicksNS();
+    Uint64 idle_begin = SDL_GetTicksNS();
+    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;
+
+            WIN_PollRawInput(_this, poll_start);
+
+            // 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;
+    }
+
+    // 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_data.flags = flags;
+        // 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

+ 1 - 1
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);

+ 1 - 3
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)

+ 0 - 1
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;