From dcd21d042fe417d88bf00b203521eb140828b882 Mon Sep 17 00:00:00 2001 From: Sam Lantinga 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 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 | 906 ++++++++++++++-------- src/joystick/iphoneos/SDL_mfijoystick_c.h | 22 +- 5 files changed, 642 insertions(+), 334 deletions(-) diff --git a/src/joystick/SDL_gamecontroller.c b/src/joystick/SDL_gamecontroller.c index 943be08ac..c01849c73 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 5b8728278..ad7226491 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 484746d44..34249ec6c 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 bf9be56de..37b789aa6 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 #include @@ -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 *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,41 +375,194 @@ 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 *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); + + 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; + } + +#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 (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) { + device->has_xbox_paddles = TRUE; + } + if (controller.physicalInputProfile.buttons[@"Button Share"] != nil) { + device->has_xbox_share_button = TRUE; + } + } +#endif // ENABLE_PHYSICAL_INPUT_PROFILE + + 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 + } 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 (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { + NSDictionary *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; + } + + element = elements[object]; + if (element.analog) { + if ([element isKindOfClass:[GCControllerAxisInput class]] || + [element isKindOfClass:[GCControllerButtonInput class]]) { + return YES; + } + } + 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; + } + + element = elements[object]; + if ([element isKindOfClass:[GCControllerButtonInput class]]) { + return YES; + } + 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); @@ -310,132 +574,38 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle nbuttons += 6; /* 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 (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) { + if (gamepad.leftThumbstickButton) { + device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_LEFTSTICK); + ++nbuttons; + } + if (gamepad.rightThumbstickButton) { + device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_RIGHTSTICK); + ++nbuttons; + } } - if ([gamepad respondsToSelector:@selector(rightThumbstickButton)] && gamepad.rightThumbstickButton) { - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_RIGHTSTICK); - ++nbuttons; - } - if ([gamepad respondsToSelector:@selector(buttonOptions)] && gamepad.buttonOptions) { - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_BACK); - ++nbuttons; - } - /* 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 (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + if (gamepad.buttonOptions) { + device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_BACK); + ++nbuttons; + } } device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_START); ++nbuttons; - has_direct_menu = [gamepad respondsToSelector:@selector(buttonMenu)] && gamepad.buttonMenu; - if (!has_direct_menu) { - device->uses_pause_handler = SDL_TRUE; + 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) { - device->uses_pause_handler = SDL_TRUE; + has_direct_menu = FALSE; } #endif - -#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 (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) { - device->has_xbox_paddles = SDL_TRUE; - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_PADDLE1); - ++nbuttons; - } - if (controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo] != nil) { - device->has_xbox_paddles = SDL_TRUE; - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_PADDLE2); - ++nbuttons; - } - if (controller.physicalInputProfile.buttons[GCInputXboxPaddleThree] != nil) { - device->has_xbox_paddles = SDL_TRUE; - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_PADDLE3); - ++nbuttons; - } - if (controller.physicalInputProfile.buttons[GCInputXboxPaddleFour] != nil) { - device->has_xbox_paddles = SDL_TRUE; - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_PADDLE4); - ++nbuttons; - } - if (controller.physicalInputProfile.buttons[GCInputXboxShareButton] != nil) { - device->has_xbox_share_button = SDL_TRUE; - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_MISC1); - ++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; - } - } 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; - } - - 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 (!has_direct_menu) { + device->pause_button_index = (nbuttons - 1); } device->naxes = 6; /* 2 thumbsticks and 2 triggers */ @@ -443,35 +613,8 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle 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; - device->uses_pause_handler = SDL_TRUE; -#endif + device->pause_button_index = (nbuttons - 1); device->naxes = 0; /* no traditional analog inputs */ device->nhats = 1; /* d-pad */ @@ -497,36 +635,38 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle int nbuttons = 0; device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_A); - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_B); /* Button X on microGamepad */ - nbuttons += 2; + device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_X); /* Button X on microGamepad */ + device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_B); + nbuttons += 3; + device->pause_button_index = (nbuttons - 1); - device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_START); - ++nbuttons; - device->uses_pause_handler = SDL_TRUE; - - vendor = USB_VENDOR_APPLE; - product = 3; - subtype = 3; device->naxes = 2; /* treat the touch surface as two axes */ device->nhats = 0; /* apparently the touch surface-as-dpad is buggy */ device->nbuttons = nbuttons; controller.microGamepad.allowsRotation = SDL_GetHintBoolean(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, SDL_FALSE); } -#endif /* TARGET_OS_TV */ +#endif + else { + /* We don't know how to get input events from this device */ + return SDL_FALSE; + } - if (vendor == USB_VENDOR_APPLE) { - /* Note that this is an MFI controller and what subtype it is */ - device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, 0, name, 'm', subtype); + if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { + signature = 0; + signature = SDL_crc16(signature, device->name, SDL_strlen(device->name)); + for (id key in device->axes) { + const char *string = ((NSString *)key).UTF8String; + signature = SDL_crc16(signature, string, SDL_strlen(string)); + } + for (id key in device->buttons) { + const char *string = ((NSString *)key).UTF8String; + signature = SDL_crc16(signature, string, SDL_strlen(string)); + } } else { - device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, 0, name, 0, subtype); - } - - /* Update the GUID with capability bits */ - { - Uint16 *guid16 = (Uint16 *)device->guid.data; - guid16[6] = SDL_SwapLE16(device->button_mask); + signature = device->button_mask; } + device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, signature, name, 'm', subtype); /* This will be set when the first button press of the controller is * detected. */ @@ -540,15 +680,6 @@ static void IOS_AddJoystickDevice(GCController *controller, SDL_bool acceleromet { SDL_JoystickDeviceItem *device = deviceList; -#if TARGET_OS_TV - if (!SDL_GetHintBoolean(SDL_HINT_TV_REMOTE_AS_JOYSTICK, SDL_TRUE)) { - /* Ignore devices that aren't actually controllers (e.g. remotes), they'll be handled as keyboard input */ - if (controller && !controller.extendedGamepad && !controller.gamepad && controller.microGamepad) { - return; - } - } -#endif - while (device != NULL) { if (device->controller == controller) { return; @@ -563,6 +694,7 @@ static void IOS_AddJoystickDevice(GCController *controller, SDL_bool acceleromet device->accelerometer = accelerometer; device->instance_id = SDL_GetNextJoystickInstanceID(); + device->pause_button_index = -1; if (accelerometer) { #ifdef SDL_JOYSTICK_iOS_ACCELEROMETER @@ -846,11 +978,11 @@ static int IOS_JoystickOpen(SDL_Joystick *joystick, int device_index) #endif } else { #ifdef SDL_JOYSTICK_MFI - if (device->uses_pause_handler) { + if (device->pause_button_index >= 0) { GCController *controller = device->controller; controller.controllerPausedHandler = ^(GCController *c) { if (joystick->hwdata) { - ++joystick->hwdata->num_pause_presses; + joystick->hwdata->pause_button_pressed = SDL_GetTicks(); } }; } @@ -883,7 +1015,7 @@ static int IOS_JoystickOpen(SDL_Joystick *joystick, int device_index) #endif /* SDL_JOYSTICK_MFI */ } } - if (device->remote) { + if (device->is_siri_remote) { ++SDL_AppleTVRemoteOpenedAsJoystick; } @@ -962,10 +1094,10 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) { #ifdef SDL_JOYSTICK_MFI @autoreleasepool { - GCController *controller = joystick->hwdata->controller; + SDL_JoystickDeviceItem *device = joystick->hwdata; + GCController *controller = device->controller; Uint8 hatstate = SDL_HAT_CENTERED; int i; - int pause_button_index = 0; #ifdef DEBUG_CONTROLLER_STATE if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { @@ -978,13 +1110,49 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) for (id key in controller.physicalInputProfile.axes) { GCControllerAxisInput *axis = controller.physicalInputProfile.axes[key]; if (axis.value != 0.0f) - NSLog(@"Axis %@ = %.2f\n", key, axis.value); + NSLog(@"Axis %@ = %g\n", key, axis.value); + } + for (id key in controller.physicalInputProfile.dpads) { + GCControllerDirectionPad *dpad = controller.physicalInputProfile.dpads[key]; + if (dpad.up.isPressed || dpad.down.isPressed || dpad.left.isPressed || dpad.right.isPressed) { + NSLog(@"Hat %@ =%s%s%s%s\n", key, + dpad.up.isPressed ? " UP" : "", + dpad.down.isPressed ? " DOWN" : "", + dpad.left.isPressed ? " LEFT" : "", + dpad.right.isPressed ? " RIGHT" : ""); + } } } } -#endif +#endif /* DEBUG_CONTROLLER_STATE */ - if (controller.extendedGamepad) { + if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { + NSDictionary *elements = controller.physicalInputProfile.elements; + NSDictionary *buttons = controller.physicalInputProfile.buttons; + int axis = 0; + int button = 0; + + for (id key in device->axes) { + Sint16 value; + GCControllerElement *element = elements[key]; + if ([element isKindOfClass:[GCControllerAxisInput class]]) { + value = (Sint16)([(GCControllerAxisInput *)element value] * 32767); + } else { + value = (Sint16)([(GCControllerButtonInput *)element value] * 32767); + } + SDL_PrivateJoystickAxis(joystick, axis++, value); + } + + for (id key in device->buttons) { + Uint8 value; + if (button == device->pause_button_index) { + value = (device->pause_button_pressed > 0); + } else { + value = buttons[key].isPressed; + } + SDL_PrivateJoystickButton(joystick, button++, value); + } + } else if (controller.extendedGamepad) { SDL_bool isstack; GCExtendedGamepad *gamepad = controller.extendedGamepad; @@ -1016,79 +1184,30 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) buttons[button_count++] = gamepad.rightShoulder.isPressed; /* These buttons are available on some newer controllers */ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunguarded-availability-new" - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_LEFTSTICK)) { - buttons[button_count++] = gamepad.leftThumbstickButton.isPressed; + if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) { + if (device->button_mask & (1 << SDL_CONTROLLER_BUTTON_LEFTSTICK)) { + buttons[button_count++] = gamepad.leftThumbstickButton.isPressed; + } + if (device->button_mask & (1 << SDL_CONTROLLER_BUTTON_RIGHTSTICK)) { + buttons[button_count++] = gamepad.rightThumbstickButton.isPressed; + } } - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_RIGHTSTICK)) { - buttons[button_count++] = gamepad.rightThumbstickButton.isPressed; + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + if (device->button_mask & (1 << SDL_CONTROLLER_BUTTON_BACK)) { + buttons[button_count++] = gamepad.buttonOptions.isPressed; + } } - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_BACK)) { - buttons[button_count++] = gamepad.buttonOptions.isPressed; - } - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_GUIDE)) { - buttons[button_count++] = gamepad.buttonHome.isPressed; - } - /* This must be the last button, so we can optionally handle it with pause_button_index below */ - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_START)) { - if (joystick->hwdata->uses_pause_handler) { - pause_button_index = button_count; - buttons[button_count++] = joystick->delayed_guide_button; + if (device->button_mask & (1 << SDL_CONTROLLER_BUTTON_START)) { + if (device->pause_button_index >= 0) { + /* Guaranteed if buttonMenu is not supported on this OS */ + buttons[button_count++] = (device->pause_button_pressed > 0); } else { - buttons[button_count++] = gamepad.buttonMenu.isPressed; + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + buttons[button_count++] = gamepad.buttonMenu.isPressed; + } } } -#ifdef ENABLE_PHYSICAL_INPUT_PROFILE - if (joystick->hwdata->has_dualshock_touchpad) { - GCControllerDirectionPad *dpad; - buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton].isPressed; - - dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]; - if (dpad.xAxis.value || dpad.yAxis.value) { - SDL_PrivateJoystickTouchpad(joystick, 0, 0, SDL_PRESSED, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f); - } else { - SDL_PrivateJoystickTouchpad(joystick, 0, 0, SDL_RELEASED, 0.0f, 0.0f, 1.0f); - } - - dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]; - if (dpad.xAxis.value || dpad.yAxis.value) { - SDL_PrivateJoystickTouchpad(joystick, 0, 1, SDL_PRESSED, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f); - } else { - SDL_PrivateJoystickTouchpad(joystick, 0, 1, SDL_RELEASED, 0.0f, 0.0f, 1.0f); - } - } - - if (joystick->hwdata->has_xbox_paddles) { - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_PADDLE1)) { - buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].isPressed; - } - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_PADDLE2)) { - buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].isPressed; - } - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_PADDLE3)) { - buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].isPressed; - } - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_PADDLE4)) { - buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].isPressed; - } - - /* - SDL_Log("Paddles: [%d,%d,%d,%d]", - controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].isPressed, - controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].isPressed, - controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].isPressed, - controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].isPressed); - */ - } - - if (joystick->hwdata->has_xbox_share_button) { - buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxShareButton].isPressed; - } -#endif -#pragma clang diagnostic pop - hatstate = IOS_MFIJoystickHatStateForDPad(gamepad.dpad); for (i = 0; i < SDL_arraysize(axes); i++) { @@ -1099,30 +1218,6 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) SDL_PrivateJoystickButton(joystick, i, buttons[i]); } -#ifdef ENABLE_MFI_SENSORS - if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { - GCMotion *motion = controller.motion; - if (motion && motion.sensorsActive) { - float data[3]; - - if (motion.hasRotationRate) { - GCRotationRate rate = motion.rotationRate; - data[0] = rate.x; - data[1] = rate.z; - data[2] = -rate.y; - SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_GYRO, 0, data, 3); - } - if (motion.hasGravityAndUserAcceleration) { - GCAcceleration accel = motion.acceleration; - data[0] = -accel.x * SDL_STANDARD_GRAVITY; - data[1] = -accel.y * SDL_STANDARD_GRAVITY; - data[2] = -accel.z * SDL_STANDARD_GRAVITY; - SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_ACCEL, 0, data, 3); - } - } - } -#endif /* ENABLE_MFI_SENSORS */ - SDL_small_free(buttons, isstack); } else if (controller.gamepad) { SDL_bool isstack; @@ -1143,8 +1238,7 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) buttons[button_count++] = gamepad.buttonY.isPressed; buttons[button_count++] = gamepad.leftShoulder.isPressed; buttons[button_count++] = gamepad.rightShoulder.isPressed; - pause_button_index = button_count; - buttons[button_count++] = joystick->delayed_guide_button; + buttons[button_count++] = (device->pause_button_pressed > 0); hatstate = IOS_MFIJoystickHatStateForDPad(gamepad.dpad); @@ -1171,18 +1265,7 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) int button_count = 0; buttons[button_count++] = gamepad.buttonA.isPressed; buttons[button_count++] = gamepad.buttonX.isPressed; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunguarded-availability-new" - /* This must be the last button, so we can optionally handle it with pause_button_index below */ - if (joystick->hwdata->button_mask & (1 << SDL_CONTROLLER_BUTTON_START)) { - if (joystick->hwdata->uses_pause_handler) { - pause_button_index = button_count; - buttons[button_count++] = joystick->delayed_guide_button; - } else { - buttons[button_count++] = gamepad.buttonMenu.isPressed; - } - } -#pragma clang diagnostic pop + buttons[button_count++] = (device->pause_button_pressed > 0); for (i = 0; i < button_count; i++) { SDL_PrivateJoystickButton(joystick, i, buttons[i]); @@ -1194,14 +1277,60 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) SDL_PrivateJoystickHat(joystick, 0, hatstate); } - if (joystick->hwdata->uses_pause_handler) { - for (i = 0; i < joystick->hwdata->num_pause_presses; i++) { - SDL_PrivateJoystickButton(joystick, pause_button_index, SDL_PRESSED); - SDL_PrivateJoystickButton(joystick, pause_button_index, SDL_RELEASED); + if (device->pause_button_pressed) { + /* The pause callback is instantaneous, so we extend the duration to allow "holding down" by pressing it repeatedly */ + const int PAUSE_BUTTON_PRESS_DURATION_MS = 250; + if (SDL_TICKS_PASSED(SDL_GetTicks(), device->pause_button_pressed + PAUSE_BUTTON_PRESS_DURATION_MS)) { + device->pause_button_pressed = 0; } - joystick->hwdata->num_pause_presses = 0; } +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { + if (device->has_dualshock_touchpad) { + GCControllerDirectionPad *dpad; + + dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]; + if (dpad.xAxis.value || dpad.yAxis.value) { + SDL_PrivateJoystickTouchpad(joystick, 0, 0, SDL_PRESSED, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f); + } else { + SDL_PrivateJoystickTouchpad(joystick, 0, 0, SDL_RELEASED, 0.0f, 0.0f, 1.0f); + } + + dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]; + if (dpad.xAxis.value || dpad.yAxis.value) { + SDL_PrivateJoystickTouchpad(joystick, 0, 1, SDL_PRESSED, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f); + } else { + SDL_PrivateJoystickTouchpad(joystick, 0, 1, SDL_RELEASED, 0.0f, 0.0f, 1.0f); + } + } + } +#endif /* ENABLE_PHYSICAL_INPUT_PROFILE */ + +#ifdef ENABLE_MFI_SENSORS + if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { + GCMotion *motion = controller.motion; + if (motion && motion.sensorsActive) { + float data[3]; + + if (motion.hasRotationRate) { + GCRotationRate rate = motion.rotationRate; + data[0] = rate.x; + data[1] = rate.z; + data[2] = -rate.y; + SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_GYRO, 0, data, 3); + } + if (motion.hasGravityAndUserAcceleration) { + GCAcceleration accel = motion.acceleration; + data[0] = -accel.x * SDL_STANDARD_GRAVITY; + data[1] = -accel.y * SDL_STANDARD_GRAVITY; + data[2] = -accel.z * SDL_STANDARD_GRAVITY; + SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_ACCEL, 0, data, 3); + } + } + } +#endif /* ENABLE_MFI_SENSORS */ + #ifdef ENABLE_MFI_BATTERY if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { GCDeviceBattery *battery = controller.battery; @@ -1649,7 +1778,7 @@ static void IOS_JoystickClose(SDL_Joystick *joystick) #endif /* SDL_JOYSTICK_MFI */ } } - if (device->remote) { + if (device->is_siri_remote) { --SDL_AppleTVRemoteOpenedAsJoystick; } } @@ -1690,6 +1819,131 @@ static void IOS_JoystickQuit(void) static SDL_bool IOS_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out) { +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index); + if (device == NULL) { + return SDL_FALSE; + } + + if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { + int axis = 0; + int button = 0; + for (id key in device->axes) { + if ([(NSString *)key isEqualToString:@"Left Thumbstick X Axis"] || + [(NSString *)key isEqualToString:@"Direction Pad X Axis"]) { + out->leftx.kind = EMappingKind_Axis; + out->leftx.target = axis; + } else if ([(NSString *)key isEqualToString:@"Left Thumbstick Y Axis"] || + [(NSString *)key isEqualToString:@"Direction Pad Y Axis"]) { + out->lefty.kind = EMappingKind_Axis; + out->lefty.target = axis; + out->lefty.axis_reversed = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Right Thumbstick X Axis"]) { + out->rightx.kind = EMappingKind_Axis; + out->rightx.target = axis; + } else if ([(NSString *)key isEqualToString:@"Right Thumbstick Y Axis"]) { + out->righty.kind = EMappingKind_Axis; + out->righty.target = axis; + out->righty.axis_reversed = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) { + out->lefttrigger.kind = EMappingKind_Axis; + out->lefttrigger.target = axis; + out->lefttrigger.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) { + out->righttrigger.kind = EMappingKind_Axis; + out->righttrigger.target = axis; + out->righttrigger.half_axis_positive = SDL_TRUE; + } + ++axis; + } + + for (id key in device->buttons) { + SDL_InputMapping *mapping = NULL; + + if ([(NSString *)key isEqualToString:GCInputButtonA]) { + if (device->is_siri_remote > 1) { + /* GCInputButtonA is triggered for any D-Pad press, ignore it in favor of "Button Center" */ + } else { + mapping = &out->a; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonB]) { + if (device->is_switch_joyconL || device->is_switch_joyconR) { + mapping = &out->x; + } else { + mapping = &out->b; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonX]) { + if (device->is_switch_joyconL || device->is_switch_joyconR) { + mapping = &out->b; + } else { + mapping = &out->x; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonY]) { + mapping = &out->y; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Left"]) { + mapping = &out->dpleft; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Right"]) { + mapping = &out->dpright; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Up"]) { + mapping = &out->dpup; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Down"]) { + mapping = &out->dpdown; + } else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Left"]) { + mapping = &out->dpleft; + } else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Right"]) { + mapping = &out->dpright; + } else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Up"]) { + mapping = &out->dpup; + } else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Down"]) { + mapping = &out->dpdown; + } else if ([(NSString *)key isEqualToString:GCInputLeftShoulder]) { + mapping = &out->leftshoulder; + } else if ([(NSString *)key isEqualToString:GCInputRightShoulder]) { + mapping = &out->rightshoulder; + } else if ([(NSString *)key isEqualToString:GCInputLeftThumbstickButton]) { + mapping = &out->leftstick; + } else if ([(NSString *)key isEqualToString:GCInputRightThumbstickButton]) { + mapping = &out->rightstick; + } else if ([(NSString *)key isEqualToString:@"Button Home"]) { + mapping = &out->guide; + } else if ([(NSString *)key isEqualToString:GCInputButtonMenu]) { + if (device->is_siri_remote) { + mapping = &out->b; + } else { + mapping = &out->start; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonOptions]) { + mapping = &out->back; + } else if ([(NSString *)key isEqualToString:@"Button Share"]) { + mapping = &out->misc1; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleOne]) { + mapping = &out->paddle1; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleTwo]) { + mapping = &out->paddle3; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleThree]) { + mapping = &out->paddle2; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleFour]) { + mapping = &out->paddle4; + } else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) { + mapping = &out->lefttrigger; + } else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) { + mapping = &out->righttrigger; + } else if ([(NSString *)key isEqualToString:GCInputDualShockTouchpadButton]) { + mapping = &out->touchpad; + } else if ([(NSString *)key isEqualToString:@"Button Center"]) { + mapping = &out->a; + } + if (mapping && mapping->kind == EMappingKind_None) { + mapping->kind = EMappingKind_Button; + mapping->target = button; + } + ++button; + } + + return SDL_TRUE; + } +#endif /* ENABLE_PHYSICAL_INPUT_PROFILE */ + return SDL_FALSE; } @@ -1728,6 +1982,10 @@ static void GetAppleSFSymbolsNameForElement(GCControllerElement *element, char * static GCControllerDirectionPad *GetDirectionalPadForController(GCController *controller) { + if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { + return controller.physicalInputProfile.dpads[GCInputDirectionPad]; + } + if (controller.extendedGamepad) { return controller.extendedGamepad.dpad; } diff --git a/src/joystick/iphoneos/SDL_mfijoystick_c.h b/src/joystick/iphoneos/SDL_mfijoystick_c.h index bf5311c98..95de2406f 100644 --- a/src/joystick/iphoneos/SDL_mfijoystick_c.h +++ b/src/joystick/iphoneos/SDL_mfijoystick_c.h @@ -26,18 +26,18 @@ #include "SDL_stdinc.h" #include "../SDL_sysjoystick.h" +#include + @class GCController; typedef struct joystick_hwdata { SDL_bool accelerometer; - SDL_bool remote; GCController __unsafe_unretained *controller; void *rumble; - SDL_bool uses_pause_handler; - int num_pause_presses; - Uint32 pause_button_down_time; + int pause_button_index; + Uint32 pause_button_pressed; char *name; SDL_Joystick *joystick; @@ -48,6 +48,20 @@ typedef struct joystick_hwdata int nbuttons; int nhats; Uint32 button_mask; + SDL_bool is_xbox; + SDL_bool is_ps4; + SDL_bool is_ps5; + SDL_bool is_switch_pro; + SDL_bool is_switch_joycon_pair; + SDL_bool is_switch_joyconL; + SDL_bool is_switch_joyconR; + SDL_bool is_stadia; + SDL_bool is_backbone_one; + int is_siri_remote; + + NSArray *axes; + NSArray *buttons; + SDL_bool has_dualshock_touchpad; SDL_bool has_xbox_paddles; SDL_bool has_xbox_share_button;