SDL: Fixed the WGI driver picking up Xbox controllers handled by RAWINPUT

From 376ef4e418d9489f5ecdd5159020184370b4ba63 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 20 Feb 2024 06:19:20 -0800
Subject: [PATCH] Fixed the WGI driver picking up Xbox controllers handled by
 RAWINPUT

The WGI driver will see them first, but the RAWINPUT driver has higher priority, so we'll defer to that when it's available.

Fixes https://github.com/libsdl-org/SDL/issues/9091
---
 .../windows/SDL_windows_gaming_input.c        | 110 ++++++++++++++++++
 1 file changed, 110 insertions(+)

diff --git a/src/joystick/windows/SDL_windows_gaming_input.c b/src/joystick/windows/SDL_windows_gaming_input.c
index af788cd920df..cb788986c949 100644
--- a/src/joystick/windows/SDL_windows_gaming_input.c
+++ b/src/joystick/windows/SDL_windows_gaming_input.c
@@ -108,6 +108,111 @@ DEFINE_GUID(IID___x_ABI_CWindows_CGaming_CInput_CIRawGameControllerStatics, 0xeb
 extern SDL_bool SDL_XINPUT_Enabled(void);
 
 
+static SDL_bool SDL_IsXInputDevice(Uint16 vendor, Uint16 product)
+{
+#if defined(SDL_JOYSTICK_XINPUT) || defined(SDL_JOYSTICK_RAWINPUT)
+    PRAWINPUTDEVICELIST raw_devices = NULL;
+    UINT i, raw_device_count = 0;
+    LONG vidpid = MAKELONG(vendor, product);
+
+    /* XInput and RawInput backends will pick up XInput-compatible devices */
+    if (!SDL_XINPUT_Enabled()
+#ifdef SDL_JOYSTICK_RAWINPUT
+        && !RAWINPUT_IsEnabled()
+#endif
+    ) {
+        return SDL_FALSE;
+    }
+
+    /* Go through RAWINPUT (WinXP and later) to find HID devices. */
+    if ((GetRawInputDeviceList(NULL, &raw_device_count, sizeof(RAWINPUTDEVICELIST)) == -1) || (!raw_device_count)) {
+        return SDL_FALSE; /* oh well. */
+    }
+
+    raw_devices = (PRAWINPUTDEVICELIST)SDL_malloc(sizeof(RAWINPUTDEVICELIST) * raw_device_count);
+    if (!raw_devices) {
+        return SDL_FALSE;
+    }
+
+    raw_device_count = GetRawInputDeviceList(raw_devices, &raw_device_count, sizeof(RAWINPUTDEVICELIST));
+    if (raw_device_count == (UINT)-1) {
+        SDL_free(raw_devices);
+        raw_devices = NULL;
+        return SDL_FALSE; /* oh well. */
+    }
+
+    for (i = 0; i < raw_device_count; i++) {
+        RID_DEVICE_INFO rdi;
+        char devName[MAX_PATH] = { 0 };
+        UINT rdiSize = sizeof(rdi);
+        UINT nameSize = SDL_arraysize(devName);
+        DEVINST devNode;
+        char devVidPidString[32];
+        int j;
+
+        rdi.cbSize = sizeof(rdi);
+
+        if ((raw_devices[i].dwType != RIM_TYPEHID) ||
+            (GetRawInputDeviceInfoA(raw_devices[i].hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) == ((UINT)-1)) ||
+            (GetRawInputDeviceInfoA(raw_devices[i].hDevice, RIDI_DEVICENAME, devName, &nameSize) == ((UINT)-1)) ||
+            (SDL_strstr(devName, "IG_") == NULL)) {
+            /* Skip non-XInput devices */
+            continue;
+        }
+
+        /* First check for a simple VID/PID match. This will work for Xbox 360 controllers. */
+        if (MAKELONG(rdi.hid.dwVendorId, rdi.hid.dwProductId) == vidpid) {
+            SDL_free(raw_devices);
+            return SDL_TRUE;
+        }
+
+        /* For Xbox One controllers, Microsoft doesn't propagate the VID/PID down to the HID stack.
+         * We'll have to walk the device tree upwards searching for a match for our VID/PID. */
+
+        /* Make sure the device interface string is something we know how to parse */
+        /* Example: \\?\HID#VID_045E&PID_02FF&IG_00#9&2c203035&2&0000#{4d1e55b2-f16f-11cf-88cb-001111000030} */
+        if ((SDL_strstr(devName, "\\\\?\\") != devName) || (SDL_strstr(devName, "#{") == NULL)) {
+            continue;
+        }
+
+        /* Unescape the backslashes in the string and terminate before the GUID portion */
+        for (j = 0; devName[j] != '\0'; j++) {
+            if (devName[j] == '#') {
+                if (devName[j + 1] == '{') {
+                    devName[j] = '\0';
+                    break;
+                } else {
+                    devName[j] = '\\';
+                }
+            }
+        }
+
+        /* We'll be left with a string like this: \\?\HID\VID_045E&PID_02FF&IG_00\9&2c203035&2&0000
+         * Simply skip the \\?\ prefix and we'll have a properly formed device instance ID */
+        if (CM_Locate_DevNodeA(&devNode, &devName[4], CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) {
+            continue;
+        }
+
+        (void)SDL_snprintf(devVidPidString, sizeof(devVidPidString), "VID_%04X&PID_%04X", vendor, product);
+
+        while (CM_Get_Parent(&devNode, devNode, 0) == CR_SUCCESS) {
+            char deviceId[MAX_DEVICE_ID_LEN];
+
+            if ((CM_Get_Device_IDA(devNode, deviceId, SDL_arraysize(deviceId), 0) == CR_SUCCESS) &&
+                (SDL_strstr(deviceId, devVidPidString) != NULL)) {
+                /* The VID/PID matched a parent device */
+                SDL_free(raw_devices);
+                return SDL_TRUE;
+            }
+        }
+    }
+
+    SDL_free(raw_devices);
+#endif /* SDL_JOYSTICK_XINPUT || SDL_JOYSTICK_RAWINPUT */
+
+    return SDL_FALSE;
+}
+
 static void WGI_LoadRawGameControllerStatics()
 {
     HRESULT hr;
@@ -346,6 +451,11 @@ static HRESULT STDMETHODCALLTYPE IEventHandler_CRawGameControllerVtbl_InvokeAdde
             ignore_joystick = SDL_TRUE;
         }
 
+        if (!ignore_joystick && SDL_IsXInputDevice(vendor, product)) {
+            /* This hasn't been detected by the RAWINPUT driver yet, but it will be picked up later. */
+            ignore_joystick = SDL_TRUE;
+        }
+
         if (!ignore_joystick) {
             if (game_controller) {
                 type = GetGameControllerType(game_controller);