SDL: Fixed dropping raw input because of mixing GetRawInputBuffer() and WM_INPUT handling

From 31851a50d2e30efa1a3c74ae83592d6a29ffc5b7 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 3 Jan 2024 02:20:23 -0800
Subject: [PATCH] Fixed dropping raw input because of mixing
 GetRawInputBuffer() and WM_INPUT handling

It turns out that when you enable raw input and then process Windows messages, you'll get the currently pending input in GetRawInputBuffer(), and you'll get any new input that occurs while processing messages as WM_INPUT.

The fix for this is to create a dedicated thread to handle raw input and only use GetRawInputBuffer() in that thread. A nice side effect of this is that we'll get mouse input at the lowest latency possible, but raw mouse events will now occur on a separate thread, outside of the normal event loop processing.

Improved fix for https://github.com/libsdl-org/SDL/issues/8756
---
 src/video/windows/SDL_windowsevents.c |  50 +++++-----
 src/video/windows/SDL_windowsevents.h |   1 +
 src/video/windows/SDL_windowsmouse.c  | 132 +++++++++++++++++++++++---
 src/video/windows/SDL_windowswindow.h |   1 +
 4 files changed, 141 insertions(+), 43 deletions(-)

diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index c0a1e4d41e85..63bc28a76e4b 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -607,34 +607,14 @@ static void WIN_HandleRawMouseInput(Uint64 timestamp, SDL_WindowData *data, RAWM
     WIN_CheckRawMouseButtons(timestamp, rawmouse->usButtonFlags, data, mouseID);
 }
 
-/* The layout of memory for data returned from GetRawInputBuffer(), documented here:
- * https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getrawinputbuffer
- */
-typedef struct
-{
-    union
-    {
-        RAWINPUTHEADER header;
-        BYTE padding[24];
-    } hdr;
-
-    union
-    {
-        RAWMOUSE mouse;
-        RAWKEYBOARD keyboard;
-        RAWHID hid;
-    } data;
-
-} ALIGNED_RAWINPUT;
-
-static void WIN_PollRawMouseInput()
+void WIN_PollRawMouseInput(void)
 {
     SDL_Mouse *mouse = SDL_GetMouse();
     SDL_Window *window;
     SDL_WindowData *data;
     UINT size, count, i, total = 0;
     RAWINPUT *input;
-    Uint64 now, timestamp, increment;
+    Uint64 now;
 
     /* We only use raw mouse input in relative mode */
     if (!mouse->relative_mode || mouse->relative_mode_warp) {
@@ -649,6 +629,14 @@ static void WIN_PollRawMouseInput()
     data = window->driverdata;
 
     if (data->rawinput_size == 0) {
+        BOOL isWow64;
+
+        data->rawinput_offset = sizeof(RAWINPUTHEADER);
+        if (IsWow64Process(GetCurrentProcess(), &isWow64) && isWow64) {
+            /* We're going to get 64-bit data, so use the 64-bit RAWINPUTHEADER size */
+            data->rawinput_offset += 8;
+        }
+
         if (GetRawInputBuffer(NULL, &data->rawinput_size, sizeof(RAWINPUTHEADER)) == (UINT)-1) {
             return;
         }
@@ -679,13 +667,21 @@ static void WIN_PollRawMouseInput()
 
     now = SDL_GetTicksNS();
     if (total > 0) {
-        /* We'll spread these events over the time since the last poll */
-        timestamp = data->last_rawinput_poll;
-        increment = (now - timestamp) / total;
+        Uint64 timestamp, increment;
+        Uint64 delta = (now - data->last_rawinput_poll);
+        if (total > 1 && delta <= SDL_MS_TO_NS(100)) {
+            /* We'll spread these events over the time since the last poll */
+            timestamp = data->last_rawinput_poll;
+            increment = delta / total;
+        } else {
+            /* Do we want to track the update rate per device? */
+            timestamp = now;
+            increment = 0;
+        }
         for (i = 0, input = data->rawinput; i < total; ++i, input = NEXTRAWINPUTBLOCK(input)) {
             timestamp += increment;
             if (input->header.dwType == RIM_TYPEMOUSE) {
-                RAWMOUSE *rawmouse = &(((ALIGNED_RAWINPUT *)input)->data.mouse);
+                RAWMOUSE *rawmouse = (RAWMOUSE *)((BYTE *)input + data->rawinput_offset);
                 WIN_HandleRawMouseInput(timestamp, window->driverdata, rawmouse);
             }
         }
@@ -1763,8 +1759,6 @@ void WIN_PumpEvents(SDL_VideoDevice *_this)
     SDL_Window *focusWindow;
 #endif
 
-    WIN_PollRawMouseInput();
-
     if (g_WindowsEnableMessageLoop) {
         SDL_processing_messages = SDL_TRUE;
 
diff --git a/src/video/windows/SDL_windowsevents.h b/src/video/windows/SDL_windowsevents.h
index 414019ba9b58..77f1dd464a2c 100644
--- a/src/video/windows/SDL_windowsevents.h
+++ b/src/video/windows/SDL_windowsevents.h
@@ -30,6 +30,7 @@ extern HINSTANCE SDL_Instance;
 extern LRESULT CALLBACK WIN_KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam);
 extern LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam,
                                        LPARAM lParam);
+extern void WIN_PollRawMouseInput(void);
 extern void WIN_PumpEvents(SDL_VideoDevice *_this);
 extern void WIN_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window);
 extern int WIN_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS);
diff --git a/src/video/windows/SDL_windowsmouse.c b/src/video/windows/SDL_windowsmouse.c
index 4ff7950406c1..a078e039d256 100644
--- a/src/video/windows/SDL_windowsmouse.c
+++ b/src/video/windows/SDL_windowsmouse.c
@@ -23,9 +23,11 @@
 #if defined(SDL_VIDEO_DRIVER_WINDOWS) && !defined(__XBOXONE__) && !defined(__XBOXSERIES__)
 
 #include "SDL_windowsvideo.h"
+#include "SDL_windowsevents.h"
 
-#include "../../events/SDL_mouse_c.h"
 #include "../SDL_video_c.h"
+#include "../../events/SDL_mouse_c.h"
+#include "../../joystick/usb_ids.h"
 
 DWORD SDL_last_warp_time = 0;
 HCURSOR SDL_cursor = NULL;
@@ -33,9 +35,84 @@ static SDL_Cursor *SDL_blank_cursor = NULL;
 
 static int rawInputEnableCount = 0;
 
+typedef struct
+{
+    HANDLE ready_event;
+    HANDLE done_event;
+    HANDLE thread;
+} RawMouseThreadData;
+
+static RawMouseThreadData thread_data = {
+    INVALID_HANDLE_VALUE,
+    INVALID_HANDLE_VALUE,
+    INVALID_HANDLE_VALUE
+};
+
+static DWORD WINAPI WIN_RawMouseThread(LPVOID param)
+{
+    RAWINPUTDEVICE rawMouse;
+    HWND window;
+
+    window = CreateWindowEx(0, TEXT("Message"), NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
+    if (!window) {
+        return 0;
+    }
+
+    rawMouse.usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP;
+    rawMouse.usUsage = USB_USAGE_GENERIC_MOUSE;
+    rawMouse.dwFlags = 0;
+    rawMouse.hwndTarget = window;
+
+    if (!RegisterRawInputDevices(&rawMouse, 1, sizeof(rawMouse))) {
+        DestroyWindow(window);
+        return 0;
+    }
+
+    /* Tell the parent we're ready to go! */
+    SetEvent(thread_data.ready_event);
+
+    for ( ; ; ) {
+        if (MsgWaitForMultipleObjects(1, &thread_data.done_event, 0, INFINITE, QS_RAWINPUT) != WAIT_OBJECT_0 + 1) {
+            break;
+        }
+
+        /* Clear the queue status so MsgWaitForMultipleObjects() will wait again */
+        (void)GetQueueStatus(QS_RAWINPUT);
+
+        WIN_PollRawMouseInput();
+    }
+
+    rawMouse.dwFlags |= RIDEV_REMOVE;
+    RegisterRawInputDevices(&rawMouse, 1, sizeof(rawMouse));
+
+    DestroyWindow(window);
+
+    return 0;
+}
+
+static void CleanupRawMouseThreadData(void)
+{
+    if (thread_data.thread != INVALID_HANDLE_VALUE) {
+        SetEvent(thread_data.done_event);
+        WaitForSingleObject(thread_data.thread, 500);
+        CloseHandle(thread_data.thread);
+        thread_data.thread = INVALID_HANDLE_VALUE;
+    }
+
+    if (thread_data.ready_event != INVALID_HANDLE_VALUE) {
+        CloseHandle(thread_data.ready_event);
+        thread_data.ready_event = INVALID_HANDLE_VALUE;
+    }
+
+    if (thread_data.done_event != INVALID_HANDLE_VALUE) {
+        CloseHandle(thread_data.done_event);
+        thread_data.done_event = INVALID_HANDLE_VALUE;
+    }
+}
+
 static int ToggleRawInput(SDL_bool enabled)
 {
-    RAWINPUTDEVICE rawMouse = { 0x01, 0x02, 0, NULL }; /* Mouse: UsagePage = 1, Usage = 2 */
+    int result = -1;
 
     if (enabled) {
         rawInputEnableCount++;
@@ -52,23 +129,48 @@ static int ToggleRawInput(SDL_bool enabled)
         }
     }
 
-    if (!enabled) {
-        rawMouse.dwFlags |= RIDEV_REMOVE;
-    }
+    if (enabled) {
+        HANDLE handles[2];
 
-    /* (Un)register raw input for mice */
-    if (RegisterRawInputDevices(&rawMouse, 1, sizeof(RAWINPUTDEVICE)) == FALSE) {
-        /* Reset the enable count, otherwise subsequent enable calls will
-           believe raw input is enabled */
-        rawInputEnableCount = 0;
+        thread_data.ready_event = CreateEvent(NULL, FALSE, FALSE, NULL);
+        if (thread_data.ready_event == INVALID_HANDLE_VALUE) {
+            WIN_SetError("CreateEvent");
+            goto done;
+        }
+
+        thread_data.done_event = CreateEvent(NULL, FALSE, FALSE, NULL);
+        if (thread_data.done_event == INVALID_HANDLE_VALUE) {
+            WIN_SetError("CreateEvent");
+            goto done;
+        }
 
-        /* Only return an error when registering. If we unregister and fail,
-           then it's probably that we unregistered twice. That's OK. */
-        if (enabled) {
-            return SDL_Unsupported();
+        thread_data.thread = CreateThread(NULL, 0, WIN_RawMouseThread, &thread_data, 0, NULL);
+        if (thread_data.thread == INVALID_HANDLE_VALUE) {
+            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) {
+            SDL_SetError("Couldn't set up raw input handling");
+            goto done;
+        }
+        result = 0;
+    } else {
+        CleanupRawMouseThreadData();
+        result = 0;
     }
-    return 0;
+
+done:
+    if (enabled && result < 0) {
+        CleanupRawMouseThreadData();
+
+        /* Reset rawInputEnableCount so we can try again */
+        rawInputEnableCount = 0;
+    }
+    return result;
 }
 
 static SDL_Cursor *WIN_CreateDefaultCursor()
diff --git a/src/video/windows/SDL_windowswindow.h b/src/video/windows/SDL_windowswindow.h
index c5ff4ce8c7b9..835ba622db60 100644
--- a/src/video/windows/SDL_windowswindow.h
+++ b/src/video/windows/SDL_windowswindow.h
@@ -67,6 +67,7 @@ struct SDL_WindowData
     SDL_bool in_window_deactivation;
     RECT cursor_clipped_rect;
     RAWINPUT *rawinput;
+    UINT rawinput_offset;
     UINT rawinput_size;
     UINT rawinput_count;
     Uint64 last_rawinput_poll;