Path: blob/master/thirdparty/sdl/joystick/darwin/SDL_iokitjoystick.c
9904 views
/*1Simple DirectMedia Layer2Copyright (C) 1997-2025 Sam Lantinga <[email protected]>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#ifdef SDL_JOYSTICK_IOKIT2324#include "../SDL_sysjoystick.h"25#include "../SDL_joystick_c.h"26#include "SDL_iokitjoystick_c.h"27#include "../hidapi/SDL_hidapijoystick_c.h"28#include "../../haptic/darwin/SDL_syshaptic_c.h" // For haptic hot plugging2930#define SDL_JOYSTICK_RUNLOOP_MODE CFSTR("SDLJoystick")3132#define CONVERT_MAGNITUDE(x) (((x)*10000) / 0x7FFF)3334// The base object of the HID Manager API35static IOHIDManagerRef hidman = NULL;3637// Linked list of all available devices38static recDevice *gpDeviceList = NULL;3940void FreeRumbleEffectData(FFEFFECT *effect)41{42if (!effect) {43return;44}45SDL_free(effect->rgdwAxes);46SDL_free(effect->rglDirection);47SDL_free(effect->lpvTypeSpecificParams);48SDL_free(effect);49}5051FFEFFECT *CreateRumbleEffectData(Sint16 magnitude)52{53FFEFFECT *effect;54FFPERIODIC *periodic;5556// Create the effect57effect = (FFEFFECT *)SDL_calloc(1, sizeof(*effect));58if (!effect) {59return NULL;60}61effect->dwSize = sizeof(*effect);62effect->dwGain = 10000;63effect->dwFlags = FFEFF_OBJECTOFFSETS;64effect->dwDuration = SDL_MAX_RUMBLE_DURATION_MS * 1000; // In microseconds.65effect->dwTriggerButton = FFEB_NOTRIGGER;6667effect->cAxes = 2;68effect->rgdwAxes = (DWORD *)SDL_calloc(effect->cAxes, sizeof(DWORD));69if (!effect->rgdwAxes) {70FreeRumbleEffectData(effect);71return NULL;72}7374effect->rglDirection = (LONG *)SDL_calloc(effect->cAxes, sizeof(LONG));75if (!effect->rglDirection) {76FreeRumbleEffectData(effect);77return NULL;78}79effect->dwFlags |= FFEFF_CARTESIAN;8081periodic = (FFPERIODIC *)SDL_calloc(1, sizeof(*periodic));82if (!periodic) {83FreeRumbleEffectData(effect);84return NULL;85}86periodic->dwMagnitude = CONVERT_MAGNITUDE(magnitude);87periodic->dwPeriod = 1000000;8889effect->cbTypeSpecificParams = sizeof(*periodic);90effect->lpvTypeSpecificParams = periodic;9192return effect;93}9495static recDevice *GetDeviceForIndex(int device_index)96{97recDevice *device = gpDeviceList;98while (device) {99if (!device->removed) {100if (device_index == 0) {101break;102}103104--device_index;105}106device = device->pNext;107}108return device;109}110111static void FreeElementList(recElement *pElement)112{113while (pElement) {114recElement *pElementNext = pElement->pNext;115SDL_free(pElement);116pElement = pElementNext;117}118}119120static recDevice *FreeDevice(recDevice *removeDevice)121{122recDevice *pDeviceNext = NULL;123if (removeDevice) {124if (removeDevice->deviceRef) {125if (removeDevice->runLoopAttached) {126/* Calling IOHIDDeviceUnscheduleFromRunLoop without a prior,127* paired call to IOHIDDeviceScheduleWithRunLoop can lead128* to crashes in MacOS 10.14.x and earlier. This doesn't129* appear to be a problem in MacOS 10.15.x, but we'll130* do it anyways. (Part-of fix for Bug 5034)131*/132IOHIDDeviceUnscheduleFromRunLoop(removeDevice->deviceRef, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE);133}134CFRelease(removeDevice->deviceRef);135removeDevice->deviceRef = NULL;136}137138/* clear out any reference to removeDevice from an associated,139* live instance of SDL_Joystick (Part-of fix for Bug 5034)140*/141SDL_LockJoysticks();142if (removeDevice->joystick) {143removeDevice->joystick->hwdata = NULL;144}145SDL_UnlockJoysticks();146147// save next device prior to disposing of this device148pDeviceNext = removeDevice->pNext;149150if (gpDeviceList == removeDevice) {151gpDeviceList = pDeviceNext;152} else if (gpDeviceList) {153recDevice *device;154155for (device = gpDeviceList; device; device = device->pNext) {156if (device->pNext == removeDevice) {157device->pNext = pDeviceNext;158break;159}160}161}162removeDevice->pNext = NULL;163164// free element lists165FreeElementList(removeDevice->firstAxis);166FreeElementList(removeDevice->firstButton);167FreeElementList(removeDevice->firstHat);168169SDL_free(removeDevice);170}171return pDeviceNext;172}173174static bool GetHIDElementState(recDevice *pDevice, recElement *pElement, SInt32 *pValue)175{176SInt32 value = 0;177bool result = false;178179if (pDevice && pDevice->deviceRef && pElement) {180IOHIDValueRef valueRef;181if (IOHIDDeviceGetValue(pDevice->deviceRef, pElement->elementRef, &valueRef) == kIOReturnSuccess) {182value = (SInt32)IOHIDValueGetIntegerValue(valueRef);183184// record min and max for auto calibration185if (value < pElement->minReport) {186pElement->minReport = value;187}188if (value > pElement->maxReport) {189pElement->maxReport = value;190}191*pValue = value;192193result = true;194}195}196return result;197}198199static bool GetHIDScaledCalibratedState(recDevice *pDevice, recElement *pElement, SInt32 min, SInt32 max, SInt32 *pValue)200{201const float deviceScale = max - min;202const float readScale = pElement->maxReport - pElement->minReport;203bool result = false;204if (GetHIDElementState(pDevice, pElement, pValue)) {205if (readScale == 0) {206result = true; // no scaling at all207} else {208*pValue = (Sint32)(((*pValue - pElement->minReport) * deviceScale / readScale) + min);209result = true;210}211}212return result;213}214215static void JoystickDeviceWasRemovedCallback(void *ctx, IOReturn result, void *sender)216{217recDevice *device = (recDevice *)ctx;218device->removed = true;219if (device->deviceRef) {220// deviceRef was invalidated due to the remove221CFRelease(device->deviceRef);222device->deviceRef = NULL;223}224if (device->ffeffect_ref) {225FFDeviceReleaseEffect(device->ffdevice, device->ffeffect_ref);226device->ffeffect_ref = NULL;227}228if (device->ffeffect) {229FreeRumbleEffectData(device->ffeffect);230device->ffeffect = NULL;231}232if (device->ffdevice) {233FFReleaseDevice(device->ffdevice);234device->ffdevice = NULL;235device->ff_initialized = false;236}237#ifdef SDL_HAPTIC_IOKIT238MacHaptic_MaybeRemoveDevice(device->ffservice);239#endif240241SDL_PrivateJoystickRemoved(device->instance_id);242}243244static void AddHIDElement(const void *value, void *parameter);245246// Call AddHIDElement() on all elements in an array of IOHIDElementRefs247static void AddHIDElements(CFArrayRef array, recDevice *pDevice)248{249const CFRange range = { 0, CFArrayGetCount(array) };250CFArrayApplyFunction(array, range, AddHIDElement, pDevice);251}252253static bool ElementAlreadyAdded(const IOHIDElementCookie cookie, const recElement *listitem)254{255while (listitem) {256if (listitem->cookie == cookie) {257return true;258}259listitem = listitem->pNext;260}261return false;262}263264// See if we care about this HID element, and if so, note it in our recDevice.265static void AddHIDElement(const void *value, void *parameter)266{267recDevice *pDevice = (recDevice *)parameter;268IOHIDElementRef refElement = (IOHIDElementRef)value;269const CFTypeID elementTypeID = refElement ? CFGetTypeID(refElement) : 0;270271if (refElement && (elementTypeID == IOHIDElementGetTypeID())) {272const IOHIDElementCookie cookie = IOHIDElementGetCookie(refElement);273const uint32_t usagePage = IOHIDElementGetUsagePage(refElement);274const uint32_t usage = IOHIDElementGetUsage(refElement);275recElement *element = NULL;276recElement **headElement = NULL;277278// look at types of interest279switch (IOHIDElementGetType(refElement)) {280case kIOHIDElementTypeInput_Misc:281case kIOHIDElementTypeInput_Button:282case kIOHIDElementTypeInput_Axis:283{284switch (usagePage) { // only interested in kHIDPage_GenericDesktop and kHIDPage_Button285case kHIDPage_GenericDesktop:286switch (usage) {287case kHIDUsage_GD_X:288case kHIDUsage_GD_Y:289case kHIDUsage_GD_Z:290case kHIDUsage_GD_Rx:291case kHIDUsage_GD_Ry:292case kHIDUsage_GD_Rz:293case kHIDUsage_GD_Slider:294case kHIDUsage_GD_Dial:295case kHIDUsage_GD_Wheel:296if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) {297element = (recElement *)SDL_calloc(1, sizeof(recElement));298if (element) {299pDevice->axes++;300headElement = &(pDevice->firstAxis);301}302}303break;304305case kHIDUsage_GD_Hatswitch:306if (!ElementAlreadyAdded(cookie, pDevice->firstHat)) {307element = (recElement *)SDL_calloc(1, sizeof(recElement));308if (element) {309pDevice->hats++;310headElement = &(pDevice->firstHat);311}312}313break;314case kHIDUsage_GD_DPadUp:315case kHIDUsage_GD_DPadDown:316case kHIDUsage_GD_DPadRight:317case kHIDUsage_GD_DPadLeft:318case kHIDUsage_GD_Start:319case kHIDUsage_GD_Select:320case kHIDUsage_GD_SystemMainMenu:321if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) {322element = (recElement *)SDL_calloc(1, sizeof(recElement));323if (element) {324pDevice->buttons++;325headElement = &(pDevice->firstButton);326}327}328break;329}330break;331332case kHIDPage_Simulation:333switch (usage) {334case kHIDUsage_Sim_Rudder:335case kHIDUsage_Sim_Throttle:336case kHIDUsage_Sim_Accelerator:337case kHIDUsage_Sim_Brake:338if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) {339element = (recElement *)SDL_calloc(1, sizeof(recElement));340if (element) {341pDevice->axes++;342headElement = &(pDevice->firstAxis);343}344}345break;346347default:348break;349}350break;351352case kHIDPage_Button:353case kHIDPage_Consumer: // e.g. 'pause' button on Steelseries MFi gamepads.354if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) {355element = (recElement *)SDL_calloc(1, sizeof(recElement));356if (element) {357pDevice->buttons++;358headElement = &(pDevice->firstButton);359}360}361break;362363default:364break;365}366} break;367368case kIOHIDElementTypeCollection:369{370CFArrayRef array = IOHIDElementGetChildren(refElement);371if (array) {372AddHIDElements(array, pDevice);373}374} break;375376default:377break;378}379380if (element && headElement) { // add to list381recElement *elementPrevious = NULL;382recElement *elementCurrent = *headElement;383while (elementCurrent && usage >= elementCurrent->usage) {384elementPrevious = elementCurrent;385elementCurrent = elementCurrent->pNext;386}387if (elementPrevious) {388elementPrevious->pNext = element;389} else {390*headElement = element;391}392393element->elementRef = refElement;394element->usagePage = usagePage;395element->usage = usage;396element->pNext = elementCurrent;397398element->minReport = element->min = (SInt32)IOHIDElementGetLogicalMin(refElement);399element->maxReport = element->max = (SInt32)IOHIDElementGetLogicalMax(refElement);400element->cookie = IOHIDElementGetCookie(refElement);401402pDevice->elements++;403}404}405}406407static int GetSteamVirtualGamepadSlot(Uint16 vendor_id, Uint16 product_id, const char *product_string)408{409int slot = -1;410411if (vendor_id == USB_VENDOR_MICROSOFT && product_id == USB_PRODUCT_XBOX360_WIRED_CONTROLLER) {412// Gamepad name is "GamePad-N", where N is slot + 1413if (SDL_sscanf(product_string, "GamePad-%d", &slot) == 1) {414slot -= 1;415}416}417return slot;418}419420static bool GetDeviceInfo(IOHIDDeviceRef hidDevice, recDevice *pDevice)421{422Sint32 vendor = 0;423Sint32 product = 0;424Sint32 version = 0;425char *name;426char manufacturer_string[256];427char product_string[256];428CFTypeRef refCF = NULL;429CFArrayRef array = NULL;430431// get usage page and usage432refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsagePageKey));433if (refCF) {434CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usagePage);435}436if (pDevice->usagePage != kHIDPage_GenericDesktop) {437return false; // Filter device list to non-keyboard/mouse stuff438}439440refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsageKey));441if (refCF) {442CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usage);443}444445if ((pDevice->usage != kHIDUsage_GD_Joystick &&446pDevice->usage != kHIDUsage_GD_GamePad &&447pDevice->usage != kHIDUsage_GD_MultiAxisController)) {448return false; // Filter device list to non-keyboard/mouse stuff449}450451/* Make sure we retain the use of the IOKit-provided device-object,452lest the device get disconnected and we try to use it. (Fixes453SDL-Bugzilla #4961, aka. https://bugzilla.libsdl.org/show_bug.cgi?id=4961 )454*/455CFRetain(hidDevice);456457/* Now that we've CFRetain'ed the device-object (for our use), we'll458save the reference to it.459*/460pDevice->deviceRef = hidDevice;461462refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey));463if (refCF) {464CFNumberGetValue(refCF, kCFNumberSInt32Type, &vendor);465}466467refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey));468if (refCF) {469CFNumberGetValue(refCF, kCFNumberSInt32Type, &product);470}471472refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVersionNumberKey));473if (refCF) {474CFNumberGetValue(refCF, kCFNumberSInt32Type, &version);475}476477if (SDL_IsJoystickXboxOne(vendor, product)) {478// We can't actually use this API for Xbox controllers479return false;480}481482// get device name483refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDManufacturerKey));484if ((!refCF) || (!CFStringGetCString(refCF, manufacturer_string, sizeof(manufacturer_string), kCFStringEncodingUTF8))) {485manufacturer_string[0] = '\0';486}487refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey));488if ((!refCF) || (!CFStringGetCString(refCF, product_string, sizeof(product_string), kCFStringEncodingUTF8))) {489product_string[0] = '\0';490}491name = SDL_CreateJoystickName(vendor, product, manufacturer_string, product_string);492if (name) {493SDL_strlcpy(pDevice->product, name, sizeof(pDevice->product));494SDL_free(name);495}496497if (SDL_ShouldIgnoreJoystick(vendor, product, version, pDevice->product)) {498return false;499}500501if (SDL_JoystickHandledByAnotherDriver(&SDL_DARWIN_JoystickDriver, vendor, product, version, pDevice->product)) {502return false;503}504505pDevice->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_USB, (Uint16)vendor, (Uint16)product, (Uint16)version, manufacturer_string, product_string, 0, 0);506pDevice->steam_virtual_gamepad_slot = GetSteamVirtualGamepadSlot((Uint16)vendor, (Uint16)product, product_string);507508array = IOHIDDeviceCopyMatchingElements(hidDevice, NULL, kIOHIDOptionsTypeNone);509if (array) {510AddHIDElements(array, pDevice);511CFRelease(array);512}513514return true;515}516517static bool JoystickAlreadyKnown(IOHIDDeviceRef ioHIDDeviceObject)518{519recDevice *i;520521#ifdef SDL_JOYSTICK_MFI522extern bool IOS_SupportedHIDDevice(IOHIDDeviceRef device);523if (IOS_SupportedHIDDevice(ioHIDDeviceObject)) {524return true;525}526#endif527528for (i = gpDeviceList; i; i = i->pNext) {529if (i->deviceRef == ioHIDDeviceObject) {530return true;531}532}533return false;534}535536static void JoystickDeviceWasAddedCallback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject)537{538recDevice *device;539io_service_t ioservice;540541if (res != kIOReturnSuccess) {542return;543}544545if (JoystickAlreadyKnown(ioHIDDeviceObject)) {546return; // IOKit sent us a duplicate.547}548549device = (recDevice *)SDL_calloc(1, sizeof(recDevice));550if (!device) {551return;552}553554if (!GetDeviceInfo(ioHIDDeviceObject, device)) {555FreeDevice(device);556return; // not a device we care about, probably.557}558559// Get notified when this device is disconnected.560IOHIDDeviceRegisterRemovalCallback(ioHIDDeviceObject, JoystickDeviceWasRemovedCallback, device);561IOHIDDeviceScheduleWithRunLoop(ioHIDDeviceObject, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE);562device->runLoopAttached = true;563564// Allocate an instance ID for this device565device->instance_id = SDL_GetNextObjectID();566567// We have to do some storage of the io_service_t for SDL_OpenHapticFromJoystick568ioservice = IOHIDDeviceGetService(ioHIDDeviceObject);569if ((ioservice) && (FFIsForceFeedback(ioservice) == FF_OK)) {570device->ffservice = ioservice;571#ifdef SDL_HAPTIC_IOKIT572MacHaptic_MaybeAddDevice(ioservice);573#endif574}575576// Add device to the end of the list577if (!gpDeviceList) {578gpDeviceList = device;579} else {580recDevice *curdevice;581582curdevice = gpDeviceList;583while (curdevice->pNext) {584curdevice = curdevice->pNext;585}586curdevice->pNext = device;587}588589SDL_PrivateJoystickAdded(device->instance_id);590}591592static bool ConfigHIDManager(CFArrayRef matchingArray)593{594CFRunLoopRef runloop = CFRunLoopGetCurrent();595596if (IOHIDManagerOpen(hidman, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {597return false;598}599600IOHIDManagerSetDeviceMatchingMultiple(hidman, matchingArray);601IOHIDManagerRegisterDeviceMatchingCallback(hidman, JoystickDeviceWasAddedCallback, NULL);602IOHIDManagerScheduleWithRunLoop(hidman, runloop, SDL_JOYSTICK_RUNLOOP_MODE);603604while (CFRunLoopRunInMode(SDL_JOYSTICK_RUNLOOP_MODE, 0, TRUE) == kCFRunLoopRunHandledSource) {605// no-op. Callback fires once per existing device.606}607608// future hotplug events will come through SDL_JOYSTICK_RUNLOOP_MODE now.609610return true; // good to go.611}612613static CFDictionaryRef CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage, int *okay)614{615CFDictionaryRef result = NULL;616CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);617CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);618const void *keys[2] = { (void *)CFSTR(kIOHIDDeviceUsagePageKey), (void *)CFSTR(kIOHIDDeviceUsageKey) };619const void *vals[2] = { (void *)pageNumRef, (void *)usageNumRef };620621if (pageNumRef && usageNumRef) {622result = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);623}624625if (pageNumRef) {626CFRelease(pageNumRef);627}628if (usageNumRef) {629CFRelease(usageNumRef);630}631632if (!result) {633*okay = 0;634}635636return result;637}638639static bool CreateHIDManager(void)640{641bool result = false;642int okay = 1;643const void *vals[] = {644(void *)CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay),645(void *)CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay),646(void *)CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay),647};648const size_t numElements = SDL_arraysize(vals);649CFArrayRef array = okay ? CFArrayCreate(kCFAllocatorDefault, vals, numElements, &kCFTypeArrayCallBacks) : NULL;650size_t i;651652for (i = 0; i < numElements; i++) {653if (vals[i]) {654CFRelease((CFTypeRef)vals[i]);655}656}657658if (array) {659hidman = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);660if (hidman != NULL) {661result = ConfigHIDManager(array);662}663CFRelease(array);664}665666return result;667}668669static bool DARWIN_JoystickInit(void)670{671if (!SDL_GetHintBoolean(SDL_HINT_JOYSTICK_IOKIT, true)) {672return true;673}674675if (!CreateHIDManager()) {676return SDL_SetError("Joystick: Couldn't initialize HID Manager");677}678679return true;680}681682static int DARWIN_JoystickGetCount(void)683{684recDevice *device = gpDeviceList;685int nJoySticks = 0;686687while (device) {688if (!device->removed) {689nJoySticks++;690}691device = device->pNext;692}693694return nJoySticks;695}696697static void DARWIN_JoystickDetect(void)698{699recDevice *device = gpDeviceList;700while (device) {701if (device->removed) {702device = FreeDevice(device);703} else {704device = device->pNext;705}706}707708if (hidman) {709/* run this after the checks above so we don't set device->removed and delete the device before710DARWIN_JoystickUpdate can run to clean up the SDL_Joystick object that owns this device */711while (CFRunLoopRunInMode(SDL_JOYSTICK_RUNLOOP_MODE, 0, TRUE) == kCFRunLoopRunHandledSource) {712// no-op. Pending callbacks will fire in CFRunLoopRunInMode().713}714}715}716717static bool DARWIN_JoystickIsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name)718{719// We don't override any other drivers720return false;721}722723static const char *DARWIN_JoystickGetDeviceName(int device_index)724{725recDevice *device = GetDeviceForIndex(device_index);726return device ? device->product : "UNKNOWN";727}728729static const char *DARWIN_JoystickGetDevicePath(int device_index)730{731return NULL;732}733734static int DARWIN_JoystickGetDeviceSteamVirtualGamepadSlot(int device_index)735{736recDevice *device = GetDeviceForIndex(device_index);737return device ? device->steam_virtual_gamepad_slot : -1;738}739740static int DARWIN_JoystickGetDevicePlayerIndex(int device_index)741{742return -1;743}744745static void DARWIN_JoystickSetDevicePlayerIndex(int device_index, int player_index)746{747}748749static SDL_GUID DARWIN_JoystickGetDeviceGUID(int device_index)750{751recDevice *device = GetDeviceForIndex(device_index);752SDL_GUID guid;753if (device) {754guid = device->guid;755} else {756SDL_zero(guid);757}758return guid;759}760761static SDL_JoystickID DARWIN_JoystickGetDeviceInstanceID(int device_index)762{763recDevice *device = GetDeviceForIndex(device_index);764return device ? device->instance_id : 0;765}766767static bool DARWIN_JoystickOpen(SDL_Joystick *joystick, int device_index)768{769recDevice *device = GetDeviceForIndex(device_index);770771joystick->hwdata = device;772device->joystick = joystick;773joystick->name = device->product;774775joystick->naxes = device->axes;776joystick->nhats = device->hats;777joystick->nbuttons = device->buttons;778779if (device->ffservice) {780SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, true);781}782783return true;784}785786/*787* Like strerror but for force feedback errors.788*/789static const char *FFStrError(unsigned int err)790{791switch (err) {792case FFERR_DEVICEFULL:793return "device full";794// This should be valid, but for some reason isn't defined...795/* case FFERR_DEVICENOTREG:796return "device not registered"; */797case FFERR_DEVICEPAUSED:798return "device paused";799case FFERR_DEVICERELEASED:800return "device released";801case FFERR_EFFECTPLAYING:802return "effect playing";803case FFERR_EFFECTTYPEMISMATCH:804return "effect type mismatch";805case FFERR_EFFECTTYPENOTSUPPORTED:806return "effect type not supported";807case FFERR_GENERIC:808return "undetermined error";809case FFERR_HASEFFECTS:810return "device has effects";811case FFERR_INCOMPLETEEFFECT:812return "incomplete effect";813case FFERR_INTERNAL:814return "internal fault";815case FFERR_INVALIDDOWNLOADID:816return "invalid download id";817case FFERR_INVALIDPARAM:818return "invalid parameter";819case FFERR_MOREDATA:820return "more data";821case FFERR_NOINTERFACE:822return "interface not supported";823case FFERR_NOTDOWNLOADED:824return "effect is not downloaded";825case FFERR_NOTINITIALIZED:826return "object has not been initialized";827case FFERR_OUTOFMEMORY:828return "out of memory";829case FFERR_UNPLUGGED:830return "device is unplugged";831case FFERR_UNSUPPORTED:832return "function call unsupported";833case FFERR_UNSUPPORTEDAXIS:834return "axis unsupported";835836default:837return "unknown error";838}839}840841static bool DARWIN_JoystickInitRumble(recDevice *device, Sint16 magnitude)842{843HRESULT result;844845if (!device->ffdevice) {846result = FFCreateDevice(device->ffservice, &device->ffdevice);847if (result != FF_OK) {848return SDL_SetError("Unable to create force feedback device from service: %s", FFStrError(result));849}850}851852// Reset and then enable actuators853result = FFDeviceSendForceFeedbackCommand(device->ffdevice, FFSFFC_RESET);854if (result != FF_OK) {855return SDL_SetError("Unable to reset force feedback device: %s", FFStrError(result));856}857858result = FFDeviceSendForceFeedbackCommand(device->ffdevice, FFSFFC_SETACTUATORSON);859if (result != FF_OK) {860return SDL_SetError("Unable to enable force feedback actuators: %s", FFStrError(result));861}862863// Create the effect864device->ffeffect = CreateRumbleEffectData(magnitude);865if (!device->ffeffect) {866return false;867}868869result = FFDeviceCreateEffect(device->ffdevice, kFFEffectType_Sine_ID,870device->ffeffect, &device->ffeffect_ref);871if (result != FF_OK) {872return SDL_SetError("Haptic: Unable to create effect: %s", FFStrError(result));873}874return true;875}876877static bool DARWIN_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)878{879HRESULT result;880recDevice *device = joystick->hwdata;881882// Scale and average the two rumble strengths883Sint16 magnitude = (Sint16)(((low_frequency_rumble / 2) + (high_frequency_rumble / 2)) / 2);884885if (!device) {886return SDL_SetError("Rumble failed, device disconnected");887}888889if (!device->ffservice) {890return SDL_Unsupported();891}892893if (device->ff_initialized) {894FFPERIODIC *periodic = ((FFPERIODIC *)device->ffeffect->lpvTypeSpecificParams);895periodic->dwMagnitude = CONVERT_MAGNITUDE(magnitude);896897result = FFEffectSetParameters(device->ffeffect_ref, device->ffeffect,898(FFEP_DURATION | FFEP_TYPESPECIFICPARAMS));899if (result != FF_OK) {900return SDL_SetError("Unable to update rumble effect: %s", FFStrError(result));901}902} else {903if (!DARWIN_JoystickInitRumble(device, magnitude)) {904return false;905}906device->ff_initialized = true;907}908909result = FFEffectStart(device->ffeffect_ref, 1, 0);910if (result != FF_OK) {911return SDL_SetError("Unable to run the rumble effect: %s", FFStrError(result));912}913return true;914}915916static bool DARWIN_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)917{918return SDL_Unsupported();919}920921static bool DARWIN_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)922{923return SDL_Unsupported();924}925926static bool DARWIN_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size)927{928return SDL_Unsupported();929}930931static bool DARWIN_JoystickSetSensorsEnabled(SDL_Joystick *joystick, bool enabled)932{933return SDL_Unsupported();934}935936static void DARWIN_JoystickUpdate(SDL_Joystick *joystick)937{938recDevice *device = joystick->hwdata;939recElement *element;940SInt32 value, range;941int i, goodRead = false;942Uint64 timestamp = SDL_GetTicksNS();943944if (!device) {945return;946}947948if (device->removed) { // device was unplugged; ignore it.949if (joystick->hwdata) {950joystick->hwdata = NULL;951}952return;953}954955element = device->firstAxis;956i = 0;957958while (element) {959goodRead = GetHIDScaledCalibratedState(device, element, -32768, 32767, &value);960if (goodRead) {961SDL_SendJoystickAxis(timestamp, joystick, i, value);962}963964element = element->pNext;965++i;966}967968element = device->firstButton;969i = 0;970while (element) {971goodRead = GetHIDElementState(device, element, &value);972if (goodRead) {973SDL_SendJoystickButton(timestamp, joystick, i, (value != 0));974}975976element = element->pNext;977++i;978}979980element = device->firstHat;981i = 0;982983while (element) {984Uint8 pos = 0;985986range = (element->max - element->min + 1);987goodRead = GetHIDElementState(device, element, &value);988if (goodRead) {989value -= element->min;990if (range == 4) { // 4 position hatswitch - scale up value991value *= 2;992} else if (range != 8) { // Neither a 4 nor 8 positions - fall back to default position (centered)993value = -1;994}995switch (value) {996case 0:997pos = SDL_HAT_UP;998break;999case 1:1000pos = SDL_HAT_RIGHTUP;1001break;1002case 2:1003pos = SDL_HAT_RIGHT;1004break;1005case 3:1006pos = SDL_HAT_RIGHTDOWN;1007break;1008case 4:1009pos = SDL_HAT_DOWN;1010break;1011case 5:1012pos = SDL_HAT_LEFTDOWN;1013break;1014case 6:1015pos = SDL_HAT_LEFT;1016break;1017case 7:1018pos = SDL_HAT_LEFTUP;1019break;1020default:1021/* Every other value is mapped to center. We do that because some1022* joysticks use 8 and some 15 for this value, and apparently1023* there are even more variants out there - so we try to be generous.1024*/1025pos = SDL_HAT_CENTERED;1026break;1027}10281029SDL_SendJoystickHat(timestamp, joystick, i, pos);1030}10311032element = element->pNext;1033++i;1034}1035}10361037static void DARWIN_JoystickClose(SDL_Joystick *joystick)1038{1039recDevice *device = joystick->hwdata;1040if (device) {1041device->joystick = NULL;1042}1043}10441045static void DARWIN_JoystickQuit(void)1046{1047while (FreeDevice(gpDeviceList)) {1048// spin1049}10501051if (hidman) {1052IOHIDManagerUnscheduleFromRunLoop(hidman, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE);1053IOHIDManagerClose(hidman, kIOHIDOptionsTypeNone);1054CFRelease(hidman);1055hidman = NULL;1056}1057}10581059static bool DARWIN_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)1060{1061return false;1062}10631064SDL_JoystickDriver SDL_DARWIN_JoystickDriver = {1065DARWIN_JoystickInit,1066DARWIN_JoystickGetCount,1067DARWIN_JoystickDetect,1068DARWIN_JoystickIsDevicePresent,1069DARWIN_JoystickGetDeviceName,1070DARWIN_JoystickGetDevicePath,1071DARWIN_JoystickGetDeviceSteamVirtualGamepadSlot,1072DARWIN_JoystickGetDevicePlayerIndex,1073DARWIN_JoystickSetDevicePlayerIndex,1074DARWIN_JoystickGetDeviceGUID,1075DARWIN_JoystickGetDeviceInstanceID,1076DARWIN_JoystickOpen,1077DARWIN_JoystickRumble,1078DARWIN_JoystickRumbleTriggers,1079DARWIN_JoystickSetLED,1080DARWIN_JoystickSendEffect,1081DARWIN_JoystickSetSensorsEnabled,1082DARWIN_JoystickUpdate,1083DARWIN_JoystickClose,1084DARWIN_JoystickQuit,1085DARWIN_JoystickGetGamepadMapping1086};10871088#endif // SDL_JOYSTICK_IOKIT108910901091