Path: blob/master/thirdparty/sdl/joystick/apple/SDL_mfijoystick.m
20786 views
/*1Simple DirectMedia Layer2Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>34This software is provided 'as-is', without any express or implied5warranty. In no event will the authors be held liable for any damages6arising from the use of this software.78Permission is granted to anyone to use this software for any purpose,9including commercial applications, and to alter it and redistribute it10freely, subject to the following restrictions:11121. The origin of this software must not be misrepresented; you must not13claim that you wrote the original software. If you use this software14in a product, an acknowledgment in the product documentation would be15appreciated but is not required.162. Altered source versions must be plainly marked as such, and must not be17misrepresented as being the original software.183. This notice may not be removed or altered from any source distribution.19*/20#include "SDL_internal.h"2122// This is the iOS implementation of the SDL joystick API23#include "../SDL_sysjoystick.h"24#include "../SDL_joystick_c.h"25#include "../hidapi/SDL_hidapijoystick_c.h"26#include "../usb_ids.h"27#include "../../events/SDL_events_c.h"2829#include "SDL_mfijoystick_c.h"303132#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS)33#import <CoreMotion/CoreMotion.h>34#endif3536#ifdef SDL_PLATFORM_MACOS37#include <IOKit/hid/IOHIDManager.h>38#include <AppKit/NSApplication.h>39#ifndef NSAppKitVersionNumber10_1540#define NSAppKitVersionNumber10_15 189441#endif42#endif // SDL_PLATFORM_MACOS4344#import <GameController/GameController.h>4546#ifdef SDL_JOYSTICK_MFI47static id connectObserver = nil;48static id disconnectObserver = nil;4950#include <objc/message.h>5152// Fix build errors when using an older SDK by defining these selectors53@interface GCController (SDL)54#if !((__IPHONE_OS_VERSION_MAX_ALLOWED >= 140500) || (__APPLETV_OS_VERSION_MAX_ALLOWED >= 140500) || (__MAC_OS_X_VERSION_MAX_ALLOWED >= 110300))55@property(class, nonatomic, readwrite) BOOL shouldMonitorBackgroundEvents;56#endif57@end5859#import <CoreHaptics/CoreHaptics.h>6061#endif // SDL_JOYSTICK_MFI6263static SDL_JoystickDeviceItem *deviceList = NULL;6465static int numjoysticks = 0;66int SDL_AppleTVRemoteOpenedAsJoystick = 0;6768static SDL_JoystickDeviceItem *GetDeviceForIndex(int device_index)69{70SDL_JoystickDeviceItem *device = deviceList;71int i = 0;7273while (i < device_index) {74if (device == NULL) {75return NULL;76}77device = device->next;78i++;79}8081return device;82}8384#ifdef SDL_JOYSTICK_MFI85static bool IsControllerPS4(GCController *controller)86{87if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {88if ([controller.productCategory isEqualToString:@"DualShock 4"]) {89return true;90}91} else {92if ([controller.vendorName containsString:@"DUALSHOCK"]) {93return true;94}95}96return false;97}98static bool IsControllerPS5(GCController *controller)99{100if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {101if ([controller.productCategory isEqualToString:@"DualSense"]) {102return true;103}104} else {105if ([controller.vendorName containsString:@"DualSense"]) {106return true;107}108}109return false;110}111static bool IsControllerXbox(GCController *controller)112{113if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {114if ([controller.productCategory isEqualToString:@"Xbox One"]) {115return true;116}117} else {118if ([controller.vendorName containsString:@"Xbox"]) {119return true;120}121}122return false;123}124static bool IsControllerSwitchPro(GCController *controller)125{126if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {127if ([controller.productCategory isEqualToString:@"Switch Pro Controller"]) {128return true;129}130}131return false;132}133static bool IsControllerSwitchJoyConL(GCController *controller)134{135if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {136if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (L)"]) {137return true;138}139}140return false;141}142static bool IsControllerSwitchJoyConR(GCController *controller)143{144if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {145if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (R)"]) {146return true;147}148}149return false;150}151static bool IsControllerSwitchJoyConPair(GCController *controller)152{153if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {154if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (L/R)"]) {155return true;156}157}158return false;159}160static bool IsControllerNVIDIASHIELD(GCController *controller)161{162if ([controller.vendorName hasPrefix:@"NVIDIA Controller"]) {163return true;164}165return false;166}167static bool IsControllerStadia(GCController *controller)168{169if ([controller.vendorName hasPrefix:@"Stadia"]) {170return true;171}172return false;173}174static bool IsControllerBackboneOne(GCController *controller)175{176if ([controller.vendorName hasPrefix:@"Backbone One"]) {177return true;178}179return false;180}181static void CheckControllerSiriRemote(GCController *controller, int *is_siri_remote)182{183if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {184if ([controller.productCategory hasPrefix:@"Siri Remote"]) {185*is_siri_remote = 1;186SDL_sscanf(controller.productCategory.UTF8String, "Siri Remote (%i%*s Generation)", is_siri_remote);187return;188}189}190*is_siri_remote = 0;191}192193static bool ElementAlreadyHandled(SDL_JoystickDeviceItem *device, NSString *element, NSDictionary<NSString *, GCControllerElement *> *elements)194{195if ([element isEqualToString:@"Left Thumbstick Left"] ||196[element isEqualToString:@"Left Thumbstick Right"]) {197if (elements[@"Left Thumbstick X Axis"]) {198return true;199}200}201if ([element isEqualToString:@"Left Thumbstick Up"] ||202[element isEqualToString:@"Left Thumbstick Down"]) {203if (elements[@"Left Thumbstick Y Axis"]) {204return true;205}206}207if ([element isEqualToString:@"Right Thumbstick Left"] ||208[element isEqualToString:@"Right Thumbstick Right"]) {209if (elements[@"Right Thumbstick X Axis"]) {210return true;211}212}213if ([element isEqualToString:@"Right Thumbstick Up"] ||214[element isEqualToString:@"Right Thumbstick Down"]) {215if (elements[@"Right Thumbstick Y Axis"]) {216return true;217}218}219if (device->is_siri_remote) {220if ([element isEqualToString:@"Direction Pad Left"] ||221[element isEqualToString:@"Direction Pad Right"]) {222if (elements[@"Direction Pad X Axis"]) {223return true;224}225}226if ([element isEqualToString:@"Direction Pad Up"] ||227[element isEqualToString:@"Direction Pad Down"]) {228if (elements[@"Direction Pad Y Axis"]) {229return true;230}231}232} else {233if ([element isEqualToString:@"Direction Pad X Axis"]) {234if (elements[@"Direction Pad Left"] &&235elements[@"Direction Pad Right"]) {236return true;237}238}239if ([element isEqualToString:@"Direction Pad Y Axis"]) {240if (elements[@"Direction Pad Up"] &&241elements[@"Direction Pad Down"]) {242return true;243}244}245}246if ([element isEqualToString:@"Cardinal Direction Pad X Axis"]) {247if (elements[@"Cardinal Direction Pad Left"] &&248elements[@"Cardinal Direction Pad Right"]) {249return true;250}251}252if ([element isEqualToString:@"Cardinal Direction Pad Y Axis"]) {253if (elements[@"Cardinal Direction Pad Up"] &&254elements[@"Cardinal Direction Pad Down"]) {255return true;256}257}258if ([element isEqualToString:@"Touchpad 1 X Axis"] ||259[element isEqualToString:@"Touchpad 1 Y Axis"] ||260[element isEqualToString:@"Touchpad 1 Left"] ||261[element isEqualToString:@"Touchpad 1 Right"] ||262[element isEqualToString:@"Touchpad 1 Up"] ||263[element isEqualToString:@"Touchpad 1 Down"] ||264[element isEqualToString:@"Touchpad 2 X Axis"] ||265[element isEqualToString:@"Touchpad 2 Y Axis"] ||266[element isEqualToString:@"Touchpad 2 Left"] ||267[element isEqualToString:@"Touchpad 2 Right"] ||268[element isEqualToString:@"Touchpad 2 Up"] ||269[element isEqualToString:@"Touchpad 2 Down"]) {270// The touchpad is handled separately271return true;272}273if ([element isEqualToString:@"Button Home"]) {274if (device->is_switch_joycon_pair) {275// The Nintendo Switch JoyCon home button doesn't ever show as being held down276return true;277}278#ifdef SDL_PLATFORM_TVOS279// The OS uses the home button, it's not available to apps280return true;281#endif282}283if ([element isEqualToString:@"Button Share"]) {284if (device->is_backbone_one) {285// The Backbone app uses share button286return true;287}288}289return false;290}291292static bool IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller)293{294Uint16 vendor = 0;295Uint16 product = 0;296Uint8 subtype = 0;297const char *name = NULL;298299if (@available(macOS 11.3, iOS 14.5, tvOS 14.5, *)) {300if (!GCController.shouldMonitorBackgroundEvents) {301GCController.shouldMonitorBackgroundEvents = YES;302}303}304305/* Explicitly retain the controller because SDL_JoystickDeviceItem is a306* struct, and ARC doesn't work with structs. */307device->controller = (__bridge GCController *)CFBridgingRetain(controller);308309if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {310if (controller.productCategory) {311name = controller.productCategory.UTF8String;312}313} else {314if (controller.vendorName) {315name = controller.vendorName.UTF8String;316}317}318319if (!name) {320name = "MFi Gamepad";321}322323device->name = SDL_CreateJoystickName(0, 0, NULL, name);324325#ifdef DEBUG_CONTROLLER_PROFILE326NSLog(@"Product name: %@\n", controller.vendorName);327NSLog(@"Product category: %@\n", controller.productCategory);328NSLog(@"Elements available:\n");329if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {330NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;331for (id key in controller.physicalInputProfile.buttons) {332NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital");333}334for (id key in controller.physicalInputProfile.axes) {335NSLog(@"\tAxis: %@\n", key);336}337for (id key in controller.physicalInputProfile.dpads) {338NSLog(@"\tHat: %@\n", key);339}340}341#endif // DEBUG_CONTROLLER_PROFILE342343device->is_xbox = IsControllerXbox(controller);344device->is_ps4 = IsControllerPS4(controller);345device->is_ps5 = IsControllerPS5(controller);346device->is_switch_pro = IsControllerSwitchPro(controller);347device->is_switch_joycon_pair = IsControllerSwitchJoyConPair(controller);348device->is_shield = IsControllerNVIDIASHIELD(controller);349device->is_stadia = IsControllerStadia(controller);350device->is_backbone_one = IsControllerBackboneOne(controller);351device->is_switch_joyconL = IsControllerSwitchJoyConL(controller);352device->is_switch_joyconR = IsControllerSwitchJoyConR(controller);353#ifdef SDL_JOYSTICK_HIDAPI354if ((device->is_xbox && (HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_XBOXONE) ||355HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_XBOX360))) ||356(device->is_ps4 && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_PS4)) ||357(device->is_ps5 && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_PS5)) ||358(device->is_switch_pro && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO)) ||359(device->is_switch_joycon_pair && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR, 0, "")) ||360(device->is_shield && HIDAPI_IsDevicePresent(USB_VENDOR_NVIDIA, USB_PRODUCT_NVIDIA_SHIELD_CONTROLLER_V104, 0, "")) ||361(device->is_stadia && HIDAPI_IsDevicePresent(USB_VENDOR_GOOGLE, USB_PRODUCT_GOOGLE_STADIA_CONTROLLER, 0, "")) ||362(device->is_switch_joyconL && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT, 0, "")) ||363(device->is_switch_joyconR && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT, 0, ""))) {364// The HIDAPI driver is taking care of this device365return false;366}367#endif368if (device->is_xbox && SDL_strncmp(name, "GamePad-", 8) == 0) {369// This is a Steam Virtual Gamepad, which isn't supported by GCController370return false;371}372CheckControllerSiriRemote(controller, &device->is_siri_remote);373374if (device->is_siri_remote && !SDL_GetHintBoolean(SDL_HINT_TV_REMOTE_AS_JOYSTICK, true)) {375// Ignore remotes, they'll be handled as keyboard input376return false;377}378379if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {380if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) {381device->has_dualshock_touchpad = TRUE;382}383if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) {384device->has_xbox_paddles = TRUE;385}386if (controller.physicalInputProfile.buttons[@"Button Share"] != nil) {387device->has_xbox_share_button = TRUE;388}389}390391if (device->is_backbone_one) {392vendor = USB_VENDOR_BACKBONE;393if (device->is_ps5) {394product = USB_PRODUCT_BACKBONE_ONE_IOS_PS5;395} else {396product = USB_PRODUCT_BACKBONE_ONE_IOS;397}398} else if (device->is_xbox) {399vendor = USB_VENDOR_MICROSOFT;400if (device->has_xbox_paddles) {401// Assume Xbox One Elite Series 2 Controller unless/until GCController flows VID/PID402product = USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH;403} else if (device->has_xbox_share_button) {404// Assume Xbox Series X Controller unless/until GCController flows VID/PID405product = USB_PRODUCT_XBOX_SERIES_X_BLE;406} else {407// Assume Xbox One S Bluetooth Controller unless/until GCController flows VID/PID408product = USB_PRODUCT_XBOX_ONE_S_REV1_BLUETOOTH;409}410} else if (device->is_ps4) {411// Assume DS4 Slim unless/until GCController flows VID/PID412vendor = USB_VENDOR_SONY;413product = USB_PRODUCT_SONY_DS4_SLIM;414if (device->has_dualshock_touchpad) {415subtype = 1;416}417} else if (device->is_ps5) {418vendor = USB_VENDOR_SONY;419product = USB_PRODUCT_SONY_DS5;420} else if (device->is_switch_pro) {421vendor = USB_VENDOR_NINTENDO;422product = USB_PRODUCT_NINTENDO_SWITCH_PRO;423device->has_nintendo_buttons = TRUE;424} else if (device->is_switch_joycon_pair) {425vendor = USB_VENDOR_NINTENDO;426product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR;427device->has_nintendo_buttons = TRUE;428} else if (device->is_switch_joyconL) {429vendor = USB_VENDOR_NINTENDO;430product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT;431} else if (device->is_switch_joyconR) {432vendor = USB_VENDOR_NINTENDO;433product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT;434} else if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {435vendor = USB_VENDOR_APPLE;436product = 4;437subtype = 4;438} else if (controller.extendedGamepad) {439vendor = USB_VENDOR_APPLE;440product = 1;441subtype = 1;442#ifdef SDL_PLATFORM_TVOS443} else if (controller.microGamepad) {444vendor = USB_VENDOR_APPLE;445product = 3;446subtype = 3;447#endif448} else {449// We don't know how to get input events from this device450return false;451}452453if (SDL_ShouldIgnoreJoystick(vendor, product, 0, name)) {454return false;455}456457if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {458NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;459460// Provide both axes and analog buttons as SDL axes461NSArray *axes = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]462filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {463if (ElementAlreadyHandled(device, (NSString *)object, elements)) {464return false;465}466467GCControllerElement *element = elements[object];468if (element.analog) {469if ([element isKindOfClass:[GCControllerAxisInput class]] ||470[element isKindOfClass:[GCControllerButtonInput class]]) {471return true;472}473}474return false;475}]];476NSArray *buttons = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]477filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {478if (ElementAlreadyHandled(device, (NSString *)object, elements)) {479return false;480}481482GCControllerElement *element = elements[object];483if ([element isKindOfClass:[GCControllerButtonInput class]]) {484return true;485}486return false;487}]];488/* Explicitly retain the arrays because SDL_JoystickDeviceItem is a489* struct, and ARC doesn't work with structs. */490device->naxes = (int)axes.count;491device->axes = (__bridge NSArray *)CFBridgingRetain(axes);492device->nbuttons = (int)buttons.count;493device->buttons = (__bridge NSArray *)CFBridgingRetain(buttons);494subtype = 4;495496#ifdef DEBUG_CONTROLLER_PROFILE497NSLog(@"Elements used:\n");498for (id key in device->buttons) {499NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital");500}501for (id key in device->axes) {502NSLog(@"\tAxis: %@\n", key);503}504#endif // DEBUG_CONTROLLER_PROFILE505506#ifdef SDL_PLATFORM_TVOS507// tvOS turns the menu button into a system gesture, so we grab it here instead508if (elements[GCInputButtonMenu] && !elements[@"Button Home"]) {509device->pause_button_index = (int)[device->buttons indexOfObject:GCInputButtonMenu];510}511#endif512} else if (controller.extendedGamepad) {513GCExtendedGamepad *gamepad = controller.extendedGamepad;514int nbuttons = 0;515BOOL has_direct_menu = FALSE;516517// These buttons are part of the original MFi spec518device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_SOUTH);519device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_EAST);520device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_WEST);521device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_NORTH);522device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER);523device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);524nbuttons += 6;525526// These buttons are available on some newer controllers527if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) {528if (gamepad.leftThumbstickButton) {529device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_STICK);530++nbuttons;531}532if (gamepad.rightThumbstickButton) {533device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK);534++nbuttons;535}536}537if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {538if (gamepad.buttonOptions) {539device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_BACK);540++nbuttons;541}542}543device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_START);544++nbuttons;545546if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {547if (gamepad.buttonMenu) {548has_direct_menu = TRUE;549}550}551#ifdef SDL_PLATFORM_TVOS552// The single menu button isn't very reliable, at least as of tvOS 16.1553if ((device->button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) == 0) {554has_direct_menu = FALSE;555}556#endif557if (!has_direct_menu) {558device->pause_button_index = (nbuttons - 1);559}560561device->naxes = 6; // 2 thumbsticks and 2 triggers562device->nhats = 1; // d-pad563device->nbuttons = nbuttons;564}565#ifdef SDL_PLATFORM_TVOS566else if (controller.microGamepad) {567int nbuttons = 0;568569device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_SOUTH);570device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_WEST); // Button X on microGamepad571device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_EAST);572nbuttons += 3;573device->pause_button_index = (nbuttons - 1);574575device->naxes = 2; // treat the touch surface as two axes576device->nhats = 0; // apparently the touch surface-as-dpad is buggy577device->nbuttons = nbuttons;578579controller.microGamepad.allowsRotation = SDL_GetHintBoolean(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, false);580}581#endif582else {583// We don't know how to get input events from this device584return false;585}586587Uint16 signature;588if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {589signature = 0;590signature = SDL_crc16(signature, device->name, SDL_strlen(device->name));591for (id key in device->axes) {592const char *string = ((NSString *)key).UTF8String;593signature = SDL_crc16(signature, string, SDL_strlen(string));594}595for (id key in device->buttons) {596const char *string = ((NSString *)key).UTF8String;597signature = SDL_crc16(signature, string, SDL_strlen(string));598}599} else {600signature = device->button_mask;601}602device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, signature, NULL, name, 'm', subtype);603604/* This will be set when the first button press of the controller is605* detected. */606controller.playerIndex = -1;607return true;608}609#endif // SDL_JOYSTICK_MFI610611#ifdef SDL_JOYSTICK_MFI612static void IOS_AddJoystickDevice(GCController *controller)613{614SDL_JoystickDeviceItem *device = deviceList;615616while (device != NULL) {617if (device->controller == controller) {618return;619}620device = device->next;621}622623device = (SDL_JoystickDeviceItem *)SDL_calloc(1, sizeof(SDL_JoystickDeviceItem));624if (device == NULL) {625return;626}627628device->instance_id = SDL_GetNextObjectID();629device->pause_button_index = -1;630631if (controller) {632#ifdef SDL_JOYSTICK_MFI633if (!IOS_AddMFIJoystickDevice(device, controller)) {634SDL_free(device->name);635SDL_free(device);636return;637}638#else639SDL_free(device);640return;641#endif // SDL_JOYSTICK_MFI642}643644if (deviceList == NULL) {645deviceList = device;646} else {647SDL_JoystickDeviceItem *lastdevice = deviceList;648while (lastdevice->next != NULL) {649lastdevice = lastdevice->next;650}651lastdevice->next = device;652}653654++numjoysticks;655656SDL_PrivateJoystickAdded(device->instance_id);657}658#endif // SDL_JOYSTICK_MFI659660static SDL_JoystickDeviceItem *IOS_RemoveJoystickDevice(SDL_JoystickDeviceItem *device)661{662SDL_JoystickDeviceItem *prev = NULL;663SDL_JoystickDeviceItem *next = NULL;664SDL_JoystickDeviceItem *item = deviceList;665666if (device == NULL) {667return NULL;668}669670next = device->next;671672while (item != NULL) {673if (item == device) {674break;675}676prev = item;677item = item->next;678}679680// Unlink the device item from the device list.681if (prev) {682prev->next = device->next;683} else if (device == deviceList) {684deviceList = device->next;685}686687if (device->joystick) {688device->joystick->hwdata = NULL;689}690691#ifdef SDL_JOYSTICK_MFI692@autoreleasepool {693// These were explicitly retained in the struct, so they should be explicitly released before freeing the struct.694if (device->controller) {695GCController *controller = CFBridgingRelease((__bridge CFTypeRef)(device->controller));696controller.controllerPausedHandler = nil;697device->controller = nil;698}699if (device->axes) {700CFRelease((__bridge CFTypeRef)device->axes);701device->axes = nil;702}703if (device->buttons) {704CFRelease((__bridge CFTypeRef)device->buttons);705device->buttons = nil;706}707}708#endif // SDL_JOYSTICK_MFI709710--numjoysticks;711712SDL_PrivateJoystickRemoved(device->instance_id);713714SDL_free(device->name);715SDL_free(device);716717return next;718}719720#ifdef SDL_PLATFORM_TVOS721static void SDLCALL SDL_AppleTVRemoteRotationHintChanged(void *udata, const char *name, const char *oldValue, const char *newValue)722{723BOOL allowRotation = newValue != NULL && *newValue != '0';724725@autoreleasepool {726for (GCController *controller in [GCController controllers]) {727if (controller.microGamepad) {728controller.microGamepad.allowsRotation = allowRotation;729}730}731}732}733#endif // SDL_PLATFORM_TVOS734735static bool IOS_JoystickInit(void)736{737if (!SDL_GetHintBoolean(SDL_HINT_JOYSTICK_MFI, true)) {738return true;739}740741#ifdef SDL_PLATFORM_MACOS742if (@available(macOS 10.16, *)) {743// Continue with initialization on macOS 11+744} else {745return true;746}747#endif748749@autoreleasepool {750#ifdef SDL_JOYSTICK_MFI751NSNotificationCenter *center;752#endif753754#ifdef SDL_JOYSTICK_MFI755// GameController.framework was added in iOS 7.756if (![GCController class]) {757return true;758}759760/* For whatever reason, this always returns an empty array on761macOS 11.0.1 */762for (GCController *controller in [GCController controllers]) {763IOS_AddJoystickDevice(controller);764}765766#ifdef SDL_PLATFORM_TVOS767SDL_AddHintCallback(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION,768SDL_AppleTVRemoteRotationHintChanged, NULL);769#endif // SDL_PLATFORM_TVOS770771center = [NSNotificationCenter defaultCenter];772773connectObserver = [center addObserverForName:GCControllerDidConnectNotification774object:nil775queue:nil776usingBlock:^(NSNotification *note) {777GCController *controller = note.object;778SDL_LockJoysticks();779IOS_AddJoystickDevice(controller);780SDL_UnlockJoysticks();781}];782783disconnectObserver = [center addObserverForName:GCControllerDidDisconnectNotification784object:nil785queue:nil786usingBlock:^(NSNotification *note) {787GCController *controller = note.object;788SDL_JoystickDeviceItem *device;789SDL_LockJoysticks();790for (device = deviceList; device != NULL; device = device->next) {791if (device->controller == controller) {792IOS_RemoveJoystickDevice(device);793break;794}795}796SDL_UnlockJoysticks();797}];798#endif // SDL_JOYSTICK_MFI799}800801return true;802}803804static int IOS_JoystickGetCount(void)805{806return numjoysticks;807}808809static void IOS_JoystickDetect(void)810{811}812813static bool IOS_JoystickIsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name)814{815// We don't override any other drivers through this method816return false;817}818819static const char *IOS_JoystickGetDeviceName(int device_index)820{821SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);822return device ? device->name : "Unknown";823}824825static const char *IOS_JoystickGetDevicePath(int device_index)826{827return NULL;828}829830static int IOS_JoystickGetDeviceSteamVirtualGamepadSlot(int device_index)831{832return -1;833}834835static int IOS_JoystickGetDevicePlayerIndex(int device_index)836{837#ifdef SDL_JOYSTICK_MFI838SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);839if (device && device->controller) {840return (int)device->controller.playerIndex;841}842#endif843return -1;844}845846static void IOS_JoystickSetDevicePlayerIndex(int device_index, int player_index)847{848#ifdef SDL_JOYSTICK_MFI849SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);850if (device && device->controller) {851device->controller.playerIndex = player_index;852}853#endif854}855856static SDL_GUID IOS_JoystickGetDeviceGUID(int device_index)857{858SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);859SDL_GUID guid;860if (device) {861guid = device->guid;862} else {863SDL_zero(guid);864}865return guid;866}867868static SDL_JoystickID IOS_JoystickGetDeviceInstanceID(int device_index)869{870SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);871return device ? device->instance_id : 0;872}873874static bool IOS_JoystickOpen(SDL_Joystick *joystick, int device_index)875{876SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);877if (device == NULL) {878return SDL_SetError("Could not open Joystick: no hardware device for the specified index");879}880881joystick->hwdata = device;882883joystick->naxes = device->naxes;884joystick->nhats = device->nhats;885joystick->nbuttons = device->nbuttons;886887if (device->has_dualshock_touchpad) {888SDL_PrivateJoystickAddTouchpad(joystick, 2);889}890891device->joystick = joystick;892893@autoreleasepool {894#ifdef SDL_JOYSTICK_MFI895if (device->pause_button_index >= 0) {896GCController *controller = device->controller;897controller.controllerPausedHandler = ^(GCController *c) {898if (joystick->hwdata) {899joystick->hwdata->pause_button_pressed = SDL_GetTicks();900}901};902}903904if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {905GCController *controller = joystick->hwdata->controller;906GCMotion *motion = controller.motion;907if (motion && motion.hasRotationRate) {908SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 0.0f);909}910if (motion && motion.hasGravityAndUserAcceleration) {911SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 0.0f);912}913}914915if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {916GCController *controller = joystick->hwdata->controller;917for (id key in controller.physicalInputProfile.buttons) {918GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];919if ([button isBoundToSystemGesture]) {920button.preferredSystemGestureState = GCSystemGestureStateDisabled;921}922}923}924925if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {926GCController *controller = device->controller;927if (controller.light) {928SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RGB_LED_BOOLEAN, true);929}930931if (controller.haptics) {932for (GCHapticsLocality locality in controller.haptics.supportedLocalities) {933if ([locality isEqualToString:GCHapticsLocalityHandles]) {934SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, true);935} else if ([locality isEqualToString:GCHapticsLocalityTriggers]) {936SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_TRIGGER_RUMBLE_BOOLEAN, true);937}938}939}940}941#endif // SDL_JOYSTICK_MFI942}943if (device->is_siri_remote) {944++SDL_AppleTVRemoteOpenedAsJoystick;945}946947return true;948}949950#ifdef SDL_JOYSTICK_MFI951static Uint8 IOS_MFIJoystickHatStateForDPad(GCControllerDirectionPad *dpad)952{953Uint8 hat = 0;954955if (dpad.up.isPressed) {956hat |= SDL_HAT_UP;957} else if (dpad.down.isPressed) {958hat |= SDL_HAT_DOWN;959}960961if (dpad.left.isPressed) {962hat |= SDL_HAT_LEFT;963} else if (dpad.right.isPressed) {964hat |= SDL_HAT_RIGHT;965}966967if (hat == 0) {968return SDL_HAT_CENTERED;969}970971return hat;972}973#endif974975static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick)976{977#ifdef SDL_JOYSTICK_MFI978@autoreleasepool {979SDL_JoystickDeviceItem *device = joystick->hwdata;980GCController *controller = device->controller;981Uint8 hatstate = SDL_HAT_CENTERED;982int i;983Uint64 timestamp = SDL_GetTicksNS();984985#ifdef DEBUG_CONTROLLER_STATE986if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {987if (controller.physicalInputProfile) {988for (id key in controller.physicalInputProfile.buttons) {989GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];990if (button.isPressed)991NSLog(@"Button %@ = %s\n", key, button.isPressed ? "pressed" : "released");992}993for (id key in controller.physicalInputProfile.axes) {994GCControllerAxisInput *axis = controller.physicalInputProfile.axes[key];995if (axis.value != 0.0f)996NSLog(@"Axis %@ = %g\n", key, axis.value);997}998for (id key in controller.physicalInputProfile.dpads) {999GCControllerDirectionPad *dpad = controller.physicalInputProfile.dpads[key];1000if (dpad.up.isPressed || dpad.down.isPressed || dpad.left.isPressed || dpad.right.isPressed) {1001NSLog(@"Hat %@ =%s%s%s%s\n", key,1002dpad.up.isPressed ? " UP" : "",1003dpad.down.isPressed ? " DOWN" : "",1004dpad.left.isPressed ? " LEFT" : "",1005dpad.right.isPressed ? " RIGHT" : "");1006}1007}1008}1009}1010#endif // DEBUG_CONTROLLER_STATE10111012if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1013NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;1014NSDictionary<NSString *, GCControllerButtonInput *> *buttons = controller.physicalInputProfile.buttons;10151016int axis = 0;1017for (id key in device->axes) {1018Sint16 value;1019GCControllerElement *element = elements[key];1020if ([element isKindOfClass:[GCControllerAxisInput class]]) {1021value = (Sint16)([(GCControllerAxisInput *)element value] * 32767);1022} else {1023value = (Sint16)([(GCControllerButtonInput *)element value] * 32767);1024}1025SDL_SendJoystickAxis(timestamp, joystick, axis++, value);1026}10271028int button = 0;1029for (id key in device->buttons) {1030bool down;1031if (button == device->pause_button_index) {1032down = (device->pause_button_pressed > 0);1033} else {1034down = buttons[key].isPressed;1035}1036SDL_SendJoystickButton(timestamp, joystick, button++, down);1037}1038} else if (controller.extendedGamepad) {1039bool isstack;1040GCExtendedGamepad *gamepad = controller.extendedGamepad;10411042// Axis order matches the XInput Windows mappings.1043Sint16 axes[] = {1044(Sint16)(gamepad.leftThumbstick.xAxis.value * 32767),1045(Sint16)(gamepad.leftThumbstick.yAxis.value * -32767),1046(Sint16)((gamepad.leftTrigger.value * 65535) - 32768),1047(Sint16)(gamepad.rightThumbstick.xAxis.value * 32767),1048(Sint16)(gamepad.rightThumbstick.yAxis.value * -32767),1049(Sint16)((gamepad.rightTrigger.value * 65535) - 32768),1050};10511052// Button order matches the XInput Windows mappings.1053bool *buttons = SDL_small_alloc(bool, joystick->nbuttons, &isstack);1054int button_count = 0;10551056if (buttons == NULL) {1057return;1058}10591060// These buttons are part of the original MFi spec1061buttons[button_count++] = gamepad.buttonA.isPressed;1062buttons[button_count++] = gamepad.buttonB.isPressed;1063buttons[button_count++] = gamepad.buttonX.isPressed;1064buttons[button_count++] = gamepad.buttonY.isPressed;1065buttons[button_count++] = gamepad.leftShoulder.isPressed;1066buttons[button_count++] = gamepad.rightShoulder.isPressed;10671068// These buttons are available on some newer controllers1069if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) {1070if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_STICK)) {1071buttons[button_count++] = gamepad.leftThumbstickButton.isPressed;1072}1073if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK)) {1074buttons[button_count++] = gamepad.rightThumbstickButton.isPressed;1075}1076}1077if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {1078if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) {1079buttons[button_count++] = gamepad.buttonOptions.isPressed;1080}1081}1082if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) {1083if (device->pause_button_index >= 0) {1084// Guaranteed if buttonMenu is not supported on this OS1085buttons[button_count++] = (device->pause_button_pressed > 0);1086} else {1087if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {1088buttons[button_count++] = gamepad.buttonMenu.isPressed;1089}1090}1091}10921093hatstate = IOS_MFIJoystickHatStateForDPad(gamepad.dpad);10941095for (i = 0; i < SDL_arraysize(axes); i++) {1096SDL_SendJoystickAxis(timestamp, joystick, i, axes[i]);1097}10981099for (i = 0; i < button_count; i++) {1100SDL_SendJoystickButton(timestamp, joystick, i, buttons[i]);1101}11021103SDL_small_free(buttons, isstack);1104}1105#ifdef SDL_PLATFORM_TVOS1106else if (controller.microGamepad) {1107GCMicroGamepad *gamepad = controller.microGamepad;11081109Sint16 axes[] = {1110(Sint16)(gamepad.dpad.xAxis.value * 32767),1111(Sint16)(gamepad.dpad.yAxis.value * -32767),1112};11131114for (i = 0; i < SDL_arraysize(axes); i++) {1115SDL_SendJoystickAxis(timestamp, joystick, i, axes[i]);1116}11171118bool buttons[joystick->nbuttons];1119int button_count = 0;1120buttons[button_count++] = gamepad.buttonA.isPressed;1121buttons[button_count++] = gamepad.buttonX.isPressed;1122buttons[button_count++] = (device->pause_button_pressed > 0);11231124for (i = 0; i < button_count; i++) {1125SDL_SendJoystickButton(timestamp, joystick, i, buttons[i]);1126}1127}1128#endif // SDL_PLATFORM_TVOS11291130if (joystick->nhats > 0) {1131SDL_SendJoystickHat(timestamp, joystick, 0, hatstate);1132}11331134if (device->pause_button_pressed) {1135// The pause callback is instantaneous, so we extend the duration to allow "holding down" by pressing it repeatedly1136const int PAUSE_BUTTON_PRESS_DURATION_MS = 250;1137if (SDL_GetTicks() >= device->pause_button_pressed + PAUSE_BUTTON_PRESS_DURATION_MS) {1138device->pause_button_pressed = 0;1139}1140}11411142if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1143if (device->has_dualshock_touchpad) {1144GCControllerDirectionPad *dpad;11451146dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne];1147if (dpad.xAxis.value != 0.f || dpad.yAxis.value != 0.f) {1148SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, true, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f);1149} else {1150SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, false, 0.0f, 0.0f, 1.0f);1151}11521153dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo];1154if (dpad.xAxis.value != 0.f || dpad.yAxis.value != 0.f) {1155SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, true, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f);1156} else {1157SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, false, 0.0f, 0.0f, 1.0f);1158}1159}1160}11611162if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1163GCMotion *motion = controller.motion;1164if (motion && motion.sensorsActive) {1165float data[3];11661167if (motion.hasRotationRate) {1168GCRotationRate rate = motion.rotationRate;1169data[0] = rate.x;1170data[1] = rate.z;1171data[2] = -rate.y;1172SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, timestamp, data, 3);1173}1174if (motion.hasGravityAndUserAcceleration) {1175GCAcceleration accel = motion.acceleration;1176data[0] = -accel.x * SDL_STANDARD_GRAVITY;1177data[1] = -accel.y * SDL_STANDARD_GRAVITY;1178data[2] = -accel.z * SDL_STANDARD_GRAVITY;1179SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, timestamp, data, 3);1180}1181}1182}11831184if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1185GCDeviceBattery *battery = controller.battery;1186if (battery) {1187SDL_PowerState state = SDL_POWERSTATE_UNKNOWN;1188int percent = (int)SDL_roundf(battery.batteryLevel * 100.0f);11891190switch (battery.batteryState) {1191case GCDeviceBatteryStateDischarging:1192state = SDL_POWERSTATE_ON_BATTERY;1193break;1194case GCDeviceBatteryStateCharging:1195state = SDL_POWERSTATE_CHARGING;1196break;1197case GCDeviceBatteryStateFull:1198state = SDL_POWERSTATE_CHARGED;1199break;1200default:1201break;1202}12031204SDL_SendJoystickPowerInfo(joystick, state, percent);1205}1206}1207}1208#endif // SDL_JOYSTICK_MFI1209}12101211#ifdef SDL_JOYSTICK_MFI1212@interface SDL3_RumbleMotor : NSObject1213@property(nonatomic, strong) CHHapticEngine *engine API_AVAILABLE(macos(10.16), ios(13.0), tvos(14.0));1214@property(nonatomic, strong) id<CHHapticPatternPlayer> player API_AVAILABLE(macos(10.16), ios(13.0), tvos(14.0));1215@property bool active;1216@end12171218@implementation SDL3_RumbleMotor1219{1220}12211222- (void)cleanup1223{1224@autoreleasepool {1225if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1226if (self.player != nil) {1227[self.player cancelAndReturnError:nil];1228self.player = nil;1229}1230if (self.engine != nil) {1231[self.engine stopWithCompletionHandler:nil];1232self.engine = nil;1233}1234}1235}1236}12371238- (bool)setIntensity:(float)intensity1239{1240@autoreleasepool {1241if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1242NSError *error = nil;1243CHHapticDynamicParameter *param;12441245if (self.engine == nil) {1246return SDL_SetError("Haptics engine was stopped");1247}12481249if (intensity == 0.0f) {1250if (self.player && self.active) {1251[self.player stopAtTime:0 error:&error];1252}1253self.active = false;1254return true;1255}12561257if (self.player == nil) {1258CHHapticEventParameter *event_param = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];1259CHHapticEvent *event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObjects:event_param, nil] relativeTime:0 duration:GCHapticDurationInfinite];1260CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:event] parameters:[[NSArray alloc] init] error:&error];1261if (error != nil) {1262return SDL_SetError("Couldn't create haptic pattern: %s", [error.localizedDescription UTF8String]);1263}12641265self.player = [self.engine createPlayerWithPattern:pattern error:&error];1266if (error != nil) {1267return SDL_SetError("Couldn't create haptic player: %s", [error.localizedDescription UTF8String]);1268}1269self.active = false;1270}12711272param = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:intensity relativeTime:0];1273[self.player sendParameters:[NSArray arrayWithObject:param] atTime:0 error:&error];1274if (error != nil) {1275return SDL_SetError("Couldn't update haptic player: %s", [error.localizedDescription UTF8String]);1276}12771278if (!self.active) {1279[self.player startAtTime:0 error:&error];1280self.active = true;1281}1282}12831284return true;1285}1286}12871288- (id)initWithController:(GCController *)controller locality:(GCHapticsLocality)locality API_AVAILABLE(macos(10.16), ios(14.0), tvos(14.0))1289{1290@autoreleasepool {1291NSError *error;1292__weak __typeof(self) weakSelf;1293self = [super init];1294weakSelf = self;12951296self.engine = [controller.haptics createEngineWithLocality:locality];1297if (self.engine == nil) {1298SDL_SetError("Couldn't create haptics engine");1299return nil;1300}13011302[self.engine startAndReturnError:&error];1303if (error != nil) {1304SDL_SetError("Couldn't start haptics engine");1305return nil;1306}13071308self.engine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {1309SDL3_RumbleMotor *_this = weakSelf;1310if (_this == nil) {1311return;1312}13131314_this.player = nil;1315_this.engine = nil;1316};1317self.engine.resetHandler = ^{1318SDL3_RumbleMotor *_this = weakSelf;1319if (_this == nil) {1320return;1321}13221323_this.player = nil;1324[_this.engine startAndReturnError:nil];1325};13261327return self;1328}1329}13301331@end13321333@interface SDL3_RumbleContext : NSObject1334@property(nonatomic, strong) SDL3_RumbleMotor *lowFrequencyMotor;1335@property(nonatomic, strong) SDL3_RumbleMotor *highFrequencyMotor;1336@property(nonatomic, strong) SDL3_RumbleMotor *leftTriggerMotor;1337@property(nonatomic, strong) SDL3_RumbleMotor *rightTriggerMotor;1338@end13391340@implementation SDL3_RumbleContext1341{1342}13431344- (id)initWithLowFrequencyMotor:(SDL3_RumbleMotor *)low_frequency_motor1345HighFrequencyMotor:(SDL3_RumbleMotor *)high_frequency_motor1346LeftTriggerMotor:(SDL3_RumbleMotor *)left_trigger_motor1347RightTriggerMotor:(SDL3_RumbleMotor *)right_trigger_motor1348{1349self = [super init];1350self.lowFrequencyMotor = low_frequency_motor;1351self.highFrequencyMotor = high_frequency_motor;1352self.leftTriggerMotor = left_trigger_motor;1353self.rightTriggerMotor = right_trigger_motor;1354return self;1355}13561357- (bool)rumbleWithLowFrequency:(Uint16)low_frequency_rumble andHighFrequency:(Uint16)high_frequency_rumble1358{1359bool result = true;13601361result &= [self.lowFrequencyMotor setIntensity:((float)low_frequency_rumble / 65535.0f)];1362result &= [self.highFrequencyMotor setIntensity:((float)high_frequency_rumble / 65535.0f)];1363return result;1364}13651366- (bool)rumbleLeftTrigger:(Uint16)left_rumble andRightTrigger:(Uint16)right_rumble1367{1368bool result = false;13691370if (self.leftTriggerMotor && self.rightTriggerMotor) {1371result &= [self.leftTriggerMotor setIntensity:((float)left_rumble / 65535.0f)];1372result &= [self.rightTriggerMotor setIntensity:((float)right_rumble / 65535.0f)];1373} else {1374result = SDL_Unsupported();1375}1376return result;1377}13781379- (void)cleanup1380{1381[self.lowFrequencyMotor cleanup];1382[self.highFrequencyMotor cleanup];1383}13841385@end13861387static SDL3_RumbleContext *IOS_JoystickInitRumble(GCController *controller)1388{1389@autoreleasepool {1390if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1391SDL3_RumbleMotor *low_frequency_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftHandle];1392SDL3_RumbleMotor *high_frequency_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightHandle];1393SDL3_RumbleMotor *left_trigger_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftTrigger];1394SDL3_RumbleMotor *right_trigger_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightTrigger];1395if (low_frequency_motor && high_frequency_motor) {1396return [[SDL3_RumbleContext alloc] initWithLowFrequencyMotor:low_frequency_motor1397HighFrequencyMotor:high_frequency_motor1398LeftTriggerMotor:left_trigger_motor1399RightTriggerMotor:right_trigger_motor];1400}1401}1402}1403return nil;1404}14051406#endif // SDL_JOYSTICK_MFI14071408static bool IOS_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)1409{1410#ifdef SDL_JOYSTICK_MFI1411SDL_JoystickDeviceItem *device = joystick->hwdata;14121413if (device == NULL) {1414return SDL_SetError("Controller is no longer connected");1415}14161417if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1418if (!device->rumble && device->controller && device->controller.haptics) {1419SDL3_RumbleContext *rumble = IOS_JoystickInitRumble(device->controller);1420if (rumble) {1421device->rumble = (void *)CFBridgingRetain(rumble);1422}1423}1424}14251426if (device->rumble) {1427SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;1428return [rumble rumbleWithLowFrequency:low_frequency_rumble andHighFrequency:high_frequency_rumble];1429}1430#endif1431return SDL_Unsupported();1432}14331434static bool IOS_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)1435{1436#ifdef SDL_JOYSTICK_MFI1437SDL_JoystickDeviceItem *device = joystick->hwdata;14381439if (device == NULL) {1440return SDL_SetError("Controller is no longer connected");1441}14421443if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1444if (!device->rumble && device->controller && device->controller.haptics) {1445SDL3_RumbleContext *rumble = IOS_JoystickInitRumble(device->controller);1446if (rumble) {1447device->rumble = (void *)CFBridgingRetain(rumble);1448}1449}1450}14511452if (device->rumble) {1453SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;1454return [rumble rumbleLeftTrigger:left_rumble andRightTrigger:right_rumble];1455}1456#endif1457return SDL_Unsupported();1458}14591460static bool IOS_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)1461{1462@autoreleasepool {1463SDL_JoystickDeviceItem *device = joystick->hwdata;14641465if (device == NULL) {1466return SDL_SetError("Controller is no longer connected");1467}14681469if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1470GCController *controller = device->controller;1471GCDeviceLight *light = controller.light;1472if (light) {1473light.color = [[GCColor alloc] initWithRed:(float)red / 255.0f1474green:(float)green / 255.0f1475blue:(float)blue / 255.0f];1476return true;1477}1478}1479}1480return SDL_Unsupported();1481}14821483static bool IOS_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size)1484{1485return SDL_Unsupported();1486}14871488static bool IOS_JoystickSetSensorsEnabled(SDL_Joystick *joystick, bool enabled)1489{1490@autoreleasepool {1491SDL_JoystickDeviceItem *device = joystick->hwdata;14921493if (device == NULL) {1494return SDL_SetError("Controller is no longer connected");1495}14961497if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1498GCController *controller = device->controller;1499GCMotion *motion = controller.motion;1500if (motion) {1501motion.sensorsActive = enabled ? YES : NO;1502return true;1503}1504}1505}15061507return SDL_Unsupported();1508}15091510static void IOS_JoystickUpdate(SDL_Joystick *joystick)1511{1512SDL_JoystickDeviceItem *device = joystick->hwdata;15131514if (device == NULL) {1515return;1516}15171518if (device->controller) {1519IOS_MFIJoystickUpdate(joystick);1520}1521}15221523static void IOS_JoystickClose(SDL_Joystick *joystick)1524{1525SDL_JoystickDeviceItem *device = joystick->hwdata;15261527if (device == NULL) {1528return;1529}15301531device->joystick = NULL;15321533#ifdef SDL_JOYSTICK_MFI1534@autoreleasepool {1535if (device->rumble) {1536SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;15371538[rumble cleanup];1539CFRelease(device->rumble);1540device->rumble = NULL;1541}15421543if (device->controller) {1544GCController *controller = device->controller;1545controller.controllerPausedHandler = nil;1546controller.playerIndex = -1;15471548if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1549for (id key in controller.physicalInputProfile.buttons) {1550GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];1551if ([button isBoundToSystemGesture]) {1552button.preferredSystemGestureState = GCSystemGestureStateEnabled;1553}1554}1555}1556}1557}1558#endif // SDL_JOYSTICK_MFI15591560if (device->is_siri_remote) {1561--SDL_AppleTVRemoteOpenedAsJoystick;1562}1563}15641565static void IOS_JoystickQuit(void)1566{1567@autoreleasepool {1568#ifdef SDL_JOYSTICK_MFI1569NSNotificationCenter *center = [NSNotificationCenter defaultCenter];15701571if (connectObserver) {1572[center removeObserver:connectObserver name:GCControllerDidConnectNotification object:nil];1573connectObserver = nil;1574}15751576if (disconnectObserver) {1577[center removeObserver:disconnectObserver name:GCControllerDidDisconnectNotification object:nil];1578disconnectObserver = nil;1579}15801581#ifdef SDL_PLATFORM_TVOS1582SDL_RemoveHintCallback(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION,1583SDL_AppleTVRemoteRotationHintChanged, NULL);1584#endif // SDL_PLATFORM_TVOS1585#endif // SDL_JOYSTICK_MFI15861587while (deviceList != NULL) {1588IOS_RemoveJoystickDevice(deviceList);1589}1590}15911592numjoysticks = 0;1593}15941595static bool IOS_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)1596{1597SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);1598if (device == NULL) {1599return false;1600}16011602if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1603int axis = 0;1604for (id key in device->axes) {1605if ([(NSString *)key isEqualToString:@"Left Thumbstick X Axis"] ||1606[(NSString *)key isEqualToString:@"Direction Pad X Axis"]) {1607out->leftx.kind = EMappingKind_Axis;1608out->leftx.target = axis;1609} else if ([(NSString *)key isEqualToString:@"Left Thumbstick Y Axis"] ||1610[(NSString *)key isEqualToString:@"Direction Pad Y Axis"]) {1611out->lefty.kind = EMappingKind_Axis;1612out->lefty.target = axis;1613out->lefty.axis_reversed = true;1614} else if ([(NSString *)key isEqualToString:@"Right Thumbstick X Axis"]) {1615out->rightx.kind = EMappingKind_Axis;1616out->rightx.target = axis;1617} else if ([(NSString *)key isEqualToString:@"Right Thumbstick Y Axis"]) {1618out->righty.kind = EMappingKind_Axis;1619out->righty.target = axis;1620out->righty.axis_reversed = true;1621} else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) {1622out->lefttrigger.kind = EMappingKind_Axis;1623out->lefttrigger.target = axis;1624out->lefttrigger.half_axis_positive = true;1625} else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) {1626out->righttrigger.kind = EMappingKind_Axis;1627out->righttrigger.target = axis;1628out->righttrigger.half_axis_positive = true;1629}1630++axis;1631}16321633int button = 0;1634for (id key in device->buttons) {1635SDL_InputMapping *mapping = NULL;16361637if ([(NSString *)key isEqualToString:GCInputButtonA]) {1638if (device->is_siri_remote > 1) {1639// GCInputButtonA is triggered for any D-Pad press, ignore it in favor of "Button Center"1640} else if (device->has_nintendo_buttons) {1641mapping = &out->b;1642} else {1643mapping = &out->a;1644}1645} else if ([(NSString *)key isEqualToString:GCInputButtonB]) {1646if (device->has_nintendo_buttons) {1647mapping = &out->a;1648} else if (device->is_switch_joyconL || device->is_switch_joyconR) {1649mapping = &out->x;1650} else {1651mapping = &out->b;1652}1653} else if ([(NSString *)key isEqualToString:GCInputButtonX]) {1654if (device->has_nintendo_buttons) {1655mapping = &out->y;1656} else if (device->is_switch_joyconL || device->is_switch_joyconR) {1657mapping = &out->b;1658} else {1659mapping = &out->x;1660}1661} else if ([(NSString *)key isEqualToString:GCInputButtonY]) {1662if (device->has_nintendo_buttons) {1663mapping = &out->x;1664} else {1665mapping = &out->y;1666}1667} else if ([(NSString *)key isEqualToString:@"Direction Pad Left"]) {1668mapping = &out->dpleft;1669} else if ([(NSString *)key isEqualToString:@"Direction Pad Right"]) {1670mapping = &out->dpright;1671} else if ([(NSString *)key isEqualToString:@"Direction Pad Up"]) {1672mapping = &out->dpup;1673} else if ([(NSString *)key isEqualToString:@"Direction Pad Down"]) {1674mapping = &out->dpdown;1675} else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Left"]) {1676mapping = &out->dpleft;1677} else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Right"]) {1678mapping = &out->dpright;1679} else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Up"]) {1680mapping = &out->dpup;1681} else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Down"]) {1682mapping = &out->dpdown;1683} else if ([(NSString *)key isEqualToString:GCInputLeftShoulder]) {1684mapping = &out->leftshoulder;1685} else if ([(NSString *)key isEqualToString:GCInputRightShoulder]) {1686mapping = &out->rightshoulder;1687} else if ([(NSString *)key isEqualToString:GCInputLeftThumbstickButton]) {1688mapping = &out->leftstick;1689} else if ([(NSString *)key isEqualToString:GCInputRightThumbstickButton]) {1690mapping = &out->rightstick;1691} else if ([(NSString *)key isEqualToString:@"Button Home"]) {1692mapping = &out->guide;1693} else if ([(NSString *)key isEqualToString:GCInputButtonMenu]) {1694if (device->is_siri_remote) {1695mapping = &out->b;1696} else {1697mapping = &out->start;1698}1699} else if ([(NSString *)key isEqualToString:GCInputButtonOptions]) {1700mapping = &out->back;1701} else if ([(NSString *)key isEqualToString:@"Button Share"]) {1702mapping = &out->misc1;1703} else if ([(NSString *)key isEqualToString:GCInputXboxPaddleOne]) {1704mapping = &out->right_paddle1;1705} else if ([(NSString *)key isEqualToString:GCInputXboxPaddleTwo]) {1706mapping = &out->right_paddle2;1707} else if ([(NSString *)key isEqualToString:GCInputXboxPaddleThree]) {1708mapping = &out->left_paddle1;1709} else if ([(NSString *)key isEqualToString:GCInputXboxPaddleFour]) {1710mapping = &out->left_paddle2;1711} else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) {1712mapping = &out->lefttrigger;1713} else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) {1714mapping = &out->righttrigger;1715} else if ([(NSString *)key isEqualToString:GCInputDualShockTouchpadButton]) {1716mapping = &out->touchpad;1717} else if ([(NSString *)key isEqualToString:@"Button Center"]) {1718mapping = &out->a;1719}1720if (mapping && mapping->kind == EMappingKind_None) {1721mapping->kind = EMappingKind_Button;1722mapping->target = button;1723}1724++button;1725}17261727return true;1728}1729return false;1730}17311732#if defined(SDL_JOYSTICK_MFI) && defined(SDL_PLATFORM_MACOS)1733bool IOS_SupportedHIDDevice(IOHIDDeviceRef device)1734{1735if (!SDL_GetHintBoolean(SDL_HINT_JOYSTICK_MFI, true)) {1736return false;1737}17381739if (@available(macOS 10.16, *)) {1740const int MAX_ATTEMPTS = 3;1741for (int attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {1742if ([GCController supportsHIDDevice:device]) {1743return true;1744}17451746// The framework may not have seen the device yet1747SDL_Delay(10);1748}1749}1750return false;1751}1752#endif17531754#ifdef SDL_JOYSTICK_MFI1755/* NOLINTNEXTLINE(readability-non-const-parameter): getCString takes a non-const char* */1756static void GetAppleSFSymbolsNameForElement(GCControllerElement *element, char *name)1757{1758if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1759if (element) {1760[element.sfSymbolsName getCString:name maxLength:255 encoding:NSASCIIStringEncoding];1761}1762}1763}17641765static GCControllerDirectionPad *GetDirectionalPadForController(GCController *controller)1766{1767if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1768return controller.physicalInputProfile.dpads[GCInputDirectionPad];1769}17701771if (controller.extendedGamepad) {1772return controller.extendedGamepad.dpad;1773}17741775if (controller.microGamepad) {1776return controller.microGamepad.dpad;1777}17781779return nil;1780}1781#endif // SDL_JOYSTICK_MFI17821783const char *IOS_GetAppleSFSymbolsNameForButton(SDL_Gamepad *gamepad, SDL_GamepadButton button)1784{1785char elementName[256];1786elementName[0] = '\0';17871788#ifdef SDL_JOYSTICK_MFI1789if (gamepad && SDL_GetGamepadJoystick(gamepad)->driver == &SDL_IOS_JoystickDriver) {1790if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1791GCController *controller = SDL_GetGamepadJoystick(gamepad)->hwdata->controller;1792NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;1793switch (button) {1794case SDL_GAMEPAD_BUTTON_SOUTH:1795GetAppleSFSymbolsNameForElement(elements[GCInputButtonA], elementName);1796break;1797case SDL_GAMEPAD_BUTTON_EAST:1798GetAppleSFSymbolsNameForElement(elements[GCInputButtonB], elementName);1799break;1800case SDL_GAMEPAD_BUTTON_WEST:1801GetAppleSFSymbolsNameForElement(elements[GCInputButtonX], elementName);1802break;1803case SDL_GAMEPAD_BUTTON_NORTH:1804GetAppleSFSymbolsNameForElement(elements[GCInputButtonY], elementName);1805break;1806case SDL_GAMEPAD_BUTTON_BACK:1807GetAppleSFSymbolsNameForElement(elements[GCInputButtonOptions], elementName);1808break;1809case SDL_GAMEPAD_BUTTON_GUIDE:1810GetAppleSFSymbolsNameForElement(elements[@"Button Home"], elementName);1811break;1812case SDL_GAMEPAD_BUTTON_START:1813GetAppleSFSymbolsNameForElement(elements[GCInputButtonMenu], elementName);1814break;1815case SDL_GAMEPAD_BUTTON_LEFT_STICK:1816GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstickButton], elementName);1817break;1818case SDL_GAMEPAD_BUTTON_RIGHT_STICK:1819GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstickButton], elementName);1820break;1821case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:1822GetAppleSFSymbolsNameForElement(elements[GCInputLeftShoulder], elementName);1823break;1824case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:1825GetAppleSFSymbolsNameForElement(elements[GCInputRightShoulder], elementName);1826break;1827case SDL_GAMEPAD_BUTTON_DPAD_UP:1828{1829GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);1830if (dpad) {1831GetAppleSFSymbolsNameForElement(dpad.up, elementName);1832if (SDL_strlen(elementName) == 0) {1833SDL_strlcpy(elementName, "dpad.up.fill", sizeof(elementName));1834}1835}1836break;1837}1838case SDL_GAMEPAD_BUTTON_DPAD_DOWN:1839{1840GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);1841if (dpad) {1842GetAppleSFSymbolsNameForElement(dpad.down, elementName);1843if (SDL_strlen(elementName) == 0) {1844SDL_strlcpy(elementName, "dpad.down.fill", sizeof(elementName));1845}1846}1847break;1848}1849case SDL_GAMEPAD_BUTTON_DPAD_LEFT:1850{1851GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);1852if (dpad) {1853GetAppleSFSymbolsNameForElement(dpad.left, elementName);1854if (SDL_strlen(elementName) == 0) {1855SDL_strlcpy(elementName, "dpad.left.fill", sizeof(elementName));1856}1857}1858break;1859}1860case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:1861{1862GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);1863if (dpad) {1864GetAppleSFSymbolsNameForElement(dpad.right, elementName);1865if (SDL_strlen(elementName) == 0) {1866SDL_strlcpy(elementName, "dpad.right.fill", sizeof(elementName));1867}1868}1869break;1870}1871case SDL_GAMEPAD_BUTTON_MISC1:1872GetAppleSFSymbolsNameForElement(elements[GCInputDualShockTouchpadButton], elementName);1873break;1874case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1:1875GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleOne], elementName);1876break;1877case SDL_GAMEPAD_BUTTON_LEFT_PADDLE1:1878GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleThree], elementName);1879break;1880case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2:1881GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleTwo], elementName);1882break;1883case SDL_GAMEPAD_BUTTON_LEFT_PADDLE2:1884GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleFour], elementName);1885break;1886case SDL_GAMEPAD_BUTTON_TOUCHPAD:1887GetAppleSFSymbolsNameForElement(elements[GCInputDualShockTouchpadButton], elementName);1888break;1889default:1890break;1891}1892}1893}1894#endif // SDL_JOYSTICK_MFI18951896return *elementName ? SDL_GetPersistentString(elementName) : NULL;1897}18981899const char *IOS_GetAppleSFSymbolsNameForAxis(SDL_Gamepad *gamepad, SDL_GamepadAxis axis)1900{1901char elementName[256];1902elementName[0] = '\0';19031904#ifdef SDL_JOYSTICK_MFI1905if (gamepad && SDL_GetGamepadJoystick(gamepad)->driver == &SDL_IOS_JoystickDriver) {1906if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {1907GCController *controller = SDL_GetGamepadJoystick(gamepad)->hwdata->controller;1908NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;1909switch (axis) {1910case SDL_GAMEPAD_AXIS_LEFTX:1911GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstick], elementName);1912break;1913case SDL_GAMEPAD_AXIS_LEFTY:1914GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstick], elementName);1915break;1916case SDL_GAMEPAD_AXIS_RIGHTX:1917GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstick], elementName);1918break;1919case SDL_GAMEPAD_AXIS_RIGHTY:1920GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstick], elementName);1921break;1922case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:1923GetAppleSFSymbolsNameForElement(elements[GCInputLeftTrigger], elementName);1924break;1925case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:1926GetAppleSFSymbolsNameForElement(elements[GCInputRightTrigger], elementName);1927break;1928default:1929break;1930}1931}1932}1933#endif // SDL_JOYSTICK_MFI19341935return *elementName ? SDL_GetPersistentString(elementName) : NULL;1936}19371938SDL_JoystickDriver SDL_IOS_JoystickDriver = {1939IOS_JoystickInit,1940IOS_JoystickGetCount,1941IOS_JoystickDetect,1942IOS_JoystickIsDevicePresent,1943IOS_JoystickGetDeviceName,1944IOS_JoystickGetDevicePath,1945IOS_JoystickGetDeviceSteamVirtualGamepadSlot,1946IOS_JoystickGetDevicePlayerIndex,1947IOS_JoystickSetDevicePlayerIndex,1948IOS_JoystickGetDeviceGUID,1949IOS_JoystickGetDeviceInstanceID,1950IOS_JoystickOpen,1951IOS_JoystickRumble,1952IOS_JoystickRumbleTriggers,1953IOS_JoystickSetLED,1954IOS_JoystickSendEffect,1955IOS_JoystickSetSensorsEnabled,1956IOS_JoystickUpdate,1957IOS_JoystickClose,1958IOS_JoystickQuit,1959IOS_JoystickGetGamepadMapping1960};196119621963