SDL: Fixed controllers showing up under both MFI and HIDAPI drivers

From 2a53f8315acd83f72be7c5e0e96c0d9480eac477 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 26 Jul 2021 23:29:20 -0700
Subject: [PATCH] Fixed controllers showing up under both MFI and HIDAPI
 drivers

The Game Controller Kit doesn't show the controllers at startup, so the HIDAPI driver sees them first and therefore gets preference when a controller is supported by both drivers.

This fixes bug https://github.com/libsdl-org/SDL/issues/4209
---
 src/joystick/hidapi/SDL_hidapijoystick.c   | 35 ++++++++++++
 src/joystick/hidapi/SDL_hidapijoystick_c.h |  3 +
 src/joystick/iphoneos/SDL_mfijoystick.m    | 66 ++++++++++++++++++++--
 3 files changed, 99 insertions(+), 5 deletions(-)

diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c
index c8f34c203e..3c7a993474 100644
--- a/src/joystick/hidapi/SDL_hidapijoystick.c
+++ b/src/joystick/hidapi/SDL_hidapijoystick.c
@@ -1110,6 +1110,40 @@ HIDAPI_IsEquivalentToDevice(Uint16 vendor_id, Uint16 product_id, SDL_HIDAPI_Devi
     return SDL_FALSE;
 }
 
+SDL_bool
+HIDAPI_IsDeviceTypePresent(SDL_GameControllerType type)
+{
+    SDL_HIDAPI_Device *device;
+    SDL_bool result = SDL_FALSE;
+
+    /* Make sure we're initialized, as this could be called from other drivers during startup */
+    if (HIDAPI_JoystickInit() < 0) {
+        return SDL_FALSE;
+    }
+
+    if (SDL_AtomicTryLock(&SDL_HIDAPI_spinlock)) {
+        HIDAPI_UpdateDeviceList();
+        SDL_AtomicUnlock(&SDL_HIDAPI_spinlock);
+    }
+
+    SDL_LockJoysticks();
+    device = SDL_HIDAPI_devices;
+    while (device) {
+        if (device->driver &&
+            SDL_GetJoystickGameControllerType(device->name, device->vendor_id, device->product_id, device->interface_number, device->interface_class, device->interface_subclass, device->interface_protocol) == type) {
+            result = SDL_TRUE;
+            break;
+        }
+        device = device->next;
+    }
+    SDL_UnlockJoysticks();
+
+#ifdef DEBUG_HIDAPI
+    SDL_Log("HIDAPI_IsDeviceTypePresent() returning %s for %d\n", result ? "true" : "false", type);
+#endif
+    return result;
+}
+
 SDL_bool
 HIDAPI_IsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name)
 {
@@ -1152,6 +1186,7 @@ HIDAPI_IsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, cons
         if (device->driver &&
             HIDAPI_IsEquivalentToDevice(vendor_id, product_id, device)) {
             result = SDL_TRUE;
+            break;
         }
         device = device->next;
     }
diff --git a/src/joystick/hidapi/SDL_hidapijoystick_c.h b/src/joystick/hidapi/SDL_hidapijoystick_c.h
index b80c6be4b3..9722775ee1 100644
--- a/src/joystick/hidapi/SDL_hidapijoystick_c.h
+++ b/src/joystick/hidapi/SDL_hidapijoystick_c.h
@@ -119,6 +119,9 @@ extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverXbox360;
 extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverXbox360W;
 extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverXboxOne;
 
+/* Return true if a HID device is present and supported as a joystick of the given type */
+extern SDL_bool HIDAPI_IsDeviceTypePresent(SDL_GameControllerType type);
+
 /* Return true if a HID device is present and supported as a joystick */
 extern SDL_bool HIDAPI_IsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name);
 
diff --git a/src/joystick/iphoneos/SDL_mfijoystick.m b/src/joystick/iphoneos/SDL_mfijoystick.m
index 2e8c137c6d..7aee4f566c 100644
--- a/src/joystick/iphoneos/SDL_mfijoystick.m
+++ b/src/joystick/iphoneos/SDL_mfijoystick.m
@@ -27,6 +27,7 @@
 #include "SDL_stdinc.h"
 #include "../SDL_sysjoystick.h"
 #include "../SDL_joystick_c.h"
+#include "../hidapi/SDL_hidapijoystick_c.h"
 #include "../usb_ids.h"
 
 #include "SDL_mfijoystick_c.h"
@@ -128,7 +129,49 @@ @interface GCMicroGamepad (SDL)
 }
 
 #ifdef SDL_JOYSTICK_MFI
-static void
+static BOOL
+IsControllerPS4(GCController *controller)
+{
+    if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+        if ([controller.productCategory isEqualToString:@"DualShock 4"]) {
+            return TRUE;
+        }
+    } else {
+        if ([controller.vendorName containsString: @"DUALSHOCK"]) {
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+static BOOL
+IsControllerPS5(GCController *controller)
+{
+    if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+        if ([controller.productCategory isEqualToString:@"DualSense"]) {
+            return TRUE;
+        }
+    } else {
+        if ([controller.vendorName containsString: @"DualSense"]) {
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+static BOOL
+IsControllerXbox(GCController *controller)
+{
+    if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+        if ([controller.productCategory isEqualToString:@"Xbox One"]) {
+            return TRUE;
+        }
+    } else {
+        if ([controller.vendorName containsString: @"Xbox"]) {
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+static BOOL
 IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller)
 {
     Uint16 *guid16 = (Uint16 *)device->guid.data;
@@ -153,14 +196,23 @@ @interface GCMicroGamepad (SDL)
 
     if (controller.extendedGamepad) {
         GCExtendedGamepad *gamepad = controller.extendedGamepad;
-        BOOL is_xbox = [controller.vendorName containsString: @"Xbox"];
-        BOOL is_ps4 = [controller.vendorName containsString: @"DUALSHOCK"];
-        BOOL is_ps5 = [controller.vendorName containsString: @"DualSense"];
+        BOOL is_xbox = IsControllerXbox(controller);
+        BOOL is_ps4 = IsControllerPS4(controller);
+        BOOL is_ps5 = IsControllerPS5(controller);
 #if TARGET_OS_TV
         BOOL is_MFi = (!is_xbox && !is_ps4 && !is_ps5);
 #endif
         int nbuttons = 0;
 
+#ifdef SDL_JOYSTICK_HIDAPI
+        if ((is_xbox && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_XBOXONE)) ||
+            (is_ps4 && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_PS4)) ||
+            (is_ps5 && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_PS5))) {
+            /* The HIDAPI driver is taking care of this device */
+            return FALSE;
+        }
+#endif
+
         /* These buttons are part of the original MFi spec */
         device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_A);
         device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_B);
@@ -342,6 +394,7 @@ @interface GCMicroGamepad (SDL)
     /* This will be set when the first button press of the controller is
      * detected. */
     controller.playerIndex = -1;
+    return TRUE;
 }
 #endif /* SDL_JOYSTICK_MFI */
 
@@ -390,7 +443,10 @@ @interface GCMicroGamepad (SDL)
 #endif /* SDL_JOYSTICK_iOS_ACCELEROMETER */
     } else if (controller) {
 #ifdef SDL_JOYSTICK_MFI
-        IOS_AddMFIJoystickDevice(device, controller);
+        if (!IOS_AddMFIJoystickDevice(device, controller)) {
+            SDL_free(device);
+            return;
+        }
 #else
         SDL_free(device);
         return;