2 Simple DirectMedia Layer
3 Copyright (C) 1997-2018 Sam Lantinga <slouken@libsdl.org>
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
21 #include "../../SDL_internal.h"
23 #ifdef SDL_JOYSTICK_IOKIT
25 #include <IOKit/hid/IOHIDLib.h>
27 /* For force feedback testing. */
28 #include <ForceFeedback/ForceFeedback.h>
29 #include <ForceFeedback/ForceFeedbackConstants.h>
31 #include "SDL_joystick.h"
32 #include "../SDL_sysjoystick.h"
33 #include "../SDL_joystick_c.h"
34 #include "SDL_sysjoystick_c.h"
35 #include "SDL_events.h"
36 #include "../../haptic/darwin/SDL_syshaptic_c.h" /* For haptic hot plugging */
38 #define SDL_JOYSTICK_RUNLOOP_MODE CFSTR("SDLJoystick")
40 /* The base object of the HID Manager API */
41 static IOHIDManagerRef hidman = NULL;
43 /* Linked list of all available devices */
44 static recDevice *gpDeviceList = NULL;
46 /* static incrementing counter for new joystick devices seen on the system. Devices should start with index 0 */
47 static int s_joystick_instance_id = -1;
49 static recDevice *GetDeviceForIndex(int device_index)
51 recDevice *device = gpDeviceList;
53 if (!device->removed) {
54 if (device_index == 0)
59 device = device->pNext;
65 FreeElementList(recElement *pElement)
68 recElement *pElementNext = pElement->pNext;
70 pElement = pElementNext;
75 FreeDevice(recDevice *removeDevice)
77 recDevice *pDeviceNext = NULL;
79 if (removeDevice->deviceRef) {
80 IOHIDDeviceUnscheduleFromRunLoop(removeDevice->deviceRef, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE);
81 removeDevice->deviceRef = NULL;
84 /* save next device prior to disposing of this device */
85 pDeviceNext = removeDevice->pNext;
87 if ( gpDeviceList == removeDevice ) {
88 gpDeviceList = pDeviceNext;
90 recDevice *device = gpDeviceList;
91 while (device->pNext != removeDevice) {
92 device = device->pNext;
94 device->pNext = pDeviceNext;
96 removeDevice->pNext = NULL;
98 /* free element lists */
99 FreeElementList(removeDevice->firstAxis);
100 FreeElementList(removeDevice->firstButton);
101 FreeElementList(removeDevice->firstHat);
103 SDL_free(removeDevice);
109 GetHIDElementState(recDevice *pDevice, recElement *pElement, SInt32 *pValue)
112 int returnValue = SDL_FALSE;
114 if (pDevice && pElement) {
115 IOHIDValueRef valueRef;
116 if (IOHIDDeviceGetValue(pDevice->deviceRef, pElement->elementRef, &valueRef) == kIOReturnSuccess) {
117 value = (SInt32) IOHIDValueGetIntegerValue(valueRef);
119 /* record min and max for auto calibration */
120 if (value < pElement->minReport) {
121 pElement->minReport = value;
123 if (value > pElement->maxReport) {
124 pElement->maxReport = value;
128 returnValue = SDL_TRUE;
135 GetHIDScaledCalibratedState(recDevice * pDevice, recElement * pElement, SInt32 min, SInt32 max, SInt32 *pValue)
137 const float deviceScale = max - min;
138 const float readScale = pElement->maxReport - pElement->minReport;
139 int returnValue = SDL_FALSE;
140 if (GetHIDElementState(pDevice, pElement, pValue))
142 if (readScale == 0) {
143 returnValue = SDL_TRUE; /* no scaling at all */
147 *pValue = ((*pValue - pElement->minReport) * deviceScale / readScale) + min;
148 returnValue = SDL_TRUE;
155 JoystickDeviceWasRemovedCallback(void *ctx, IOReturn result, void *sender)
157 recDevice *device = (recDevice *) ctx;
158 device->removed = SDL_TRUE;
159 device->deviceRef = NULL; // deviceRef was invalidated due to the remove
161 MacHaptic_MaybeRemoveDevice(device->ffservice);
164 SDL_PrivateJoystickRemoved(device->instance_id);
168 static void AddHIDElement(const void *value, void *parameter);
170 /* Call AddHIDElement() on all elements in an array of IOHIDElementRefs */
172 AddHIDElements(CFArrayRef array, recDevice *pDevice)
174 const CFRange range = { 0, CFArrayGetCount(array) };
175 CFArrayApplyFunction(array, range, AddHIDElement, pDevice);
179 ElementAlreadyAdded(const IOHIDElementCookie cookie, const recElement *listitem) {
181 if (listitem->cookie == cookie) {
184 listitem = listitem->pNext;
189 /* See if we care about this HID element, and if so, note it in our recDevice. */
191 AddHIDElement(const void *value, void *parameter)
193 recDevice *pDevice = (recDevice *) parameter;
194 IOHIDElementRef refElement = (IOHIDElementRef) value;
195 const CFTypeID elementTypeID = refElement ? CFGetTypeID(refElement) : 0;
197 if (refElement && (elementTypeID == IOHIDElementGetTypeID())) {
198 const IOHIDElementCookie cookie = IOHIDElementGetCookie(refElement);
199 const uint32_t usagePage = IOHIDElementGetUsagePage(refElement);
200 const uint32_t usage = IOHIDElementGetUsage(refElement);
201 recElement *element = NULL;
202 recElement **headElement = NULL;
204 /* look at types of interest */
205 switch (IOHIDElementGetType(refElement)) {
206 case kIOHIDElementTypeInput_Misc:
207 case kIOHIDElementTypeInput_Button:
208 case kIOHIDElementTypeInput_Axis: {
209 switch (usagePage) { /* only interested in kHIDPage_GenericDesktop and kHIDPage_Button */
210 case kHIDPage_GenericDesktop:
215 case kHIDUsage_GD_Rx:
216 case kHIDUsage_GD_Ry:
217 case kHIDUsage_GD_Rz:
218 case kHIDUsage_GD_Slider:
219 case kHIDUsage_GD_Dial:
220 case kHIDUsage_GD_Wheel:
221 if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) {
222 element = (recElement *) SDL_calloc(1, sizeof (recElement));
225 headElement = &(pDevice->firstAxis);
230 case kHIDUsage_GD_Hatswitch:
231 if (!ElementAlreadyAdded(cookie, pDevice->firstHat)) {
232 element = (recElement *) SDL_calloc(1, sizeof (recElement));
235 headElement = &(pDevice->firstHat);
239 case kHIDUsage_GD_DPadUp:
240 case kHIDUsage_GD_DPadDown:
241 case kHIDUsage_GD_DPadRight:
242 case kHIDUsage_GD_DPadLeft:
243 case kHIDUsage_GD_Start:
244 case kHIDUsage_GD_Select:
245 case kHIDUsage_GD_SystemMainMenu:
246 if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) {
247 element = (recElement *) SDL_calloc(1, sizeof (recElement));
250 headElement = &(pDevice->firstButton);
257 case kHIDPage_Simulation:
259 case kHIDUsage_Sim_Rudder:
260 case kHIDUsage_Sim_Throttle:
261 case kHIDUsage_Sim_Accelerator:
262 case kHIDUsage_Sim_Brake:
263 if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) {
264 element = (recElement *) SDL_calloc(1, sizeof (recElement));
267 headElement = &(pDevice->firstAxis);
277 case kHIDPage_Button:
278 case kHIDPage_Consumer: /* e.g. 'pause' button on Steelseries MFi gamepads. */
279 if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) {
280 element = (recElement *) SDL_calloc(1, sizeof (recElement));
283 headElement = &(pDevice->firstButton);
294 case kIOHIDElementTypeCollection: {
295 CFArrayRef array = IOHIDElementGetChildren(refElement);
297 AddHIDElements(array, pDevice);
306 if (element && headElement) { /* add to list */
307 recElement *elementPrevious = NULL;
308 recElement *elementCurrent = *headElement;
309 while (elementCurrent && usage >= elementCurrent->usage) {
310 elementPrevious = elementCurrent;
311 elementCurrent = elementCurrent->pNext;
313 if (elementPrevious) {
314 elementPrevious->pNext = element;
316 *headElement = element;
319 element->elementRef = refElement;
320 element->usagePage = usagePage;
321 element->usage = usage;
322 element->pNext = elementCurrent;
324 element->minReport = element->min = (SInt32) IOHIDElementGetLogicalMin(refElement);
325 element->maxReport = element->max = (SInt32) IOHIDElementGetLogicalMax(refElement);
326 element->cookie = IOHIDElementGetCookie(refElement);
334 GetDeviceInfo(IOHIDDeviceRef hidDevice, recDevice *pDevice)
336 const Uint16 BUS_USB = 0x03;
337 const Uint16 BUS_BLUETOOTH = 0x05;
341 CFTypeRef refCF = NULL;
342 CFArrayRef array = NULL;
343 Uint16 *guid16 = (Uint16 *)pDevice->guid.data;
345 /* get usage page and usage */
346 refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsagePageKey));
348 CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usagePage);
350 if (pDevice->usagePage != kHIDPage_GenericDesktop) {
351 return SDL_FALSE; /* Filter device list to non-keyboard/mouse stuff */
354 refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsageKey));
356 CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usage);
359 if ((pDevice->usage != kHIDUsage_GD_Joystick &&
360 pDevice->usage != kHIDUsage_GD_GamePad &&
361 pDevice->usage != kHIDUsage_GD_MultiAxisController)) {
362 return SDL_FALSE; /* Filter device list to non-keyboard/mouse stuff */
365 pDevice->deviceRef = hidDevice;
367 /* get device name */
368 refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey));
370 /* Maybe we can't get "AwesomeJoystick2000", but we can get "Logitech"? */
371 refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDManufacturerKey));
373 if ((!refCF) || (!CFStringGetCString(refCF, pDevice->product, sizeof (pDevice->product), kCFStringEncodingUTF8))) {
374 SDL_strlcpy(pDevice->product, "Unidentified joystick", sizeof (pDevice->product));
377 refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey));
379 CFNumberGetValue(refCF, kCFNumberSInt32Type, &vendor);
382 refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey));
384 CFNumberGetValue(refCF, kCFNumberSInt32Type, &product);
387 refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVersionNumberKey));
389 CFNumberGetValue(refCF, kCFNumberSInt32Type, &version);
392 SDL_memset(pDevice->guid.data, 0, sizeof(pDevice->guid.data));
394 if (vendor && product) {
395 *guid16++ = SDL_SwapLE16(BUS_USB);
397 *guid16++ = SDL_SwapLE16((Uint16)vendor);
399 *guid16++ = SDL_SwapLE16((Uint16)product);
401 *guid16++ = SDL_SwapLE16((Uint16)version);
404 *guid16++ = SDL_SwapLE16(BUS_BLUETOOTH);
406 SDL_strlcpy((char*)guid16, pDevice->product, sizeof(pDevice->guid.data) - 4);
409 array = IOHIDDeviceCopyMatchingElements(hidDevice, NULL, kIOHIDOptionsTypeNone);
411 AddHIDElements(array, pDevice);
419 JoystickAlreadyKnown(IOHIDDeviceRef ioHIDDeviceObject)
422 for (i = gpDeviceList; i != NULL; i = i->pNext) {
423 if (i->deviceRef == ioHIDDeviceObject) {
432 JoystickDeviceWasAddedCallback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject)
435 int device_index = 0;
436 io_service_t ioservice;
438 if (res != kIOReturnSuccess) {
442 if (JoystickAlreadyKnown(ioHIDDeviceObject)) {
443 return; /* IOKit sent us a duplicate. */
446 device = (recDevice *) SDL_calloc(1, sizeof(recDevice));
453 if (!GetDeviceInfo(ioHIDDeviceObject, device)) {
455 return; /* not a device we care about, probably. */
458 if (SDL_IsGameControllerNameAndGUID(device->product, device->guid) &&
459 SDL_ShouldIgnoreGameController(device->product, device->guid)) {
464 /* Get notified when this device is disconnected. */
465 IOHIDDeviceRegisterRemovalCallback(ioHIDDeviceObject, JoystickDeviceWasRemovedCallback, device);
466 IOHIDDeviceScheduleWithRunLoop(ioHIDDeviceObject, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE);
468 /* Allocate an instance ID for this device */
469 device->instance_id = ++s_joystick_instance_id;
471 /* We have to do some storage of the io_service_t for SDL_HapticOpenFromJoystick */
472 ioservice = IOHIDDeviceGetService(ioHIDDeviceObject);
474 if ((ioservice) && (FFIsForceFeedback(ioservice) == FF_OK)) {
475 device->ffservice = ioservice;
476 MacHaptic_MaybeAddDevice(ioservice);
480 /* Add device to the end of the list */
481 if ( !gpDeviceList ) {
482 gpDeviceList = device;
484 recDevice *curdevice;
486 curdevice = gpDeviceList;
487 while ( curdevice->pNext ) {
489 curdevice = curdevice->pNext;
491 curdevice->pNext = device;
492 ++device_index; /* bump by one since we counted by pNext. */
495 SDL_PrivateJoystickAdded(device_index);
499 ConfigHIDManager(CFArrayRef matchingArray)
501 CFRunLoopRef runloop = CFRunLoopGetCurrent();
503 if (IOHIDManagerOpen(hidman, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
507 IOHIDManagerSetDeviceMatchingMultiple(hidman, matchingArray);
508 IOHIDManagerRegisterDeviceMatchingCallback(hidman, JoystickDeviceWasAddedCallback, NULL);
509 IOHIDManagerScheduleWithRunLoop(hidman, runloop, SDL_JOYSTICK_RUNLOOP_MODE);
511 while (CFRunLoopRunInMode(SDL_JOYSTICK_RUNLOOP_MODE,0,TRUE) == kCFRunLoopRunHandledSource) {
512 /* no-op. Callback fires once per existing device. */
515 /* future hotplug events will come through SDL_JOYSTICK_RUNLOOP_MODE now. */
517 return SDL_TRUE; /* good to go. */
521 static CFDictionaryRef
522 CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage, int *okay)
524 CFDictionaryRef retval = NULL;
525 CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);
526 CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
527 const void *keys[2] = { (void *) CFSTR(kIOHIDDeviceUsagePageKey), (void *) CFSTR(kIOHIDDeviceUsageKey) };
528 const void *vals[2] = { (void *) pageNumRef, (void *) usageNumRef };
530 if (pageNumRef && usageNumRef) {
531 retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
535 CFRelease(pageNumRef);
538 CFRelease(usageNumRef);
549 CreateHIDManager(void)
551 SDL_bool retval = SDL_FALSE;
553 const void *vals[] = {
554 (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay),
555 (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay),
556 (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay),
558 const size_t numElements = SDL_arraysize(vals);
559 CFArrayRef array = okay ? CFArrayCreate(kCFAllocatorDefault, vals, numElements, &kCFTypeArrayCallBacks) : NULL;
562 for (i = 0; i < numElements; i++) {
564 CFRelease((CFTypeRef) vals[i]);
569 hidman = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
570 if (hidman != NULL) {
571 retval = ConfigHIDManager(array);
580 /* Function to scan the system for joysticks.
581 * Joystick 0 should be the system default joystick.
582 * This function should return the number of available joysticks, or -1
583 * on an unrecoverable fatal error.
586 SDL_SYS_JoystickInit(void)
589 return SDL_SetError("Joystick: Device list already inited.");
592 if (!CreateHIDManager()) {
593 return SDL_SetError("Joystick: Couldn't initialize HID Manager");
596 return SDL_SYS_NumJoysticks();
599 /* Function to return the number of joystick devices plugged in right now */
601 SDL_SYS_NumJoysticks(void)
603 recDevice *device = gpDeviceList;
607 if (!device->removed) {
610 device = device->pNext;
616 /* Function to cause any queued joystick insertions to be processed
619 SDL_SYS_JoystickDetect(void)
621 recDevice *device = gpDeviceList;
623 if (device->removed) {
624 device = FreeDevice(device);
626 device = device->pNext;
630 /* run this after the checks above so we don't set device->removed and delete the device before
631 SDL_SYS_JoystickUpdate can run to clean up the SDL_Joystick object that owns this device */
632 while (CFRunLoopRunInMode(SDL_JOYSTICK_RUNLOOP_MODE,0,TRUE) == kCFRunLoopRunHandledSource) {
633 /* no-op. Pending callbacks will fire in CFRunLoopRunInMode(). */
637 /* Function to get the device-dependent name of a joystick */
639 SDL_SYS_JoystickNameForDeviceIndex(int device_index)
641 recDevice *device = GetDeviceForIndex(device_index);
642 return device ? device->product : "UNKNOWN";
645 /* Function to return the instance id of the joystick at device_index
648 SDL_SYS_GetInstanceIdOfDeviceIndex(int device_index)
650 recDevice *device = GetDeviceForIndex(device_index);
651 return device ? device->instance_id : 0;
654 /* Function to open a joystick for use.
655 * The joystick to open is specified by the device index.
656 * This should fill the nbuttons and naxes fields of the joystick structure.
657 * It returns 0, or -1 if there is an error.
660 SDL_SYS_JoystickOpen(SDL_Joystick * joystick, int device_index)
662 recDevice *device = GetDeviceForIndex(device_index);
664 joystick->instance_id = device->instance_id;
665 joystick->hwdata = device;
666 joystick->name = device->product;
668 joystick->naxes = device->axes;
669 joystick->nhats = device->hats;
670 joystick->nballs = 0;
671 joystick->nbuttons = device->buttons;
675 /* Function to query if the joystick is currently attached
676 * It returns SDL_TRUE if attached, SDL_FALSE otherwise.
679 SDL_SYS_JoystickAttached(SDL_Joystick * joystick)
681 return joystick->hwdata != NULL;
684 /* Function to update the state of a joystick - called as a device poll.
685 * This function shouldn't update the joystick structure directly,
686 * but instead should call SDL_PrivateJoystick*() to deliver events
687 * and update joystick device state.
690 SDL_SYS_JoystickUpdate(SDL_Joystick * joystick)
692 recDevice *device = joystick->hwdata;
701 if (device->removed) { /* device was unplugged; ignore it. */
702 if (joystick->hwdata) {
703 joystick->force_recentering = SDL_TRUE;
704 joystick->hwdata = NULL;
709 element = device->firstAxis;
712 int goodRead = SDL_FALSE;
714 goodRead = GetHIDScaledCalibratedState(device, element, -32768, 32767, &value);
716 SDL_PrivateJoystickAxis(joystick, i, value);
719 element = element->pNext;
723 element = device->firstButton;
726 goodRead = GetHIDElementState(device, element, &value);
728 if (value > 1) { /* handle pressure-sensitive buttons */
731 SDL_PrivateJoystickButton(joystick, i, value);
734 element = element->pNext;
738 element = device->firstHat;
744 range = (element->max - element->min + 1);
745 goodRead = GetHIDElementState(device, element, &value);
747 value -= element->min;
748 if (range == 4) { /* 4 position hatswitch - scale up value */
750 } else if (range != 8) { /* Neither a 4 nor 8 positions - fall back to default position (centered) */
758 pos = SDL_HAT_RIGHTUP;
764 pos = SDL_HAT_RIGHTDOWN;
770 pos = SDL_HAT_LEFTDOWN;
776 pos = SDL_HAT_LEFTUP;
779 /* Every other value is mapped to center. We do that because some
780 * joysticks use 8 and some 15 for this value, and apparently
781 * there are even more variants out there - so we try to be generous.
783 pos = SDL_HAT_CENTERED;
787 SDL_PrivateJoystickHat(joystick, i, pos);
790 element = element->pNext;
795 /* Function to close a joystick after use */
797 SDL_SYS_JoystickClose(SDL_Joystick * joystick)
801 /* Function to perform any system-specific joystick related cleanup */
803 SDL_SYS_JoystickQuit(void)
805 while (FreeDevice(gpDeviceList)) {
810 IOHIDManagerUnscheduleFromRunLoop(hidman, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE);
811 IOHIDManagerClose(hidman, kIOHIDOptionsTypeNone);
818 SDL_JoystickGUID SDL_SYS_JoystickGetDeviceGUID( int device_index )
820 recDevice *device = GetDeviceForIndex(device_index);
821 SDL_JoystickGUID guid;
830 SDL_JoystickGUID SDL_SYS_JoystickGetGUID(SDL_Joystick *joystick)
832 return joystick->hwdata->guid;
835 #endif /* SDL_JOYSTICK_IOKIT */
837 /* vi: set ts=4 sw=4 expandtab: */