SDL: XInput: Use XInputGetCapabilitiesEx instead of fragile GuessXInputDevice

From 08a7ca4d53ef0d504ec780936fd65616fe3a9f09 Mon Sep 17 00:00:00 2001
From: Dimitriy Ryazantcev <[EMAIL REDACTED]>
Date: Fri, 15 Dec 2023 17:09:15 +0200
Subject: [PATCH] XInput: Use XInputGetCapabilitiesEx instead of fragile
 GuessXInputDevice

XInputGetCapabilitiesEx (ordinal 108) is available in XInput 1.4 that is shipped with Windows 8 and newer.
---
 src/core/windows/SDL_xinput.c             |   3 +
 src/core/windows/SDL_xinput.h             |  24 ++++
 src/joystick/controller_list.h            |   4 +-
 src/joystick/windows/SDL_xinputjoystick.c | 147 ++--------------------
 4 files changed, 42 insertions(+), 136 deletions(-)

diff --git a/src/core/windows/SDL_xinput.c b/src/core/windows/SDL_xinput.c
index 8a410e783a01..fe34b75fe148 100644
--- a/src/core/windows/SDL_xinput.c
+++ b/src/core/windows/SDL_xinput.c
@@ -30,6 +30,7 @@ extern "C" {
 XInputGetState_t SDL_XInputGetState = NULL;
 XInputSetState_t SDL_XInputSetState = NULL;
 XInputGetCapabilities_t SDL_XInputGetCapabilities = NULL;
+XInputGetCapabilitiesEx_t SDL_XInputGetCapabilitiesEx = NULL;
 XInputGetBatteryInformation_t SDL_XInputGetBatteryInformation = NULL;
 DWORD SDL_XInputVersion = 0;
 
@@ -111,6 +112,8 @@ int WIN_LoadXInputDLL(void)
     }
     SDL_XInputSetState = (XInputSetState_t)GetProcAddress(s_pXInputDLL, "XInputSetState");
     SDL_XInputGetCapabilities = (XInputGetCapabilities_t)GetProcAddress(s_pXInputDLL, "XInputGetCapabilities");
+    /* 108 is the ordinal for _XInputGetCapabilitiesEx, which additionally returns VID/PID of the controller. */
+    SDL_XInputGetCapabilitiesEx = (XInputGetCapabilitiesEx_t)GetProcAddress(s_pXInputDLL, (LPCSTR)108);
     SDL_XInputGetBatteryInformation = (XInputGetBatteryInformation_t)GetProcAddress(s_pXInputDLL, "XInputGetBatteryInformation");
     if (!SDL_XInputGetState || !SDL_XInputSetState || !SDL_XInputGetCapabilities) {
         WIN_UnloadXInputDLL();
diff --git a/src/core/windows/SDL_xinput.h b/src/core/windows/SDL_xinput.h
index a92cb8b8cb8f..1d01da3674d5 100644
--- a/src/core/windows/SDL_xinput.h
+++ b/src/core/windows/SDL_xinput.h
@@ -44,6 +44,9 @@ using namespace XInputOnGameInput;
 #ifndef XINPUT_CAPS_FFB_SUPPORTED
 #define XINPUT_CAPS_FFB_SUPPORTED 0x0001
 #endif
+#ifndef XINPUT_CAPS_WIRELESS
+#define XINPUT_CAPS_WIRELESS 0x0002
+#endif
 
 #ifndef XINPUT_DEVSUBTYPE_UNKNOWN
 #define XINPUT_DEVSUBTYPE_UNKNOWN 0x00
@@ -207,6 +210,17 @@ typedef struct
 
 #endif /* HAVE_XINPUT_H */
 
+/* This struct is not defined in XInput headers. */
+typedef struct _XINPUT_CAPABILITIES_EX
+{
+    XINPUT_CAPABILITIES Capabilities;
+    WORD VendorId;
+    WORD ProductId;
+    WORD ProductVersion;
+    WORD unk1;
+    DWORD unk2;
+} XINPUT_CAPABILITIES_EX, *PXINPUT_CAPABILITIES_EX;
+
 /* Forward decl's for XInput API's we load dynamically and use if available */
 typedef DWORD(WINAPI *XInputGetState_t)(
     DWORD dwUserIndex,      /* [in] Index of the gamer associated with the device */
@@ -224,6 +238,14 @@ typedef DWORD(WINAPI *XInputGetCapabilities_t)(
     XINPUT_CAPABILITIES *pCapabilities /* [out] Receives the capabilities */
 );
 
+/* Only available in XInput 1.4 that is shipped with Windows 8 and newer. */
+typedef DWORD(WINAPI *XInputGetCapabilitiesEx_t)(
+    DWORD dwReserved,                       /* [in] Must be 1 */
+    DWORD dwUserIndex,                      /* [in] Index of the gamer associated with the device */
+    DWORD dwFlags,                          /* [in] Input flags that identify the device type */
+    XINPUT_CAPABILITIES_EX *pCapabilitiesEx /* [out] Receives the capabilities */
+);
+
 typedef DWORD(WINAPI *XInputGetBatteryInformation_t)(
     DWORD dwUserIndex,
     BYTE devType,
@@ -235,6 +257,7 @@ extern void WIN_UnloadXInputDLL(void);
 extern XInputGetState_t SDL_XInputGetState;
 extern XInputSetState_t SDL_XInputSetState;
 extern XInputGetCapabilities_t SDL_XInputGetCapabilities;
+extern XInputGetCapabilitiesEx_t SDL_XInputGetCapabilitiesEx;
 extern XInputGetBatteryInformation_t SDL_XInputGetBatteryInformation;
 extern DWORD SDL_XInputVersion; /* ((major << 16) & 0xFF00) | (minor & 0xFF) */
 
@@ -246,6 +269,7 @@ extern DWORD SDL_XInputVersion; /* ((major << 16) & 0xFF00) | (minor & 0xFF) */
 #define XINPUTGETSTATE              SDL_XInputGetState
 #define XINPUTSETSTATE              SDL_XInputSetState
 #define XINPUTGETCAPABILITIES       SDL_XInputGetCapabilities
+#define XINPUTGETCAPABILITIESEX     SDL_XInputGetCapabilitiesEx
 #define XINPUTGETBATTERYINFORMATION SDL_XInputGetBatteryInformation
 
 #endif /* SDL_xinput_h_ */
diff --git a/src/joystick/controller_list.h b/src/joystick/controller_list.h
index 38f638b2a3ce..f474be7ce0d9 100644
--- a/src/joystick/controller_list.h
+++ b/src/joystick/controller_list.h
@@ -169,7 +169,7 @@ static const ControllerDescription_t arrControllers[] = {
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x028f ), k_eControllerType_XBox360Controller, "Xbox 360 Controller" },	// Microsoft X-Box 360 pad v2
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x0291 ), k_eControllerType_XBox360Controller, "Xbox 360 Wireless Controller" },	// Xbox 360 Wireless Receiver (XBOX)
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x02a0 ), k_eControllerType_XBox360Controller, NULL },	// Microsoft X-Box 360 Big Button IR
-	{ MAKE_CONTROLLER_ID( 0x045e, 0x02a1 ), k_eControllerType_XBox360Controller, NULL },	// Microsoft X-Box 360 Wireless Controller with XUSB driver on Windows
+	{ MAKE_CONTROLLER_ID( 0x045e, 0x02a1 ), k_eControllerType_XBox360Controller, "Xbox 360 Wireless Controller" },	// Xbox 360 Wireless Controller with XUSB driver on Windows
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x02a9 ), k_eControllerType_XBox360Controller, "Xbox 360 Wireless Controller" },	// Xbox 360 Wireless Receiver (third party knockoff)
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x0719 ), k_eControllerType_XBox360Controller, "Xbox 360 Wireless Controller" },	// Xbox 360 Wireless Receiver
 	{ MAKE_CONTROLLER_ID( 0x046d, 0xc21d ), k_eControllerType_XBox360Controller, NULL },	// Logitech Gamepad F310
@@ -308,7 +308,7 @@ static const ControllerDescription_t arrControllers[] = {
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x02e3 ), k_eControllerType_XBoxOneController, "Xbox One Elite Controller" },	// Microsoft X-Box One Elite pad
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x02ea ), k_eControllerType_XBoxOneController, "Xbox One S Controller" },	// Microsoft X-Box One S pad
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x02fd ), k_eControllerType_XBoxOneController, "Xbox One S Controller" },	// Microsoft X-Box One S pad (Bluetooth)
-	{ MAKE_CONTROLLER_ID( 0x045e, 0x02ff ), k_eControllerType_XBoxOneController, NULL },	// Microsoft X-Box One controller with XBOXGIP driver on Windows
+	{ MAKE_CONTROLLER_ID( 0x045e, 0x02ff ), k_eControllerType_XBoxOneController, "Xbox One Controller" },	// Microsoft X-Box One controller with XBOXGIP driver on Windows
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x0b00 ), k_eControllerType_XBoxOneController, "Xbox One Elite 2 Controller" },	// Microsoft X-Box One Elite Series 2 pad
 //	{ MAKE_CONTROLLER_ID( 0x045e, 0x0b02 ), k_eControllerType_XBoxOneController, "Xbox One Elite 2 Controller" },	// The virtual keyboard generated by XboxGip drivers for Xbox One Controllers (see https://github.com/libsdl-org/SDL/pull/5121 for details)
 	{ MAKE_CONTROLLER_ID( 0x045e, 0x0b05 ), k_eControllerType_XBoxOneController, "Xbox One Elite 2 Controller" },	// Microsoft X-Box One Elite Series 2 pad (Bluetooth)
diff --git a/src/joystick/windows/SDL_xinputjoystick.c b/src/joystick/windows/SDL_xinputjoystick.c
index f1990da9377a..0a1baccf874f 100644
--- a/src/joystick/windows/SDL_xinputjoystick.c
+++ b/src/joystick/windows/SDL_xinputjoystick.c
@@ -38,7 +38,6 @@ extern "C" {
  * Internal stuff.
  */
 static SDL_bool s_bXInputEnabled = SDL_TRUE;
-static char *s_arrXInputDevicePath[XUSER_MAX_COUNT];
 
 static SDL_bool SDL_XInputUseOldJoystickMapping()
 {
@@ -114,116 +113,25 @@ static const char *GetXInputName(const Uint8 userid, BYTE SubType)
     return name;
 }
 
-/* We can't really tell what device is being used for XInput, but we can guess
-   and we'll be correct for the case where only one device is connected.
- */
-static void GuessXInputDevice(Uint8 userid, Uint16 *pVID, Uint16 *pPID, Uint16 *pVersion)
+static SDL_bool GetXInputDeviceInfo(Uint8 userid, Uint16 *pVID, Uint16 *pPID, Uint16 *pVersion)
 {
-#if !defined(__WINRT__) && !defined(__XBOXONE__) && !defined(__XBOXSERIES__) /* TODO: remove this ifndef __WINRT__ block, but only after integrating with UWP/WinRT's HID API */
-    PRAWINPUTDEVICELIST devices = NULL;
-    UINT i, j, device_count = 0;
-
-    if ((GetRawInputDeviceList(NULL, &device_count, sizeof(RAWINPUTDEVICELIST)) == -1) || (!device_count)) {
-        return; /* oh well. */
-    }
+    XINPUT_CAPABILITIES_EX capabilities;
 
-    devices = (PRAWINPUTDEVICELIST)SDL_malloc(sizeof(RAWINPUTDEVICELIST) * device_count);
-    if (!devices) {
-        return;
+    if (!XINPUTGETCAPABILITIESEX || XINPUTGETCAPABILITIESEX(1, userid, 0, &capabilities) != ERROR_SUCCESS) {
+        return SDL_FALSE;
     }
 
-    device_count = GetRawInputDeviceList(devices, &device_count, sizeof(RAWINPUTDEVICELIST));
-    if (device_count == (UINT)-1) {
-        SDL_free(devices);
-        return; /* oh well. */
-    }
-
-    /* First see if we have a cached entry for this index */
-    if (s_arrXInputDevicePath[userid]) {
-        for (i = 0; i < device_count; i++) {
-            RID_DEVICE_INFO rdi;
-            char devName[128] = { 0 };
-            UINT rdiSize = sizeof(rdi);
-            UINT nameSize = SDL_arraysize(devName);
-
-            rdi.cbSize = sizeof(rdi);
-            if (devices[i].dwType == RIM_TYPEHID &&
-                GetRawInputDeviceInfoA(devices[i].hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) != (UINT)-1 &&
-                GetRawInputDeviceInfoA(devices[i].hDevice, RIDI_DEVICENAME, devName, &nameSize) != (UINT)-1) {
-                if (SDL_strcmp(devName, s_arrXInputDevicePath[userid]) == 0) {
-                    *pVID = (Uint16)rdi.hid.dwVendorId;
-                    *pPID = (Uint16)rdi.hid.dwProductId;
-                    *pVersion = (Uint16)rdi.hid.dwVersionNumber;
-                    SDL_free(devices);
-                    return;
-                }
-            }
-        }
+    /* Fixup for Wireless Xbox 360 Controller */
+    if (capabilities.ProductId == 0 && capabilities.Capabilities.Flags & XINPUT_CAPS_WIRELESS) {
+        capabilities.VendorId = USB_VENDOR_MICROSOFT;
+        capabilities.ProductId = USB_PRODUCT_XBOX360_XUSB_CONTROLLER;
     }
 
-    for (i = 0; i < device_count; i++) {
-        RID_DEVICE_INFO rdi;
-        char devName[MAX_PATH] = { 0 };
-        UINT rdiSize = sizeof(rdi);
-        UINT nameSize = SDL_arraysize(devName);
-
-        rdi.cbSize = sizeof(rdi);
-        if (devices[i].dwType == RIM_TYPEHID &&
-            GetRawInputDeviceInfoA(devices[i].hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) != (UINT)-1 &&
-            GetRawInputDeviceInfoA(devices[i].hDevice, RIDI_DEVICENAME, devName, &nameSize) != (UINT)-1) {
-#ifdef DEBUG_JOYSTICK
-            SDL_Log("Raw input device: VID = 0x%x, PID = 0x%x, %s\n", rdi.hid.dwVendorId, rdi.hid.dwProductId, devName);
-#endif
-            if (SDL_strstr(devName, "IG_") != NULL) {
-                SDL_bool found = SDL_FALSE;
-                for (j = 0; j < SDL_arraysize(s_arrXInputDevicePath); ++j) {
-                    if (!s_arrXInputDevicePath[j]) {
-                        continue;
-                    }
-                    if (SDL_strcmp(devName, s_arrXInputDevicePath[j]) == 0) {
-                        found = SDL_TRUE;
-                        break;
-                    }
-                }
-                if (found) {
-                    /* We already have this device in our XInput device list */
-                    continue;
-                }
-
-                /* We don't actually know if this is the right device for this
-                 * userid, but we'll record it so we'll at least be consistent
-                 * when the raw device list changes.
-                 */
-                if (rdi.hid.dwVendorId == USB_VENDOR_VALVE &&
-                    rdi.hid.dwProductId == USB_PRODUCT_STEAM_VIRTUAL_GAMEPAD) {
-                    /* Steam encodes the real device in the path */
-                    int realVID = rdi.hid.dwVendorId;
-                    int realPID = rdi.hid.dwProductId;
-                    (void)SDL_sscanf(devName, "\\\\.\\pipe\\HID#VID_045E&PID_028E&IG_00#%x&%x&", &realVID, &realPID);
-                    *pVID = (Uint16)realVID;
-                    *pPID = (Uint16)realPID;
-                    *pVersion = 0;
-                } else {
-                    *pVID = (Uint16)rdi.hid.dwVendorId;
-                    *pPID = (Uint16)rdi.hid.dwProductId;
-                    *pVersion = (Uint16)rdi.hid.dwVersionNumber;
-                }
-                if (s_arrXInputDevicePath[userid]) {
-                    SDL_free(s_arrXInputDevicePath[userid]);
-                }
-                s_arrXInputDevicePath[userid] = SDL_strdup(devName);
-                SDL_free(devices);
-                return;
-            }
-        }
-    }
-    SDL_free(devices);
-#endif /* !__WINRT__ */
+    *pVID = capabilities.VendorId;
+    *pPID = capabilities.ProductId;
+    *pVersion = capabilities.ProductVersion;
 
-    /* The device wasn't in the raw HID device list, it's probably Bluetooth */
-    *pVID = 0x045e; /* Microsoft */
-    *pPID = 0x02fd; /* XBox One S Bluetooth */
-    *pVersion = 0;
+    return SDL_TRUE;
 }
 
 static void AddXInputDevice(Uint8 userid, BYTE SubType, JoyStick_DeviceData **pContext)
@@ -279,6 +187,7 @@ static void AddXInputDevice(Uint8 userid, BYTE SubType, JoyStick_DeviceData **pC
         return; /* better luck next time? */
     }
 
+    GetXInputDeviceInfo(userid, &vendor, &product, &version);
     pNewJoystick->bXInputDevice = SDL_TRUE;
     pNewJoystick->joystickname = SDL_CreateJoystickName(vendor, product, NULL, GetXInputName(userid, SubType));
     if (!pNewJoystick->joystickname) {
@@ -287,8 +196,6 @@ static void AddXInputDevice(Uint8 userid, BYTE SubType, JoyStick_DeviceData **pC
     }
     (void)SDL_snprintf(pNewJoystick->path, sizeof(pNewJoystick->path), "XInput#%u", userid);
     if (!SDL_XInputUseOldJoystickMapping()) {
-        GuessXInputDevice(userid, &vendor, &product, &version);
-
         pNewJoystick->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_USB, vendor, product, version, pNewJoystick->joystickname, 'x', SubType);
     }
     pNewJoystick->SubType = SubType;
@@ -319,14 +226,6 @@ static void AddXInputDevice(Uint8 userid, BYTE SubType, JoyStick_DeviceData **pC
     WINDOWS_AddJoystickDevice(pNewJoystick);
 }
 
-static void DelXInputDevice(Uint8 userid)
-{
-    if (s_arrXInputDevicePath[userid]) {
-        SDL_free(s_arrXInputDevicePath[userid]);
-        s_arrXInputDevicePath[userid] = NULL;
-    }
-}
-
 void SDL_XINPUT_JoystickDetect(JoyStick_DeviceData **pContext)
 {
     int iuserid;
@@ -340,21 +239,7 @@ void SDL_XINPUT_JoystickDetect(JoyStick_DeviceData **pContext)
         const Uint8 userid = (Uint8)iuserid;
         XINPUT_CAPABILITIES capabilities;
         if (XINPUTGETCAPABILITIES(userid, XINPUT_FLAG_GAMEPAD, &capabilities) == ERROR_SUCCESS) {
-            /* Adding a new device, must handle all removes first, or GuessXInputDevice goes terribly wrong (returns
-              a product/vendor ID that is not even attached to the system) when we get a remove and add on the same tick
-              (e.g. when disconnecting a device and the OS reassigns which userid an already-attached controller is)
-            */
-            int iuserid2;
-            for (iuserid2 = iuserid - 1; iuserid2 >= 0; iuserid2--) {
-                const Uint8 userid2 = (Uint8)iuserid2;
-                XINPUT_CAPABILITIES capabilities2;
-                if (XINPUTGETCAPABILITIES(userid2, XINPUT_FLAG_GAMEPAD, &capabilities2) != ERROR_SUCCESS) {
-                    DelXInputDevice(userid2);
-                }
-            }
             AddXInputDevice(userid, capabilities.SubType, pContext);
-        } else {
-            DelXInputDevice(userid);
         }
     }
 }
@@ -552,12 +437,6 @@ void SDL_XINPUT_JoystickClose(SDL_Joystick *joystick)
 
 void SDL_XINPUT_JoystickQuit(void)
 {
-    int iuserid;
-
-    for (iuserid = 0; iuserid < XUSER_MAX_COUNT; ++iuserid) {
-        DelXInputDevice(iuserid);
-    }
-
     if (s_bXInputEnabled) {
         WIN_UnloadXInputDLL();
     }