From 0fe5713964287b17e05eb09dd4f83d8580dba254 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Tue, 14 Nov 2023 12:58:33 -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 --- src/joystick/SDL_gamepad.c | 9 +- src/joystick/SDL_joystick.c | 21 + src/joystick/SDL_joystick_c.h | 18 +- src/joystick/apple/SDL_mfijoystick.m | 545 ++++++++++++++++++------- src/joystick/apple/SDL_mfijoystick_c.h | 10 +- 5 files changed, 446 insertions(+), 157 deletions(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index a365254b3..689816abd 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -935,7 +935,7 @@ static GamepadMapping_t *SDL_PrivateGetGamepadMappingForGUID(SDL_JoystickGUID gu /* 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_PrivateMatchGamepadMappingForGUID(guid, SDL_TRUE, SDL_FALSE); @@ -1720,7 +1720,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); @@ -1780,6 +1784,7 @@ static GamepadMapping_t *SDL_PrivateGenerateAutomaticGamepadMapping(const char * 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_GAMEPAD_MAPPING_PRIORITY_DEFAULT); } diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index 664f05e7e..f26c3db20 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -2436,6 +2436,22 @@ SDL_GamepadType SDL_GetGamepadTypeFromGUID(SDL_JoystickGUID guid, const char *na 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); @@ -2656,6 +2672,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 8b37204d5..68424775b 100644 --- a/src/joystick/SDL_joystick_c.h +++ b/src/joystick/SDL_joystick_c.h @@ -80,6 +80,9 @@ extern void SDL_SetJoystickGUIDCRC(SDL_JoystickGUID *guid, Uint16 crc); extern SDL_GamepadType SDL_GetGamepadTypeFromVIDPID(Uint16 vendor, Uint16 product, const char *name, SDL_bool forUI); extern SDL_GamepadType SDL_GetGamepadTypeFromGUID(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); @@ -132,6 +135,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); @@ -166,16 +172,19 @@ extern SDL_bool SDL_IsJoystickValid(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 @@ -206,6 +215,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/apple/SDL_mfijoystick.m b/src/joystick/apple/SDL_mfijoystick.m index 624a8a0fe..b3929f192 100644 --- a/src/joystick/apple/SDL_mfijoystick.m +++ b/src/joystick/apple/SDL_mfijoystick.m @@ -231,6 +231,78 @@ static BOOL IsControllerBackboneOne(GCController *controller) } return FALSE; } +static BOOL IsControllerSiriRemote(GCController *controller) +{ + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + if ([controller.productCategory hasPrefix:@"Siri Remote"]) { + return TRUE; + } + } + return FALSE; +} + +static BOOL ElementAlreadyHandled(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 ([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:@"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 TARGET_OS_TV + if ([element isEqualToString:GCInputButtonHome]) { + /* The OS uses the home button, it's not available to apps */ + return TRUE; + } +#endif + return FALSE; +} + static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller) { Uint16 vendor = 0; @@ -259,13 +331,20 @@ 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) { + NSDictionary *elements = controller.physicalInputProfile.elements; for (id key in controller.physicalInputProfile.buttons) { - NSLog(@"Button %@ available\n", key); + NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital"); } for (id key in controller.physicalInputProfile.axes) { - NSLog(@"Axis %@ available\n", key); + NSLog(@"\tAxis: %@\n", key); + } + for (id key in controller.physicalInputProfile.dpads) { + NSLog(@"\tHat: %@\n", key); } } } @@ -297,6 +376,21 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle (void)is_switch_joyconL; (void)is_switch_joyconR; #endif + device->is_siri_remote = IsControllerSiriRemote(controller); + +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + if ([controller respondsToSelector:@selector(physicalInputProfile)]) { + if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) { + device->has_dualshock_touchpad = SDL_TRUE; + } + if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) { + device->has_xbox_paddles = SDL_TRUE; + } + if (controller.physicalInputProfile.buttons[GCInputXboxShareButton] != nil) { + device->has_xbox_share_button = SDL_TRUE; + } + } +#endif // ENABLE_PHYSICAL_INPUT_PROFILE if (is_backbone_one) { vendor = USB_VENDOR_BACKBONE; @@ -305,21 +399,17 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle } 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 */ @@ -327,29 +417,32 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle 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; + device->has_nintendo_buttons = SDL_TRUE; } else if (is_switch_joycon_pair) { vendor = USB_VENDOR_NINTENDO; product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR; - subtype = 0; + device->has_nintendo_buttons = SDL_TRUE; } else if (is_switch_joyconL) { vendor = USB_VENDOR_NINTENDO; product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT; - subtype = 0; + device->is_single_joycon = SDL_TRUE; } else if (is_switch_joyconR) { vendor = USB_VENDOR_NINTENDO; product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT; - subtype = 0; + device->is_single_joycon = SDL_TRUE; +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + } else if ([controller respondsToSelector:@selector(physicalInputProfile)]) { + vendor = USB_VENDOR_APPLE; + product = 4; + subtype = 4; +#endif } else if (controller.extendedGamepad) { vendor = USB_VENDOR_APPLE; product = 1; @@ -363,12 +456,6 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle vendor = USB_VENDOR_APPLE; product = 3; subtype = 3; -#endif -#ifdef ENABLE_PHYSICAL_INPUT_PROFILE - } else if ([controller respondsToSelector:@selector(physicalInputProfile)]) { - vendor = USB_VENDOR_APPLE; - product = 4; - subtype = 4; #endif } else { vendor = USB_VENDOR_APPLE; @@ -376,6 +463,61 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle subtype = 5; } +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + if ([controller respondsToSelector:@selector(physicalInputProfile)]) { + NSDictionary *elements = controller.physicalInputProfile.elements; + + /* Provide both axes and analog buttons as SDL axes */ + device->use_physical_profile = SDL_TRUE; + device->axes = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) { + if (ElementAlreadyHandled((NSString *)object, elements)) { + return NO; + } + + GCControllerElement *element = elements[object]; + if (element.analog) { + if ([element isKindOfClass:[GCControllerAxisInput class]] || + [element isKindOfClass:[GCControllerButtonInput class]]) { + return YES; + } + } + return NO; + }]]; + device->naxes = device->axes.count; + device->buttons = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) { + if (ElementAlreadyHandled((NSString *)object, elements)) { + return NO; + } + + GCControllerElement *element = elements[object]; + if ([element isKindOfClass:[GCControllerButtonInput class]]) { + return YES; + } + return NO; + }]]; + device->nbuttons = 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[GCInputButtonHome]) { + device->pause_button_index = [device->buttons indexOfObject:GCInputButtonMenu]; + } +#endif + } else +#endif if (controller.extendedGamepad) { GCExtendedGamepad *gamepad = controller.extendedGamepad; int nbuttons = 0; @@ -415,12 +557,12 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle has_direct_menu = [gamepad respondsToSelector:@selector(buttonMenu)] && gamepad.buttonMenu; if (!has_direct_menu) { - device->uses_pause_handler = SDL_TRUE; + device->pause_button_index = (nbuttons - 1); } #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_GAMEPAD_BUTTON_BACK)) == 0) { - device->uses_pause_handler = SDL_TRUE; + device->pause_button_index = (nbuttons - 1); } #endif @@ -437,15 +579,13 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle ++nbuttons; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo] != nil) { - /* TODO: Is this right? SDL_gamepad.h says P2 is the lower right */ device->has_xbox_paddles = SDL_TRUE; - device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1); + device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2); ++nbuttons; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleThree] != nil) { - /* TODO: Is this right? SDL_gamepad.h says P3 is the upper left */ device->has_xbox_paddles = SDL_TRUE; - device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2); + device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1); ++nbuttons; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleFour] != nil) { @@ -485,14 +625,9 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_NORTH); device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER); device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER); -#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_GAMEPAD_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 */ @@ -504,11 +639,9 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_SOUTH); device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_EAST); /* Button X on microGamepad */ - nbuttons += 2; - device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_START); - ++nbuttons; - device->uses_pause_handler = SDL_TRUE; + nbuttons += 3; + device->pause_button_index = (nbuttons - 1); device->naxes = 2; /* treat the touch surface as two axes */ device->nhats = 0; /* apparently the touch surface-as-dpad is buggy */ @@ -516,35 +649,32 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle controller.microGamepad.allowsRotation = SDL_GetHintBoolean(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, SDL_FALSE); } -#endif -#ifdef ENABLE_PHYSICAL_INPUT_PROFILE - else if ([controller respondsToSelector:@selector(physicalInputProfile)]) { - device->use_physical_profile = SDL_TRUE; - device->axes = [controller.physicalInputProfile.axes allKeys]; - device->naxes = controller.physicalInputProfile.axes.count; - device->dpads = [controller.physicalInputProfile.dpads allKeys]; - device->nhats = controller.physicalInputProfile.dpads.count; - device->buttons = [controller.physicalInputProfile.buttons allKeys]; - device->nbuttons = controller.physicalInputProfile.buttons.count; - subtype = 4; - } #endif else { /* We can't detect any inputs on this */ return SDL_FALSE; } + Uint16 signature; + if (device->use_physical_profile) { + 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 { + signature = device->button_mask; + } 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); + device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, signature, name, 'm', subtype); } 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); + device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, signature, name, 0, subtype); } /* This will be set when the first button press of the controller is @@ -582,6 +712,7 @@ static void IOS_AddJoystickDevice(GCController *controller, SDL_bool acceleromet device->accelerometer = accelerometer; device->instance_id = SDL_GetNextObjectID(); + device->pause_button_index = -1; if (accelerometer) { #ifdef SDL_JOYSTICK_iOS_ACCELEROMETER @@ -864,11 +995,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(); } }; } @@ -901,7 +1032,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; } @@ -985,9 +1116,9 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) GCController *controller = device->controller; Uint8 hatstate = SDL_HAT_CENTERED; int i; - int pause_button_index = 0; Uint64 timestamp = SDL_GetTicksNS(); +#define DEBUG_CONTROLLER_STATE #ifdef DEBUG_CONTROLLER_STATE if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { if (controller.physicalInputProfile) { @@ -1016,23 +1147,31 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) #endif if (device->use_physical_profile) { + NSDictionary *elements = controller.physicalInputProfile.elements; + NSDictionary *buttons = controller.physicalInputProfile.buttons; + int axis = 0; for (id key in device->axes) { - Sint16 value = (Sint16)(controller.physicalInputProfile.axes[key].value * 32767); + 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_SendJoystickAxis(timestamp, joystick, axis++, value); } int button = 0; for (id key in device->buttons) { - Uint8 value = controller.physicalInputProfile.buttons[key].isPressed; + Uint8 value; + if (button == device->pause_button_index) { + value = (device->pause_button_pressed > 0); + } else { + value = buttons[key].isPressed; + } SDL_SendJoystickButton(timestamp, joystick, button++, value); } - - int hat = 0; - for (id key in device->dpads) { - hatstate = IOS_MFIJoystickHatStateForDPad(controller.physicalInputProfile.dpads[key]); - SDL_SendJoystickHat(timestamp, joystick, hat++, hatstate); - } } else if (controller.extendedGamepad) { SDL_bool isstack; GCExtendedGamepad *gamepad = controller.extendedGamepad; @@ -1079,11 +1218,9 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) if (device->button_mask & (1 << SDL_GAMEPAD_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 (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) { - if (device->uses_pause_handler) { - pause_button_index = button_count; - buttons[button_count++] = joystick->delayed_guide_button; + if (device->pause_button_index >= 0) { + buttons[button_count++] = (device->pause_button_pressed > 0); } else { buttons[button_count++] = gamepad.buttonMenu.isPressed; } @@ -1091,45 +1228,22 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) #ifdef ENABLE_PHYSICAL_INPUT_PROFILE if (device->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_SendJoystickTouchpad(timestamp, 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_SendJoystickTouchpad(timestamp, joystick, 0, 0, SDL_RELEASED, 0.0f, 0.0f, 1.0f); - } - - dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]; - if (dpad.xAxis.value || dpad.yAxis.value) { - SDL_SendJoystickTouchpad(timestamp, 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_SendJoystickTouchpad(timestamp, joystick, 0, 1, SDL_RELEASED, 0.0f, 0.0f, 1.0f); - } } if (device->has_xbox_paddles) { if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1)) { buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].isPressed; } - if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1)) { + if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2)) { buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].isPressed; } - if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2)) { + if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1)) { buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].isPressed; } if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE2)) { 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 (device->has_xbox_share_button) { @@ -1148,30 +1262,6 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) SDL_SendJoystickButton(timestamp, 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_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, timestamp, 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_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, timestamp, data, 3); - } - } - } -#endif /* ENABLE_MFI_SENSORS */ - SDL_small_free(buttons, isstack); } else if (controller.gamepad) { SDL_bool isstack; @@ -1192,8 +1282,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); @@ -1220,18 +1309,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 (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) { - if (device->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_SendJoystickButton(timestamp, joystick, i, buttons[i]); @@ -1239,18 +1317,62 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) } #endif /* TARGET_OS_TV */ - if (joystick->nhats > 0 && !device->use_physical_profile) { + if (joystick->nhats > 0) { SDL_SendJoystickHat(timestamp, joystick, 0, hatstate); } - if (device->uses_pause_handler) { - for (i = 0; i < device->num_pause_presses; i++) { - SDL_SendJoystickButton(timestamp, joystick, pause_button_index, SDL_PRESSED); - SDL_SendJoystickButton(timestamp, 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_GetTicks() >= device->pause_button_pressed + PAUSE_BUTTON_PRESS_DURATION_MS) { + device->pause_button_pressed = 0; } - device->num_pause_presses = 0; } +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + if (device->has_dualshock_touchpad) { + GCControllerDirectionPad *dpad; + + dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]; + if (dpad.xAxis.value || dpad.yAxis.value) { + SDL_SendJoystickTouchpad(timestamp, 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_SendJoystickTouchpad(timestamp, joystick, 0, 0, SDL_RELEASED, 0.0f, 0.0f, 1.0f); + } + + dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]; + if (dpad.xAxis.value || dpad.yAxis.value) { + SDL_SendJoystickTouchpad(timestamp, 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_SendJoystickTouchpad(timestamp, 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_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, timestamp, 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_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, timestamp, 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; @@ -1698,7 +1820,7 @@ static void IOS_JoystickClose(SDL_Joystick *joystick) #endif /* SDL_JOYSTICK_MFI */ } } - if (device->remote) { + if (device->is_siri_remote) { --SDL_AppleTVRemoteOpenedAsJoystick; } } @@ -1739,7 +1861,138 @@ static void IOS_JoystickQuit(void) static SDL_bool IOS_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out) { - return SDL_FALSE; + SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index); + if (device == NULL) { + return SDL_FALSE; + } + + if (!device->use_physical_profile) { + return SDL_FALSE; + } + + int axis = 0; + for (id key in device->axes) { + if ([(NSString *)key isEqualToString:@"Left Thumbstick X Axis"]) { + out->leftx.kind = EMappingKind_Axis; + out->leftx.target = axis; + } else if ([(NSString *)key isEqualToString:@"Left Thumbstick 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; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Left"] && device->is_siri_remote) { + out->dpleft.kind = EMappingKind_Axis; + out->dpleft.target = axis; + out->dpleft.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Right"] && device->is_siri_remote) { + out->dpright.kind = EMappingKind_Axis; + out->dpright.target = axis; + out->dpright.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Up"] && device->is_siri_remote) { + out->dpup.kind = EMappingKind_Axis; + out->dpup.target = axis; + out->dpup.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Down"] && device->is_siri_remote) { + out->dpdown.kind = EMappingKind_Axis; + out->dpdown.target = axis; + out->dpdown.half_axis_positive = SDL_TRUE; + } + ++axis; + } + + int button = 0; + for (id key in device->buttons) { + SDL_InputMapping *mapping = NULL; + + if ([(NSString *)key isEqualToString:GCInputButtonA]) { + if (device->has_nintendo_buttons) { + mapping = &out->b; + } else { + mapping = &out->a; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonB]) { + if (device->has_nintendo_buttons) { + mapping = &out->a; + } else if (device->is_single_joycon) { + mapping = &out->x; + } else { + mapping = &out->b; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonX]) { + if (device->has_nintendo_buttons) { + mapping = &out->y; + } else if (device->is_single_joycon) { + mapping = &out->b; + } else { + mapping = &out->x; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonY]) { + if (device->has_nintendo_buttons) { + mapping = &out->x; + } else { + mapping = &out->y; + } + } else if ([(NSString *)key isEqualToString:@"Direction Pad Left"] && !device->is_siri_remote) { + mapping = &out->dpleft; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Right"] && !device->is_siri_remote) { + mapping = &out->dpright; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Up"] && !device->is_siri_remote) { + mapping = &out->dpup; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Down"] && !device->is_siri_remote) { + 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:GCInputButtonHome]) { + mapping = &out->guide; + } else if ([(NSString *)key isEqualToString:GCInputButtonMenu]) { + mapping = &out->start; + } else if ([(NSString *)key isEqualToString:GCInputButtonOptions]) { + mapping = &out->back; + } else if ([(NSString *)key isEqualToString:GCInputButtonShare]) { + mapping = &out->misc1; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleOne]) { + mapping = &out->right_paddle1; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleTwo]) { + mapping = &out->right_paddle2; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleThree]) { + mapping = &out->left_paddle1; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleFour]) { + mapping = &out->left_paddle2; + } 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; + } + if (mapping && mapping->kind == EMappingKind_None) { + mapping->kind = EMappingKind_Button; + mapping->target = button; + } + ++button; + } + + return SDL_TRUE; } #if defined(SDL_JOYSTICK_MFI) && defined(__MACOS__) @@ -1777,6 +2030,10 @@ static void GetAppleSFSymbolsNameForElement(GCControllerElement *element, char * static GCControllerDirectionPad *GetDirectionalPadForController(GCController *controller) { + if ([controller respondsToSelector:@selector(physicalInputProfile)]) { + return controller.physicalInputProfile.dpads[GCInputDirectionPad]; + } + if (controller.extendedGamepad) { return controller.extendedGamepad.dpad; } @@ -1789,10 +2046,6 @@ static GCControllerDirectionPad *GetDirectionalPadForController(GCController *co return controller.microGamepad.dpad; } - if ([controller respondsToSelector:@selector(physicalInputProfile)]) { - return controller.physicalInputProfile.dpads[GCInputDirectionPad]; - } - return nil; } #endif /* SDL_JOYSTICK_MFI && ENABLE_PHYSICAL_INPUT_PROFILE */ diff --git a/src/joystick/apple/SDL_mfijoystick_c.h b/src/joystick/apple/SDL_mfijoystick_c.h index 06a9cd8f2..fa289fc84 100644 --- a/src/joystick/apple/SDL_mfijoystick_c.h +++ b/src/joystick/apple/SDL_mfijoystick_c.h @@ -32,13 +32,11 @@ 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; + Uint64 pause_button_pressed; char *name; SDL_Joystick *joystick; @@ -52,10 +50,12 @@ typedef struct joystick_hwdata SDL_bool has_dualshock_touchpad; SDL_bool has_xbox_paddles; SDL_bool has_xbox_share_button; + SDL_bool has_nintendo_buttons; + SDL_bool is_single_joycon; + SDL_bool is_siri_remote; SDL_bool use_physical_profile; NSArray *axes; - NSArray *dpads; NSArray *buttons; struct joystick_hwdata *next;