SDL: Improved GCController handling on Apple platforms (dcd21)

From dcd21d042fe417d88bf00b203521eb140828b882 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 21 Nov 2023 15:17:53 -0800
Subject: [PATCH] Improved GCController handling on Apple platforms

Automatically map controllers as gamepads when using the GCController framework and prefer the physicalInputProfile when possible.

Testing with macOS 13.4.1, macOS 14.1.1, iOS 15.7.4, tvOS 17.1:
* iBuffalo Classic USB Gamepad (macOS only)
* Logitech F310 (macOS only)
* Apple TV remote (tvOS only)
* Nimbus MFi controller
* PS4 DualShock controller
* PS5 DualSense controller
* Xbox Series X controller
* Xbox Elite Series 2 controller
* Nintendo Switch Pro controller
* Nintendo Switch Joy-Con controllers

(cherry picked from commit 0fe5713964287b17e05eb09dd4f83d8580dba254)
Author: Sam Lantinga <slouken@libsdl.org>
Date:   Tue Nov 14 12:58:33 2023 -0800
---
 src/joystick/SDL_gamecontroller.c         |   9 +-
 src/joystick/SDL_joystick.c               |  21 +
 src/joystick/SDL_joystick_c.h             |  18 +-
 src/joystick/iphoneos/SDL_mfijoystick.m   | 902 ++++++++++++++--------
 src/joystick/iphoneos/SDL_mfijoystick_c.h |  22 +-
 5 files changed, 640 insertions(+), 332 deletions(-)

diff --git a/src/joystick/SDL_gamecontroller.c b/src/joystick/SDL_gamecontroller.c
index 943be08ac4e0..c01849c73bc8 100644
--- a/src/joystick/SDL_gamecontroller.c
+++ b/src/joystick/SDL_gamecontroller.c
@@ -813,7 +813,7 @@ static ControllerMapping_t *SDL_PrivateGetControllerMappingForGUID(SDL_JoystickG
 
     /* Try harder to get the best match, or create a mapping */
 
-    if (vendor && product) {
+    if (SDL_JoystickGUIDUsesVersion(guid)) {
         /* Try again, ignoring the version */
         if (crc) {
             mapping = SDL_PrivateMatchControllerMappingForGUID(guid, SDL_TRUE, SDL_FALSE);
@@ -1392,7 +1392,11 @@ static void SDL_PrivateAppendToMappingString(char *mapping_string,
         (void)SDL_snprintf(buffer, sizeof(buffer), "b%i", mapping->target);
         break;
     case EMappingKind_Axis:
-        (void)SDL_snprintf(buffer, sizeof(buffer), "a%i", mapping->target);
+        (void)SDL_snprintf(buffer, sizeof(buffer), "%sa%i%s",
+            mapping->half_axis_positive ? "+" :
+            mapping->half_axis_negative ? "-" : "",
+            mapping->target,
+            mapping->axis_reversed ? "~" : "");
         break;
     case EMappingKind_Hat:
         (void)SDL_snprintf(buffer, sizeof(buffer), "h%i.%i", mapping->target >> 4, mapping->target & 0x0F);
@@ -1450,6 +1454,7 @@ static ControllerMapping_t *SDL_PrivateGenerateAutomaticControllerMapping(const
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "righty", &raw_map->righty);
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "lefttrigger", &raw_map->lefttrigger);
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "righttrigger", &raw_map->righttrigger);
+    SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "touchpad", &raw_map->touchpad);
 
     return SDL_PrivateAddMappingForGUID(guid, mapping, &existing, SDL_CONTROLLER_MAPPING_PRIORITY_DEFAULT);
 }
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index 5b87282785eb..ad7226491ace 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -2285,6 +2285,22 @@ SDL_GameControllerType SDL_GetJoystickGameControllerTypeFromGUID(SDL_JoystickGUI
     return type;
 }
 
+SDL_bool SDL_JoystickGUIDUsesVersion(SDL_JoystickGUID guid)
+{
+    Uint16 vendor, product;
+
+    if (SDL_IsJoystickMFI(guid)) {
+        /* The version bits are used as button capability mask */
+        return SDL_FALSE;
+    }
+
+    SDL_GetJoystickGUIDInfo(guid, &vendor, &product, NULL, NULL);
+    if (vendor && product) {
+        return SDL_TRUE;
+    }
+    return SDL_FALSE;
+}
+
 SDL_bool SDL_IsJoystickXboxOne(Uint16 vendor_id, Uint16 product_id)
 {
     EControllerType eType = GuessControllerType(vendor_id, product_id);
@@ -2463,6 +2479,11 @@ SDL_bool SDL_IsJoystickHIDAPI(SDL_JoystickGUID guid)
     return (guid.data[14] == 'h') ? SDL_TRUE : SDL_FALSE;
 }
 
+SDL_bool SDL_IsJoystickMFI(SDL_JoystickGUID guid)
+{
+    return (guid.data[14] == 'm') ? SDL_TRUE : SDL_FALSE;
+}
+
 SDL_bool SDL_IsJoystickRAWINPUT(SDL_JoystickGUID guid)
 {
     return (guid.data[14] == 'r') ? SDL_TRUE : SDL_FALSE;
diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h
index 484746d44a27..34249ec6ce35 100644
--- a/src/joystick/SDL_joystick_c.h
+++ b/src/joystick/SDL_joystick_c.h
@@ -91,6 +91,9 @@ extern void SDL_SetJoystickGUIDCRC(SDL_JoystickGUID *guid, Uint16 crc);
 extern SDL_GameControllerType SDL_GetJoystickGameControllerTypeFromVIDPID(Uint16 vendor, Uint16 product, const char *name, SDL_bool forUI);
 extern SDL_GameControllerType SDL_GetJoystickGameControllerTypeFromGUID(SDL_JoystickGUID guid, const char *name);
 
+/* Function to return whether a joystick GUID uses the version field */
+extern SDL_bool SDL_JoystickGUIDUsesVersion(SDL_JoystickGUID guid);
+
 /* Function to return whether a joystick is an Xbox One controller */
 extern SDL_bool SDL_IsJoystickXboxOne(Uint16 vendor_id, Uint16 product_id);
 
@@ -131,6 +134,9 @@ extern SDL_bool SDL_IsJoystickWGI(SDL_JoystickGUID guid);
 /* Function to return whether a joystick guid comes from the HIDAPI driver */
 extern SDL_bool SDL_IsJoystickHIDAPI(SDL_JoystickGUID guid);
 
+/* Function to return whether a joystick guid comes from the MFI driver */
+extern SDL_bool SDL_IsJoystickMFI(SDL_JoystickGUID guid);
+
 /* Function to return whether a joystick guid comes from the RAWINPUT driver */
 extern SDL_bool SDL_IsJoystickRAWINPUT(SDL_JoystickGUID guid);
 
@@ -175,16 +181,19 @@ extern SDL_bool SDL_PrivateJoystickValid(SDL_Joystick *joystick);
 
 typedef enum
 {
-    EMappingKind_None = 0,
-    EMappingKind_Button = 1,
-    EMappingKind_Axis = 2,
-    EMappingKind_Hat = 3
+    EMappingKind_None,
+    EMappingKind_Button,
+    EMappingKind_Axis,
+    EMappingKind_Hat,
 } EMappingKind;
 
 typedef struct _SDL_InputMapping
 {
     EMappingKind kind;
     Uint8 target;
+    SDL_bool axis_reversed;
+    SDL_bool half_axis_positive;
+    SDL_bool half_axis_negative;
 } SDL_InputMapping;
 
 typedef struct _SDL_GamepadMapping
@@ -215,6 +224,7 @@ typedef struct _SDL_GamepadMapping
     SDL_InputMapping righty;
     SDL_InputMapping lefttrigger;
     SDL_InputMapping righttrigger;
+    SDL_InputMapping touchpad;
 } SDL_GamepadMapping;
 
 /* Function to get autodetected gamepad controller mapping from the driver */
diff --git a/src/joystick/iphoneos/SDL_mfijoystick.m b/src/joystick/iphoneos/SDL_mfijoystick.m
index bf9be56dece1..37b789aa6269 100644
--- a/src/joystick/iphoneos/SDL_mfijoystick.m
+++ b/src/joystick/iphoneos/SDL_mfijoystick.m
@@ -55,7 +55,6 @@
 
 static id connectObserver = nil;
 static id disconnectObserver = nil;
-static NSString *GCInputXboxShareButton = @"Button Share";
 
 #include <Availability.h>
 #include <objc/message.h>
@@ -236,11 +235,123 @@ static BOOL IsControllerBackboneOne(GCController *controller)
     }
     return FALSE;
 }
+static void CheckControllerSiriRemote(GCController *controller, int *is_siri_remote)
+{
+    if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+        if ([controller.productCategory hasPrefix:@"Siri Remote"]) {
+            *is_siri_remote = 1;
+            SDL_sscanf(controller.productCategory.UTF8String, "Siri Remote (%i%*s Generation)", is_siri_remote);
+            return;
+        }
+    }
+    *is_siri_remote = 0;
+}
+
+static BOOL ElementAlreadyHandled(SDL_JoystickDeviceItem *device, NSString *element, NSDictionary<NSString *, GCControllerElement *> *elements)
+{
+    if ([element isEqualToString:@"Left Thumbstick Left"] ||
+        [element isEqualToString:@"Left Thumbstick Right"]) {
+        if (elements[@"Left Thumbstick X Axis"]) {
+            return TRUE;
+        }
+    }
+    if ([element isEqualToString:@"Left Thumbstick Up"] ||
+        [element isEqualToString:@"Left Thumbstick Down"]) {
+        if (elements[@"Left Thumbstick Y Axis"]) {
+            return TRUE;
+        }
+    }
+    if ([element isEqualToString:@"Right Thumbstick Left"] ||
+        [element isEqualToString:@"Right Thumbstick Right"]) {
+        if (elements[@"Right Thumbstick X Axis"]) {
+            return TRUE;
+        }
+    }
+    if ([element isEqualToString:@"Right Thumbstick Up"] ||
+        [element isEqualToString:@"Right Thumbstick Down"]) {
+        if (elements[@"Right Thumbstick Y Axis"]) {
+            return TRUE;
+        }
+    }
+    if (device->is_siri_remote) {
+        if ([element isEqualToString:@"Direction Pad Left"] ||
+            [element isEqualToString:@"Direction Pad Right"]) {
+            if (elements[@"Direction Pad X Axis"]) {
+                return TRUE;
+            }
+        }
+        if ([element isEqualToString:@"Direction Pad Up"] ||
+            [element isEqualToString:@"Direction Pad Down"]) {
+            if (elements[@"Direction Pad Y Axis"]) {
+                return TRUE;
+            }
+        }
+    } else {
+        if ([element isEqualToString:@"Direction Pad X Axis"]) {
+            if (elements[@"Direction Pad Left"] &&
+                elements[@"Direction Pad Right"]) {
+                return TRUE;
+            }
+        }
+        if ([element isEqualToString:@"Direction Pad Y Axis"]) {
+            if (elements[@"Direction Pad Up"] &&
+                elements[@"Direction Pad Down"]) {
+                return TRUE;
+            }
+        }
+    }
+    if ([element isEqualToString:@"Cardinal Direction Pad X Axis"]) {
+        if (elements[@"Cardinal Direction Pad Left"] &&
+            elements[@"Cardinal Direction Pad Right"]) {
+            return TRUE;
+        }
+    }
+    if ([element isEqualToString:@"Cardinal Direction Pad Y Axis"]) {
+        if (elements[@"Cardinal Direction Pad Up"] &&
+            elements[@"Cardinal Direction Pad Down"]) {
+            return TRUE;
+        }
+    }
+    if ([element isEqualToString:@"Touchpad 1 X Axis"] ||
+        [element isEqualToString:@"Touchpad 1 Y Axis"] ||
+        [element isEqualToString:@"Touchpad 1 Left"] ||
+        [element isEqualToString:@"Touchpad 1 Right"] ||
+        [element isEqualToString:@"Touchpad 1 Up"] ||
+        [element isEqualToString:@"Touchpad 1 Down"] ||
+        [element isEqualToString:@"Touchpad 2 X Axis"] ||
+        [element isEqualToString:@"Touchpad 2 Y Axis"] ||
+        [element isEqualToString:@"Touchpad 2 Left"] ||
+        [element isEqualToString:@"Touchpad 2 Right"] ||
+        [element isEqualToString:@"Touchpad 2 Up"] ||
+        [element isEqualToString:@"Touchpad 2 Down"]) {
+        /* The touchpad is handled separately */
+        return TRUE;
+    }
+    if ([element isEqualToString:@"Button Home"]) {
+        if (device->is_switch_joycon_pair) {
+            /* The Nintendo Switch JoyCon home button doesn't ever show as being held down */
+            return TRUE;
+        }
+#if TARGET_OS_TV
+        /* The OS uses the home button, it's not available to apps */
+        return TRUE;
+#endif
+    }
+    if ([element isEqualToString:@"Button Share"]) {
+        if (device->is_backbone_one) {
+            /* The Backbone app uses share button */
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
 static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller)
 {
     Uint16 vendor = 0;
     Uint16 product = 0;
     Uint8 subtype = 0;
+    Uint16 signature = 0;
     const char *name = NULL;
 
     if (@available(macOS 11.3, iOS 14.5, tvOS 14.5, *)) {
@@ -264,214 +375,246 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle
     device->name = SDL_CreateJoystickName(0, 0, NULL, name);
 
 #ifdef DEBUG_CONTROLLER_PROFILE
+    NSLog(@"Product name: %@\n", controller.vendorName);
+    NSLog(@"Product category: %@\n", controller.productCategory);
+    NSLog(@"Elements available:\n");
     if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
-        if (controller.physicalInputProfile) {
-            for (id key in controller.physicalInputProfile.buttons) {
-                NSLog(@"Button %@ available\n", key);
-            }
-            for (id key in controller.physicalInputProfile.axes) {
-                NSLog(@"Axis %@ available\n", key);
-            }
+        NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
+        for (id key in controller.physicalInputProfile.buttons) {
+            NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital");
+        }
+        for (id key in controller.physicalInputProfile.axes) {
+            NSLog(@"\tAxis: %@\n", key);
+        }
+        for (id key in controller.physicalInputProfile.dpads) {
+            NSLog(@"\tHat: %@\n", key);
         }
     }
 #endif
 
-    if (controller.extendedGamepad) {
-        GCExtendedGamepad *gamepad = controller.extendedGamepad;
-        BOOL is_xbox = IsControllerXbox(controller);
-        BOOL is_ps4 = IsControllerPS4(controller);
-        BOOL is_ps5 = IsControllerPS5(controller);
-        BOOL is_switch_pro = IsControllerSwitchPro(controller);
-        BOOL is_switch_joycon_pair = IsControllerSwitchJoyConPair(controller);
-        BOOL is_stadia = IsControllerStadia(controller);
-        BOOL is_backbone_one = IsControllerBackboneOne(controller);
-        int nbuttons = 0;
-        BOOL has_direct_menu;
-
+    device->is_xbox = IsControllerXbox(controller);
+    device->is_ps4 = IsControllerPS4(controller);
+    device->is_ps5 = IsControllerPS5(controller);
+    device->is_switch_pro = IsControllerSwitchPro(controller);
+    device->is_switch_joycon_pair = IsControllerSwitchJoyConPair(controller);
+    device->is_stadia = IsControllerStadia(controller);
+    device->is_backbone_one = IsControllerBackboneOne(controller);
+    device->is_switch_joyconL = IsControllerSwitchJoyConL(controller);
+    device->is_switch_joyconR = IsControllerSwitchJoyConR(controller);
 #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)) ||
-            (is_switch_pro && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO)) ||
-            (is_switch_joycon_pair && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR, 0, "")) ||
-            (is_stadia && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_GOOGLE_STADIA))) {
-            /* The HIDAPI driver is taking care of this device */
-            return FALSE;
-        }
+    if ((device->is_xbox && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_XBOXONE)) ||
+        (device->is_ps4 && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_PS4)) ||
+        (device->is_ps5 && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_PS5)) ||
+        (device->is_switch_pro && HIDAPI_IsDeviceTypePresent(SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO)) ||
+        (device->is_switch_joycon_pair && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR, 0, "")) ||
+        (device->is_stadia && HIDAPI_IsDevicePresent(USB_VENDOR_GOOGLE, USB_PRODUCT_GOOGLE_STADIA_CONTROLLER, 0, "")) ||
+        (device->is_switch_joyconL && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT, 0, "")) ||
+        (device->is_switch_joyconR && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT, 0, ""))) {
+        /* The HIDAPI driver is taking care of this device */
+        return FALSE;
+    }
 #endif
+    CheckControllerSiriRemote(controller, &device->is_siri_remote);
 
-        /* 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);
-        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_X);
-        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_Y);
-        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
-        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
-        nbuttons += 6;
+    if (device->is_siri_remote && !SDL_GetHintBoolean(SDL_HINT_TV_REMOTE_AS_JOYSTICK, SDL_TRUE)) {
+        /* Ignore remotes, they'll be handled as keyboard input */
+        return SDL_FALSE;
+    }
 
-        /* These buttons are available on some newer controllers */
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wunguarded-availability-new"
-        if ([gamepad respondsToSelector:@selector(leftThumbstickButton)] && gamepad.leftThumbstickButton) {
-            device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_LEFTSTICK);
-            ++nbuttons;
-        }
-        if ([gamepad respondsToSelector:@selector(rightThumbstickButton)] && gamepad.rightThumbstickButton) {
-            device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_RIGHTSTICK);
-            ++nbuttons;
+#ifdef ENABLE_PHYSICAL_INPUT_PROFILE
+    if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
+        if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) {
+            device->has_dualshock_touchpad = TRUE;
         }
-        if ([gamepad respondsToSelector:@selector(buttonOptions)] && gamepad.buttonOptions) {
-            device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_BACK);
-            ++nbuttons;
+        if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) {
+            device->has_xbox_paddles = TRUE;
         }
-        /* The Nintendo Switch JoyCon home button doesn't ever show as being held down */
-        if ([gamepad respondsToSelector:@selector(buttonHome)] && gamepad.buttonHome && !is_switch_joycon_pair) {
-            device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_GUIDE);
-            ++nbuttons;
+        if (controller.physicalInputProfile.buttons[@"Button Share"] != nil) {
+            device->has_xbox_share_button = TRUE;
         }
-        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_START);
-        ++nbuttons;
+    }
+#endif // ENABLE_PHYSICAL_INPUT_PROFILE
 
-        has_direct_menu = [gamepad respondsToSelector:@selector(buttonMenu)] && gamepad.buttonMenu;
-        if (!has_direct_menu) {
-            device->uses_pause_handler = SDL_TRUE;
+    if (device->is_backbone_one) {
+        vendor = USB_VENDOR_BACKBONE;
+        if (device->is_ps5) {
+            product = USB_PRODUCT_BACKBONE_ONE_IOS_PS5;
+        } else {
+            product = USB_PRODUCT_BACKBONE_ONE_IOS;
+        }
+    } else if (device->is_xbox) {
+        vendor = USB_VENDOR_MICROSOFT;
+        if (device->has_xbox_paddles) {
+            /* Assume Xbox One Elite Series 2 Controller unless/until GCController flows VID/PID */
+            product = USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH;
+        } else if (device->has_xbox_share_button) {
+            /* Assume Xbox Series X Controller unless/until GCController flows VID/PID */
+            product = USB_PRODUCT_XBOX_SERIES_X_BLE;
+        } else {
+            /* Assume Xbox One S Bluetooth Controller unless/until GCController flows VID/PID */
+            product = USB_PRODUCT_XBOX_ONE_S_REV1_BLUETOOTH;
+        }
+    } else if (device->is_ps4) {
+        /* Assume DS4 Slim unless/until GCController flows VID/PID */
+        vendor = USB_VENDOR_SONY;
+        product = USB_PRODUCT_SONY_DS4_SLIM;
+        if (device->has_dualshock_touchpad) {
+            subtype = 1;
         }
+    } else if (device->is_ps5) {
+        vendor = USB_VENDOR_SONY;
+        product = USB_PRODUCT_SONY_DS5;
+    } else if (device->is_switch_pro) {
+        vendor = USB_VENDOR_NINTENDO;
+        product = USB_PRODUCT_NINTENDO_SWITCH_PRO;
+    } else if (device->is_switch_joycon_pair) {
+        vendor = USB_VENDOR_NINTENDO;
+        product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR;
+    } else if (device->is_switch_joyconL) {
+        vendor = USB_VENDOR_NINTENDO;
+        product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT;
+    } else if (device->is_switch_joyconR) {
+        vendor = USB_VENDOR_NINTENDO;
+        product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT;
+#ifdef ENABLE_PHYSICAL_INPUT_PROFILE
+    } else if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
+        vendor = USB_VENDOR_APPLE;
+        product = 4;
+        subtype = 4;
+#endif
+    } else if (controller.extendedGamepad) {
+        vendor = USB_VENDOR_APPLE;
+        product = 1;
+        subtype = 1;
+    } else if (controller.gamepad) {
+        vendor = USB_VENDOR_APPLE;
+        product = 2;
+        subtype = 2;
 #if TARGET_OS_TV
-        /* The single menu button isn't very reliable, at least as of tvOS 16.1 */
-        if ((device->button_mask & (1 << SDL_CONTROLLER_BUTTON_BACK)) == 0) {
-            device->uses_pause_handler = SDL_TRUE;
-        }
+    } else if (controller.microGamepad) {
+        vendor = USB_VENDOR_APPLE;
+        product = 3;
+        subtype = 3;
 #endif
+    } else {
+        /* We don't know how to get input events from this device */
+        return SDL_FALSE;
+    }
 
 #ifdef ENABLE_PHYSICAL_INPUT_PROFILE
-        if ([controller respondsToSelector:@selector(physicalInputProfile)]) {
-            if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) {
-                device->has_dualshock_touchpad = SDL_TRUE;
-                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_MISC1);
-                ++nbuttons;
+    if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
+        NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
+
+        /* Provide both axes and analog buttons as SDL axes */
+        device->axes = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]
+                                         filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {
+            GCControllerElement *element;
+
+            if (ElementAlreadyHandled(device, (NSString *)object, elements)) {
+                return NO;
             }
-            if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) {
-                device->has_xbox_paddles = SDL_TRUE;
-                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_PADDLE1);
-                ++nbuttons;
+
+            element = elements[object];
+            if (element.analog) {
+                if ([element isKindOfClass:[GCControllerAxisInput class]] ||
+                    [element isKindOfClass:[GCControllerButtonInput class]]) {
+                    return YES;
+                }
             }
-            if (controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo] != nil) {
-                device->has_xbox_paddles = SDL_TRUE;
-                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_PADDLE2);
-                ++nbuttons;
+            return NO;
+        }]];
+        device->naxes = (int)device->axes.count;
+        device->buttons = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]
+                                            filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {
+            GCControllerElement *element;
+
+            if (ElementAlreadyHandled(device, (NSString *)object, elements)) {
+                return NO;
             }
-            if (controller.physicalInputProfile.buttons[GCInputXboxPaddleThree] != nil) {
-                device->has_xbox_paddles = SDL_TRUE;
-                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_PADDLE3);
-                ++nbuttons;
+
+            element = elements[object];
+            if ([element isKindOfClass:[GCControllerButtonInput class]]) {
+                return YES;
             }
-            if (controller.physicalInputProfile.buttons[GCInputXboxPaddleFour] != nil) {
-                device->has_xbox_paddles = SDL_TRUE;
-                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_PADDLE4);
+            return NO;
+        }]];
+        device->nbuttons = (int)device->buttons.count;
+        subtype = 4;
+
+#ifdef DEBUG_CONTROLLER_PROFILE
+        NSLog(@"Elements used:\n", controller.vendorName);
+        for (id key in device->buttons) {
+            NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital");
+        }
+        for (id key in device->axes) {
+            NSLog(@"\tAxis: %@\n", key);
+        }
+#endif /* DEBUG_CONTROLLER_PROFILE */
+
+#if TARGET_OS_TV
+        /* tvOS turns the menu button into a system gesture, so we grab it here instead */
+        if (elements[GCInputButtonMenu] && !elements[@"Button Home"]) {
+            device->pause_button_index = [device->buttons indexOfObject:GCInputButtonMenu];
+        }
+#endif
+    } else
+#endif
+    if (controller.extendedGamepad) {
+        GCExtendedGamepad *gamepad = controller.extendedGamepad;
+        int nbuttons = 0;
+        BOOL has_direct_menu = FALSE;
+
+        /* 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);
+        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_X);
+        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_Y);
+        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
+        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
+        nbuttons += 6;
+
+        /* These buttons are available on some newer controllers */
+        if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) {
+            if (gamepad.leftThumbstickButton) {
+                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_LEFTSTICK);
                 ++nbuttons;
             }
-            if (controller.physicalInputProfile.buttons[GCInputXboxShareButton] != nil) {
-                device->has_xbox_share_button = SDL_TRUE;
-                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_MISC1);
+            if (gamepad.rightThumbstickButton) {
+                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_RIGHTSTICK);
                 ++nbuttons;
             }
         }
-#endif
-#pragma clang diagnostic pop
-
-        if (is_backbone_one) {
-            vendor = USB_VENDOR_BACKBONE;
-            if (is_ps5) {
-                product = USB_PRODUCT_BACKBONE_ONE_IOS_PS5;
-            } else {
-                product = USB_PRODUCT_BACKBONE_ONE_IOS;
-            }
-            subtype = 0;
-        } else if (is_xbox) {
-            vendor = USB_VENDOR_MICROSOFT;
-            if (device->has_xbox_paddles) {
-                /* Assume Xbox One Elite Series 2 Controller unless/until GCController flows VID/PID */
-                product = USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH;
-                subtype = 1;
-            } else if (device->has_xbox_share_button) {
-                /* Assume Xbox Series X Controller unless/until GCController flows VID/PID */
-                product = USB_PRODUCT_XBOX_SERIES_X_BLE;
-                subtype = 1;
-            } else {
-                /* Assume Xbox One S Bluetooth Controller unless/until GCController flows VID/PID */
-                product = USB_PRODUCT_XBOX_ONE_S_REV1_BLUETOOTH;
-                subtype = 0;
-            }
-        } else if (is_ps4) {
-            /* Assume DS4 Slim unless/until GCController flows VID/PID */
-            vendor = USB_VENDOR_SONY;
-            product = USB_PRODUCT_SONY_DS4_SLIM;
-            if (device->has_dualshock_touchpad) {
-                subtype = 1;
-            } else {
-                subtype = 0;
+        if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+            if (gamepad.buttonOptions) {
+                device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_BACK);
+                ++nbuttons;
             }
-        } else if (is_ps5) {
-            vendor = USB_VENDOR_SONY;
-            product = USB_PRODUCT_SONY_DS5;
-            subtype = 0;
-        } else if (is_switch_pro) {
-            vendor = USB_VENDOR_NINTENDO;
-            product = USB_PRODUCT_NINTENDO_SWITCH_PRO;
-            subtype = 0;
-        } else if (is_switch_joycon_pair) {
-            vendor = USB_VENDOR_NINTENDO;
-            product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR;
-            subtype = 0;
-        } else {
-            vendor = USB_VENDOR_APPLE;
-            product = 1;
-            subtype = 1;
         }
+        device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_START);
+        ++nbuttons;
 
-        if (is_backbone_one) {
-            /* The Backbone app uses share button */
-            if ((device->button_mask & (1 << SDL_CONTROLLER_BUTTON_MISC1)) != 0) {
-                device->button_mask &= ~(1 << SDL_CONTROLLER_BUTTON_MISC1);
-                --nbuttons;
-                device->has_xbox_share_button = SDL_FALSE;
+        if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+            if (gamepad.buttonMenu) {
+                has_direct_menu = TRUE;
             }
         }
+#if TARGET_OS_TV
+        /* The single menu button isn't very reliable, at least as of tvOS 16.1 */
+        if ((device->button_mask & (1 << SDL_CONTROLLER_BUTTON_BACK)) == 0) {
+            has_direct_menu = FALSE;
+        }
+#endif
+        if (!has_direct_menu) {
+            device->pause_button_index = (nbuttons - 1);
+        }
 
         device->naxes = 6; /* 2 thumbsticks and 2 triggers */
         device->nhats = 1; /* d-pad */
         device->nbuttons = nbuttons;
 
     } else if (controller.gamepad) {
-        BOOL is_switch_joyconL = IsControllerSwitchJoyConL(controller);
-        BOOL is_switch_joyconR = IsControllerSwitchJoyConR(controller);
         int nbuttons = 0;
 
-#ifdef SDL_JOYSTICK_HIDAPI
-        if ((is_switch_joyconL && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT, 0, "")) ||
-            (is_switch_joyconR && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT, 0, ""))) {
-            /* The HIDAPI driver is taking care of this device */
-            return FALSE;
-        }
-#else
-        (void)is_switch_joyconL;
-        (void)is_switch_joyconR;
-#endif
-
-        if (is_switch_joyconL) {
-            vendor = USB_VENDOR_NINTENDO;
-            product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT;
-            subtype = 0;
-        } else if (is_switch_joyconR) {
-            vendor = USB_VENDOR_NINTENDO;
-            product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT;
-            subtype = 0;
-        } else {
-            vendor = USB_VENDOR_APPLE;
-            product = 2;
-            subtype = 2;
-        }
-
         /* 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);
@@ -479,14 +622,9 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle
         device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_Y);
         device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
         device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
-#if TARGET_OS_TV
-        /* The menu button is used by the OS and not available to applications */
-        nbuttons += 6;
-#else
         device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_START);
         nbuttons += 7

(Patch may be truncated, please check the link at the top of this post.)