cocoa_joystick.m (15740B)
1 //======================================================================== 2 // GLFW 3.4 Cocoa - www.glfw.org 3 //------------------------------------------------------------------------ 4 // Copyright (c) 2009-2019 Camilla Löwy <elmindreda@glfw.org> 5 // Copyright (c) 2012 Torsten Walluhn <tw@mad-cad.net> 6 // 7 // This software is provided 'as-is', without any express or implied 8 // warranty. In no event will the authors be held liable for any damages 9 // arising from the use of this software. 10 // 11 // Permission is granted to anyone to use this software for any purpose, 12 // including commercial applications, and to alter it and redistribute it 13 // freely, subject to the following restrictions: 14 // 15 // 1. The origin of this software must not be misrepresented; you must not 16 // claim that you wrote the original software. If you use this software 17 // in a product, an acknowledgment in the product documentation would 18 // be appreciated but is not required. 19 // 20 // 2. Altered source versions must be plainly marked as such, and must not 21 // be misrepresented as being the original software. 22 // 23 // 3. This notice may not be removed or altered from any source 24 // distribution. 25 // 26 //======================================================================== 27 28 #include "internal.h" 29 30 #if defined(_GLFW_COCOA) 31 32 #include <unistd.h> 33 #include <ctype.h> 34 #include <string.h> 35 36 #include <mach/mach.h> 37 #include <mach/mach_error.h> 38 39 #include <CoreFoundation/CoreFoundation.h> 40 #include <Kernel/IOKit/hidsystem/IOHIDUsageTables.h> 41 42 43 // Joystick element information 44 // 45 typedef struct _GLFWjoyelementNS 46 { 47 IOHIDElementRef native; 48 uint32_t usage; 49 int index; 50 long minimum; 51 long maximum; 52 53 } _GLFWjoyelementNS; 54 55 56 // Returns the value of the specified element of the specified joystick 57 // 58 static long getElementValue(_GLFWjoystick* js, _GLFWjoyelementNS* element) 59 { 60 IOHIDValueRef valueRef; 61 long value = 0; 62 63 if (js->ns.device) 64 { 65 if (IOHIDDeviceGetValue(js->ns.device, 66 element->native, 67 &valueRef) == kIOReturnSuccess) 68 { 69 value = IOHIDValueGetIntegerValue(valueRef); 70 } 71 } 72 73 return value; 74 } 75 76 // Comparison function for matching the SDL element order 77 // 78 static CFComparisonResult compareElements(const void* fp, 79 const void* sp, 80 void* user) 81 { 82 const _GLFWjoyelementNS* fe = fp; 83 const _GLFWjoyelementNS* se = sp; 84 if (fe->usage < se->usage) 85 return kCFCompareLessThan; 86 if (fe->usage > se->usage) 87 return kCFCompareGreaterThan; 88 if (fe->index < se->index) 89 return kCFCompareLessThan; 90 if (fe->index > se->index) 91 return kCFCompareGreaterThan; 92 return kCFCompareEqualTo; 93 } 94 95 // Removes the specified joystick 96 // 97 static void closeJoystick(_GLFWjoystick* js) 98 { 99 _glfwInputJoystick(js, GLFW_DISCONNECTED); 100 101 for (int i = 0; i < CFArrayGetCount(js->ns.axes); i++) 102 _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.axes, i)); 103 CFRelease(js->ns.axes); 104 105 for (int i = 0; i < CFArrayGetCount(js->ns.buttons); i++) 106 _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.buttons, i)); 107 CFRelease(js->ns.buttons); 108 109 for (int i = 0; i < CFArrayGetCount(js->ns.hats); i++) 110 _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.hats, i)); 111 CFRelease(js->ns.hats); 112 113 _glfwFreeJoystick(js); 114 } 115 116 // Callback for user-initiated joystick addition 117 // 118 static void matchCallback(void* context, 119 IOReturn result, 120 void* sender, 121 IOHIDDeviceRef device) 122 { 123 int jid; 124 char name[256]; 125 char guid[33]; 126 CFTypeRef property; 127 uint32_t vendor = 0, product = 0, version = 0; 128 _GLFWjoystick* js; 129 CFMutableArrayRef axes, buttons, hats; 130 131 for (jid = 0; jid <= GLFW_JOYSTICK_LAST; jid++) 132 { 133 if (_glfw.joysticks[jid].ns.device == device) 134 return; 135 } 136 137 CFArrayRef elements = 138 IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone); 139 140 // It is reportedly possible for this to fail on macOS 13 Ventura 141 // if the application does not have input monitoring permissions 142 if (!elements) 143 return; 144 145 axes = CFArrayCreateMutable(NULL, 0, NULL); 146 buttons = CFArrayCreateMutable(NULL, 0, NULL); 147 hats = CFArrayCreateMutable(NULL, 0, NULL); 148 149 property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey)); 150 if (property) 151 { 152 CFStringGetCString(property, 153 name, 154 sizeof(name), 155 kCFStringEncodingUTF8); 156 } 157 else 158 strncpy(name, "Unknown", sizeof(name)); 159 160 property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey)); 161 if (property) 162 CFNumberGetValue(property, kCFNumberSInt32Type, &vendor); 163 164 property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)); 165 if (property) 166 CFNumberGetValue(property, kCFNumberSInt32Type, &product); 167 168 property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVersionNumberKey)); 169 if (property) 170 CFNumberGetValue(property, kCFNumberSInt32Type, &version); 171 172 // Generate a joystick GUID that matches the SDL 2.0.5+ one 173 if (vendor && product) 174 { 175 sprintf(guid, "03000000%02x%02x0000%02x%02x0000%02x%02x0000", 176 (uint8_t) vendor, (uint8_t) (vendor >> 8), 177 (uint8_t) product, (uint8_t) (product >> 8), 178 (uint8_t) version, (uint8_t) (version >> 8)); 179 } 180 else 181 { 182 sprintf(guid, "05000000%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x00", 183 name[0], name[1], name[2], name[3], 184 name[4], name[5], name[6], name[7], 185 name[8], name[9], name[10]); 186 } 187 188 for (CFIndex i = 0; i < CFArrayGetCount(elements); i++) 189 { 190 IOHIDElementRef native = (IOHIDElementRef) 191 CFArrayGetValueAtIndex(elements, i); 192 if (CFGetTypeID(native) != IOHIDElementGetTypeID()) 193 continue; 194 195 const IOHIDElementType type = IOHIDElementGetType(native); 196 if ((type != kIOHIDElementTypeInput_Axis) && 197 (type != kIOHIDElementTypeInput_Button) && 198 (type != kIOHIDElementTypeInput_Misc)) 199 { 200 continue; 201 } 202 203 CFMutableArrayRef target = NULL; 204 205 const uint32_t usage = IOHIDElementGetUsage(native); 206 const uint32_t page = IOHIDElementGetUsagePage(native); 207 if (page == kHIDPage_GenericDesktop) 208 { 209 switch (usage) 210 { 211 case kHIDUsage_GD_X: 212 case kHIDUsage_GD_Y: 213 case kHIDUsage_GD_Z: 214 case kHIDUsage_GD_Rx: 215 case kHIDUsage_GD_Ry: 216 case kHIDUsage_GD_Rz: 217 case kHIDUsage_GD_Slider: 218 case kHIDUsage_GD_Dial: 219 case kHIDUsage_GD_Wheel: 220 target = axes; 221 break; 222 case kHIDUsage_GD_Hatswitch: 223 target = hats; 224 break; 225 case kHIDUsage_GD_DPadUp: 226 case kHIDUsage_GD_DPadRight: 227 case kHIDUsage_GD_DPadDown: 228 case kHIDUsage_GD_DPadLeft: 229 case kHIDUsage_GD_SystemMainMenu: 230 case kHIDUsage_GD_Select: 231 case kHIDUsage_GD_Start: 232 target = buttons; 233 break; 234 } 235 } 236 else if (page == kHIDPage_Simulation) 237 { 238 switch (usage) 239 { 240 case kHIDUsage_Sim_Accelerator: 241 case kHIDUsage_Sim_Brake: 242 case kHIDUsage_Sim_Throttle: 243 case kHIDUsage_Sim_Rudder: 244 case kHIDUsage_Sim_Steering: 245 target = axes; 246 break; 247 } 248 } 249 else if (page == kHIDPage_Button || page == kHIDPage_Consumer) 250 target = buttons; 251 252 if (target) 253 { 254 _GLFWjoyelementNS* element = _glfw_calloc(1, sizeof(_GLFWjoyelementNS)); 255 element->native = native; 256 element->usage = usage; 257 element->index = (int) CFArrayGetCount(target); 258 element->minimum = IOHIDElementGetLogicalMin(native); 259 element->maximum = IOHIDElementGetLogicalMax(native); 260 CFArrayAppendValue(target, element); 261 } 262 } 263 264 CFRelease(elements); 265 266 CFArraySortValues(axes, CFRangeMake(0, CFArrayGetCount(axes)), 267 compareElements, NULL); 268 CFArraySortValues(buttons, CFRangeMake(0, CFArrayGetCount(buttons)), 269 compareElements, NULL); 270 CFArraySortValues(hats, CFRangeMake(0, CFArrayGetCount(hats)), 271 compareElements, NULL); 272 273 js = _glfwAllocJoystick(name, guid, 274 (int) CFArrayGetCount(axes), 275 (int) CFArrayGetCount(buttons), 276 (int) CFArrayGetCount(hats)); 277 278 js->ns.device = device; 279 js->ns.axes = axes; 280 js->ns.buttons = buttons; 281 js->ns.hats = hats; 282 283 _glfwInputJoystick(js, GLFW_CONNECTED); 284 } 285 286 // Callback for user-initiated joystick removal 287 // 288 static void removeCallback(void* context, 289 IOReturn result, 290 void* sender, 291 IOHIDDeviceRef device) 292 { 293 for (int jid = 0; jid <= GLFW_JOYSTICK_LAST; jid++) 294 { 295 if (_glfw.joysticks[jid].connected && _glfw.joysticks[jid].ns.device == device) 296 { 297 closeJoystick(&_glfw.joysticks[jid]); 298 break; 299 } 300 } 301 } 302 303 304 ////////////////////////////////////////////////////////////////////////// 305 ////// GLFW platform API ////// 306 ////////////////////////////////////////////////////////////////////////// 307 308 GLFWbool _glfwInitJoysticksCocoa(void) 309 { 310 CFMutableArrayRef matching; 311 const long usages[] = 312 { 313 kHIDUsage_GD_Joystick, 314 kHIDUsage_GD_GamePad, 315 kHIDUsage_GD_MultiAxisController 316 }; 317 318 _glfw.ns.hidManager = IOHIDManagerCreate(kCFAllocatorDefault, 319 kIOHIDOptionsTypeNone); 320 321 matching = CFArrayCreateMutable(kCFAllocatorDefault, 322 0, 323 &kCFTypeArrayCallBacks); 324 if (!matching) 325 { 326 _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create array"); 327 return GLFW_FALSE; 328 } 329 330 for (size_t i = 0; i < sizeof(usages) / sizeof(long); i++) 331 { 332 const long page = kHIDPage_GenericDesktop; 333 334 CFMutableDictionaryRef dict = 335 CFDictionaryCreateMutable(kCFAllocatorDefault, 336 0, 337 &kCFTypeDictionaryKeyCallBacks, 338 &kCFTypeDictionaryValueCallBacks); 339 if (!dict) 340 continue; 341 342 CFNumberRef pageRef = CFNumberCreate(kCFAllocatorDefault, 343 kCFNumberLongType, 344 &page); 345 CFNumberRef usageRef = CFNumberCreate(kCFAllocatorDefault, 346 kCFNumberLongType, 347 &usages[i]); 348 if (pageRef && usageRef) 349 { 350 CFDictionarySetValue(dict, 351 CFSTR(kIOHIDDeviceUsagePageKey), 352 pageRef); 353 CFDictionarySetValue(dict, 354 CFSTR(kIOHIDDeviceUsageKey), 355 usageRef); 356 CFArrayAppendValue(matching, dict); 357 } 358 359 if (pageRef) 360 CFRelease(pageRef); 361 if (usageRef) 362 CFRelease(usageRef); 363 364 CFRelease(dict); 365 } 366 367 IOHIDManagerSetDeviceMatchingMultiple(_glfw.ns.hidManager, matching); 368 CFRelease(matching); 369 370 IOHIDManagerRegisterDeviceMatchingCallback(_glfw.ns.hidManager, 371 &matchCallback, NULL); 372 IOHIDManagerRegisterDeviceRemovalCallback(_glfw.ns.hidManager, 373 &removeCallback, NULL); 374 IOHIDManagerScheduleWithRunLoop(_glfw.ns.hidManager, 375 CFRunLoopGetMain(), 376 kCFRunLoopDefaultMode); 377 IOHIDManagerOpen(_glfw.ns.hidManager, kIOHIDOptionsTypeNone); 378 379 // Execute the run loop once in order to register any initially-attached 380 // joysticks 381 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false); 382 return GLFW_TRUE; 383 } 384 385 void _glfwTerminateJoysticksCocoa(void) 386 { 387 for (int jid = 0; jid <= GLFW_JOYSTICK_LAST; jid++) 388 { 389 if (_glfw.joysticks[jid].connected) 390 closeJoystick(&_glfw.joysticks[jid]); 391 } 392 393 if (_glfw.ns.hidManager) 394 { 395 CFRelease(_glfw.ns.hidManager); 396 _glfw.ns.hidManager = NULL; 397 } 398 } 399 400 401 GLFWbool _glfwPollJoystickCocoa(_GLFWjoystick* js, int mode) 402 { 403 if (mode & _GLFW_POLL_AXES) 404 { 405 for (CFIndex i = 0; i < CFArrayGetCount(js->ns.axes); i++) 406 { 407 _GLFWjoyelementNS* axis = (_GLFWjoyelementNS*) 408 CFArrayGetValueAtIndex(js->ns.axes, i); 409 410 const long raw = getElementValue(js, axis); 411 // Perform auto calibration 412 if (raw < axis->minimum) 413 axis->minimum = raw; 414 if (raw > axis->maximum) 415 axis->maximum = raw; 416 417 const long size = axis->maximum - axis->minimum; 418 if (size == 0) 419 _glfwInputJoystickAxis(js, (int) i, 0.f); 420 else 421 { 422 const float value = (2.f * (raw - axis->minimum) / size) - 1.f; 423 _glfwInputJoystickAxis(js, (int) i, value); 424 } 425 } 426 } 427 428 if (mode & _GLFW_POLL_BUTTONS) 429 { 430 for (CFIndex i = 0; i < CFArrayGetCount(js->ns.buttons); i++) 431 { 432 _GLFWjoyelementNS* button = (_GLFWjoyelementNS*) 433 CFArrayGetValueAtIndex(js->ns.buttons, i); 434 const char value = getElementValue(js, button) - button->minimum; 435 const int state = (value > 0) ? GLFW_PRESS : GLFW_RELEASE; 436 _glfwInputJoystickButton(js, (int) i, state); 437 } 438 439 for (CFIndex i = 0; i < CFArrayGetCount(js->ns.hats); i++) 440 { 441 const int states[9] = 442 { 443 GLFW_HAT_UP, 444 GLFW_HAT_RIGHT_UP, 445 GLFW_HAT_RIGHT, 446 GLFW_HAT_RIGHT_DOWN, 447 GLFW_HAT_DOWN, 448 GLFW_HAT_LEFT_DOWN, 449 GLFW_HAT_LEFT, 450 GLFW_HAT_LEFT_UP, 451 GLFW_HAT_CENTERED 452 }; 453 454 _GLFWjoyelementNS* hat = (_GLFWjoyelementNS*) 455 CFArrayGetValueAtIndex(js->ns.hats, i); 456 long state = getElementValue(js, hat) - hat->minimum; 457 if (state < 0 || state > 8) 458 state = 8; 459 460 _glfwInputJoystickHat(js, (int) i, states[state]); 461 } 462 } 463 464 return js->connected; 465 } 466 467 const char* _glfwGetMappingNameCocoa(void) 468 { 469 return "Mac OS X"; 470 } 471 472 void _glfwUpdateGamepadGUIDCocoa(char* guid) 473 { 474 if ((strncmp(guid + 4, "000000000000", 12) == 0) && 475 (strncmp(guid + 20, "000000000000", 12) == 0)) 476 { 477 char original[33]; 478 strncpy(original, guid, sizeof(original) - 1); 479 sprintf(guid, "03000000%.4s0000%.4s000000000000", 480 original, original + 16); 481 } 482 } 483 484 #endif // _GLFW_COCOA 485