1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 // mostly derived from the Allegro source code at:
8 // http://alleg.svn.sourceforge.net/viewvc/alleg/allegro/branches/4.9/src/macosx/hidjoy.m?revision=13760&view=markup
9
10 #include "mozilla/dom/GamepadPlatformService.h"
11 #include "mozilla/ArrayUtils.h"
12 #include "nsITimer.h"
13 #include "nsThreadUtils.h"
14 #include <CoreFoundation/CoreFoundation.h>
15 #include <IOKit/hid/IOHIDBase.h>
16 #include <IOKit/hid/IOHIDKeys.h>
17 #include <IOKit/hid/IOHIDManager.h>
18
19 #include <stdio.h>
20 #include <vector>
21
22 namespace {
23
24 using namespace mozilla;
25 using namespace mozilla::dom;
26 using std::vector;
27 class DarwinGamepadService;
28
29 DarwinGamepadService* gService = nullptr;
30
31 struct Button {
32 int id;
33 bool analog;
34 IOHIDElementRef element;
35 CFIndex min;
36 CFIndex max;
37
Button__anond6aa904a0111::Button38 Button(int aId, IOHIDElementRef aElement, CFIndex aMin, CFIndex aMax)
39 : id(aId),
40 analog((aMax - aMin) > 1),
41 element(aElement),
42 min(aMin),
43 max(aMax) {}
44 };
45
46 struct Axis {
47 int id;
48 IOHIDElementRef element;
49 uint32_t usagePage;
50 uint32_t usage;
51 CFIndex min;
52 CFIndex max;
53 };
54
55 typedef bool dpad_buttons[4];
56
57 // These values can be found in the USB HID Usage Tables:
58 // http://www.usb.org/developers/hidpage
59 const unsigned kDesktopUsagePage = 0x01;
60 const unsigned kSimUsagePage = 0x02;
61 const unsigned kAcceleratorUsage = 0xC4;
62 const unsigned kBrakeUsage = 0xC5;
63 const unsigned kJoystickUsage = 0x04;
64 const unsigned kGamepadUsage = 0x05;
65 const unsigned kAxisUsageMin = 0x30;
66 const unsigned kAxisUsageMax = 0x35;
67 const unsigned kDpadUsage = 0x39;
68 const unsigned kButtonUsagePage = 0x09;
69 const unsigned kConsumerPage = 0x0C;
70 const unsigned kHomeUsage = 0x223;
71 const unsigned kBackUsage = 0x224;
72
73 // We poll it periodically,
74 // 50ms is arbitrarily chosen.
75 const uint32_t kDarwinGamepadPollInterval = 50;
76
77 class Gamepad {
78 private:
79 IOHIDDeviceRef mDevice;
80 nsTArray<Button> buttons;
81 nsTArray<Axis> axes;
82 IOHIDElementRef mDpad;
83 dpad_buttons mDpadState;
84
85 public:
Gamepad()86 Gamepad() : mDevice(nullptr), mDpad(nullptr), mSuperIndex(-1) {}
operator ==(IOHIDDeviceRef device) const87 bool operator==(IOHIDDeviceRef device) const { return mDevice == device; }
empty() const88 bool empty() const { return mDevice == nullptr; }
clear()89 void clear() {
90 mDevice = nullptr;
91 buttons.Clear();
92 axes.Clear();
93 mDpad = nullptr;
94 mSuperIndex = -1;
95 }
96 void init(IOHIDDeviceRef device);
numButtons()97 size_t numButtons() { return buttons.Length() + (mDpad ? 4 : 0); }
numAxes()98 size_t numAxes() { return axes.Length(); }
99
100 // Index given by our superclass.
101 uint32_t mSuperIndex;
102
isDpad(IOHIDElementRef element) const103 bool isDpad(IOHIDElementRef element) const { return element == mDpad; }
104
getDpadState() const105 const dpad_buttons& getDpadState() const { return mDpadState; }
106
setDpadState(const dpad_buttons & dpadState)107 void setDpadState(const dpad_buttons& dpadState) {
108 for (unsigned i = 0; i < ArrayLength(mDpadState); i++) {
109 mDpadState[i] = dpadState[i];
110 }
111 }
112
lookupButton(IOHIDElementRef element) const113 const Button* lookupButton(IOHIDElementRef element) const {
114 for (unsigned i = 0; i < buttons.Length(); i++) {
115 if (buttons[i].element == element) return &buttons[i];
116 }
117 return nullptr;
118 }
119
lookupAxis(IOHIDElementRef element) const120 const Axis* lookupAxis(IOHIDElementRef element) const {
121 for (unsigned i = 0; i < axes.Length(); i++) {
122 if (axes[i].element == element) return &axes[i];
123 }
124 return nullptr;
125 }
126 };
127
128 class AxisComparator {
129 public:
Equals(const Axis & a1,const Axis & a2) const130 bool Equals(const Axis& a1, const Axis& a2) const {
131 return a1.usagePage == a2.usagePage && a1.usage == a2.usage;
132 }
LessThan(const Axis & a1,const Axis & a2) const133 bool LessThan(const Axis& a1, const Axis& a2) const {
134 if (a1.usagePage == a2.usagePage) {
135 return a1.usage < a2.usage;
136 }
137 return a1.usagePage < a2.usagePage;
138 }
139 };
140
init(IOHIDDeviceRef device)141 void Gamepad::init(IOHIDDeviceRef device) {
142 clear();
143 mDevice = device;
144
145 CFArrayRef elements =
146 IOHIDDeviceCopyMatchingElements(device, nullptr, kIOHIDOptionsTypeNone);
147 CFIndex n = CFArrayGetCount(elements);
148 for (CFIndex i = 0; i < n; i++) {
149 IOHIDElementRef element =
150 (IOHIDElementRef)CFArrayGetValueAtIndex(elements, i);
151 uint32_t usagePage = IOHIDElementGetUsagePage(element);
152 uint32_t usage = IOHIDElementGetUsage(element);
153
154 if (usagePage == kDesktopUsagePage && usage >= kAxisUsageMin &&
155 usage <= kAxisUsageMax) {
156 Axis axis = {int(axes.Length()),
157 element,
158 usagePage,
159 usage,
160 IOHIDElementGetLogicalMin(element),
161 IOHIDElementGetLogicalMax(element)};
162 axes.AppendElement(axis);
163 } else if (usagePage == kDesktopUsagePage && usage == kDpadUsage &&
164 // Don't know how to handle d-pads that return weird values.
165 IOHIDElementGetLogicalMax(element) -
166 IOHIDElementGetLogicalMin(element) ==
167 7) {
168 mDpad = element;
169 } else if ((usagePage == kSimUsagePage &&
170 (usage == kAcceleratorUsage || usage == kBrakeUsage)) ||
171 (usagePage == kButtonUsagePage) ||
172 (usagePage == kConsumerPage &&
173 (usage == kHomeUsage || usage == kBackUsage))) {
174 Button button(int(buttons.Length()), element,
175 IOHIDElementGetLogicalMin(element),
176 IOHIDElementGetLogicalMax(element));
177 buttons.AppendElement(button);
178 } else {
179 // TODO: handle other usage pages
180 }
181 }
182
183 AxisComparator comparator;
184 axes.Sort(comparator);
185 for (unsigned i = 0; i < axes.Length(); i++) {
186 axes[i].id = i;
187 }
188 }
189
190 // This service is created and destroyed in Background thread while
191 // operates in a seperate thread(We call it Monitor thread here).
192 class DarwinGamepadService {
193 private:
194 IOHIDManagerRef mManager;
195 vector<Gamepad> mGamepads;
196
197 nsCOMPtr<nsIThread> mMonitorThread;
198 nsCOMPtr<nsIThread> mBackgroundThread;
199 nsCOMPtr<nsITimer> mPollingTimer;
200 bool mIsRunning;
201
202 static void DeviceAddedCallback(void* data, IOReturn result, void* sender,
203 IOHIDDeviceRef device);
204 static void DeviceRemovedCallback(void* data, IOReturn result, void* sender,
205 IOHIDDeviceRef device);
206 static void InputValueChangedCallback(void* data, IOReturn result,
207 void* sender, IOHIDValueRef newValue);
208 static void EventLoopOnceCallback(nsITimer* aTimer, void* aClosure);
209
210 void DeviceAdded(IOHIDDeviceRef device);
211 void DeviceRemoved(IOHIDDeviceRef device);
212 void InputValueChanged(IOHIDValueRef value);
213 void StartupInternal();
214 void RunEventLoopOnce();
215
216 public:
217 DarwinGamepadService();
218 ~DarwinGamepadService();
219 void Startup();
220 void Shutdown();
221 friend class DarwinGamepadServiceStartupRunnable;
222 friend class DarwinGamepadServiceShutdownRunnable;
223 };
224
225 class DarwinGamepadServiceStartupRunnable final : public Runnable {
226 private:
~DarwinGamepadServiceStartupRunnable()227 ~DarwinGamepadServiceStartupRunnable() {}
228 // This Runnable schedules startup of DarwinGamepadService
229 // in a new thread, pointer to DarwinGamepadService is only
230 // used by this Runnable within its thread.
231 DarwinGamepadService MOZ_NON_OWNING_REF* mService;
232
233 public:
DarwinGamepadServiceStartupRunnable(DarwinGamepadService * service)234 explicit DarwinGamepadServiceStartupRunnable(DarwinGamepadService* service)
235 : Runnable("DarwinGamepadServiceStartupRunnable"), mService(service) {}
Run()236 NS_IMETHOD Run() override {
237 MOZ_ASSERT(mService);
238 mService->StartupInternal();
239 return NS_OK;
240 }
241 };
242
243 class DarwinGamepadServiceShutdownRunnable final : public Runnable {
244 private:
~DarwinGamepadServiceShutdownRunnable()245 ~DarwinGamepadServiceShutdownRunnable() {}
246
247 public:
248 // This Runnable schedules shutdown of DarwinGamepadService
249 // in background thread.
DarwinGamepadServiceShutdownRunnable()250 explicit DarwinGamepadServiceShutdownRunnable()
251 : Runnable("DarwinGamepadServiceStartupRunnable") {}
Run()252 NS_IMETHOD Run() override {
253 MOZ_ASSERT(gService);
254 MOZ_ASSERT(NS_GetCurrentThread() == gService->mBackgroundThread);
255
256 IOHIDManagerRef manager = (IOHIDManagerRef)gService->mManager;
257
258 if (manager) {
259 IOHIDManagerClose(manager, 0);
260 CFRelease(manager);
261 gService->mManager = nullptr;
262 }
263 gService->mMonitorThread->Shutdown();
264 delete gService;
265 gService = nullptr;
266 return NS_OK;
267 }
268 };
269
DeviceAdded(IOHIDDeviceRef device)270 void DarwinGamepadService::DeviceAdded(IOHIDDeviceRef device) {
271 RefPtr<GamepadPlatformService> service =
272 GamepadPlatformService::GetParentService();
273 if (!service) {
274 return;
275 }
276
277 size_t slot = size_t(-1);
278 for (size_t i = 0; i < mGamepads.size(); i++) {
279 if (mGamepads[i] == device) return;
280 if (slot == size_t(-1) && mGamepads[i].empty()) slot = i;
281 }
282
283 if (slot == size_t(-1)) {
284 slot = mGamepads.size();
285 mGamepads.push_back(Gamepad());
286 }
287 mGamepads[slot].init(device);
288
289 // Gather some identifying information
290 CFNumberRef vendorIdRef =
291 (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey));
292 CFNumberRef productIdRef =
293 (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey));
294 CFStringRef productRef =
295 (CFStringRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
296 int vendorId, productId;
297 CFNumberGetValue(vendorIdRef, kCFNumberIntType, &vendorId);
298 CFNumberGetValue(productIdRef, kCFNumberIntType, &productId);
299 char product_name[128];
300 CFStringGetCString(productRef, product_name, sizeof(product_name),
301 kCFStringEncodingASCII);
302 char buffer[256];
303 sprintf(buffer, "%x-%x-%s", vendorId, productId, product_name);
304 uint32_t index = service->AddGamepad(
305 buffer, mozilla::dom::GamepadMappingType::_empty,
306 mozilla::dom::GamepadHand::_empty, (int)mGamepads[slot].numButtons(),
307 (int)mGamepads[slot].numAxes(),
308 0); // TODO: Bug 680289, implement gamepad haptics for cocoa
309 mGamepads[slot].mSuperIndex = index;
310 }
311
DeviceRemoved(IOHIDDeviceRef device)312 void DarwinGamepadService::DeviceRemoved(IOHIDDeviceRef device) {
313 RefPtr<GamepadPlatformService> service =
314 GamepadPlatformService::GetParentService();
315 if (!service) {
316 return;
317 }
318 for (size_t i = 0; i < mGamepads.size(); i++) {
319 if (mGamepads[i] == device) {
320 service->RemoveGamepad(mGamepads[i].mSuperIndex);
321 mGamepads[i].clear();
322 return;
323 }
324 }
325 }
326
327 /*
328 * Given a value from a d-pad (POV hat in USB HID terminology),
329 * represent it as 4 buttons, one for each cardinal direction.
330 */
UnpackDpad(int dpad_value,int min,int max,dpad_buttons & buttons)331 static void UnpackDpad(int dpad_value, int min, int max,
332 dpad_buttons& buttons) {
333 const unsigned kUp = 0;
334 const unsigned kDown = 1;
335 const unsigned kLeft = 2;
336 const unsigned kRight = 3;
337
338 // Different controllers have different ways of representing
339 // "nothing is pressed", but they're all outside the range of values.
340 if (dpad_value < min || dpad_value > max) {
341 // Nothing is pressed.
342 return;
343 }
344
345 // Normalize value to start at 0.
346 int value = dpad_value - min;
347
348 // Value will be in the range 0-7. The value represents the
349 // position of the d-pad around a circle, with 0 being straight up,
350 // 2 being right, 4 being straight down, and 6 being left.
351 if (value < 2 || value > 6) {
352 buttons[kUp] = true;
353 }
354 if (value > 2 && value < 6) {
355 buttons[kDown] = true;
356 }
357 if (value > 4) {
358 buttons[kLeft] = true;
359 }
360 if (value > 0 && value < 4) {
361 buttons[kRight] = true;
362 }
363 }
364
InputValueChanged(IOHIDValueRef value)365 void DarwinGamepadService::InputValueChanged(IOHIDValueRef value) {
366 RefPtr<GamepadPlatformService> service =
367 GamepadPlatformService::GetParentService();
368 if (!service) {
369 return;
370 }
371
372 uint32_t value_length = IOHIDValueGetLength(value);
373 if (value_length > 4) {
374 // Workaround for bizarre issue with PS3 controllers that try to return
375 // massive (30+ byte) values and crash IOHIDValueGetIntegerValue
376 return;
377 }
378 IOHIDElementRef element = IOHIDValueGetElement(value);
379 IOHIDDeviceRef device = IOHIDElementGetDevice(element);
380
381 for (unsigned i = 0; i < mGamepads.size(); i++) {
382 Gamepad& gamepad = mGamepads[i];
383 if (gamepad == device) {
384 if (gamepad.isDpad(element)) {
385 const dpad_buttons& oldState = gamepad.getDpadState();
386 dpad_buttons newState = {false, false, false, false};
387 UnpackDpad(IOHIDValueGetIntegerValue(value),
388 IOHIDElementGetLogicalMin(element),
389 IOHIDElementGetLogicalMax(element), newState);
390 const int numButtons = gamepad.numButtons();
391 for (unsigned b = 0; b < ArrayLength(newState); b++) {
392 if (newState[b] != oldState[b]) {
393 service->NewButtonEvent(gamepad.mSuperIndex, numButtons - 4 + b,
394 newState[b]);
395 }
396 }
397 gamepad.setDpadState(newState);
398 } else if (const Axis* axis = gamepad.lookupAxis(element)) {
399 double d = IOHIDValueGetIntegerValue(value);
400 double v =
401 2.0f * (d - axis->min) / (double)(axis->max - axis->min) - 1.0f;
402 service->NewAxisMoveEvent(gamepad.mSuperIndex, axis->id, v);
403 } else if (const Button* button = gamepad.lookupButton(element)) {
404 int iv = IOHIDValueGetIntegerValue(value);
405 bool pressed = iv != 0;
406 double v = 0;
407 if (button->analog) {
408 double dv = iv;
409 v = (dv - button->min) / (double)(button->max - button->min);
410 } else {
411 v = pressed ? 1.0 : 0.0;
412 }
413 service->NewButtonEvent(gamepad.mSuperIndex, button->id, pressed, v);
414 }
415 return;
416 }
417 }
418 }
419
DeviceAddedCallback(void * data,IOReturn result,void * sender,IOHIDDeviceRef device)420 void DarwinGamepadService::DeviceAddedCallback(void* data, IOReturn result,
421 void* sender,
422 IOHIDDeviceRef device) {
423 DarwinGamepadService* service = (DarwinGamepadService*)data;
424 service->DeviceAdded(device);
425 }
426
DeviceRemovedCallback(void * data,IOReturn result,void * sender,IOHIDDeviceRef device)427 void DarwinGamepadService::DeviceRemovedCallback(void* data, IOReturn result,
428 void* sender,
429 IOHIDDeviceRef device) {
430 DarwinGamepadService* service = (DarwinGamepadService*)data;
431 service->DeviceRemoved(device);
432 }
433
InputValueChangedCallback(void * data,IOReturn result,void * sender,IOHIDValueRef newValue)434 void DarwinGamepadService::InputValueChangedCallback(void* data,
435 IOReturn result,
436 void* sender,
437 IOHIDValueRef newValue) {
438 DarwinGamepadService* service = (DarwinGamepadService*)data;
439 service->InputValueChanged(newValue);
440 }
441
EventLoopOnceCallback(nsITimer * aTimer,void * aClosure)442 void DarwinGamepadService::EventLoopOnceCallback(nsITimer* aTimer,
443 void* aClosure) {
444 DarwinGamepadService* service = static_cast<DarwinGamepadService*>(aClosure);
445 service->RunEventLoopOnce();
446 }
447
MatchingDictionary(UInt32 inUsagePage,UInt32 inUsage)448 static CFMutableDictionaryRef MatchingDictionary(UInt32 inUsagePage,
449 UInt32 inUsage) {
450 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(
451 kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
452 &kCFTypeDictionaryValueCallBacks);
453 if (!dict) return nullptr;
454 CFNumberRef number =
455 CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &inUsagePage);
456 if (!number) {
457 CFRelease(dict);
458 return nullptr;
459 }
460 CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsagePageKey), number);
461 CFRelease(number);
462
463 number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &inUsage);
464 if (!number) {
465 CFRelease(dict);
466 return nullptr;
467 }
468 CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsageKey), number);
469 CFRelease(number);
470
471 return dict;
472 }
473
DarwinGamepadService()474 DarwinGamepadService::DarwinGamepadService()
475 : mManager(nullptr), mIsRunning(false) {}
476
~DarwinGamepadService()477 DarwinGamepadService::~DarwinGamepadService() {
478 if (mManager != nullptr) CFRelease(mManager);
479 mMonitorThread = nullptr;
480 mBackgroundThread = nullptr;
481 if (mPollingTimer) {
482 mPollingTimer->Cancel();
483 mPollingTimer = nullptr;
484 }
485 }
486
RunEventLoopOnce()487 void DarwinGamepadService::RunEventLoopOnce() {
488 MOZ_ASSERT(NS_GetCurrentThread() == mMonitorThread);
489 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true);
490
491 // This timer must be created in monitor thread
492 if (!mPollingTimer) {
493 mPollingTimer = do_CreateInstance("@mozilla.org/timer;1");
494 }
495 mPollingTimer->Cancel();
496 if (mIsRunning) {
497 mPollingTimer->InitWithNamedFuncCallback(
498 EventLoopOnceCallback, this, kDarwinGamepadPollInterval,
499 nsITimer::TYPE_ONE_SHOT, "EventLoopOnceCallback");
500 } else {
501 // We schedule a task shutdown and cleaning up resources to Background
502 // thread here to make sure no runloop is running to prevent potential race
503 // condition.
504 RefPtr<Runnable> shutdownTask = new DarwinGamepadServiceShutdownRunnable();
505 mBackgroundThread->Dispatch(shutdownTask.forget(), NS_DISPATCH_NORMAL);
506 }
507 }
508
StartupInternal()509 void DarwinGamepadService::StartupInternal() {
510 if (mManager != nullptr) return;
511
512 IOHIDManagerRef manager =
513 IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
514
515 CFMutableDictionaryRef criteria_arr[2];
516 criteria_arr[0] = MatchingDictionary(kDesktopUsagePage, kJoystickUsage);
517 if (!criteria_arr[0]) {
518 CFRelease(manager);
519 return;
520 }
521
522 criteria_arr[1] = MatchingDictionary(kDesktopUsagePage, kGamepadUsage);
523 if (!criteria_arr[1]) {
524 CFRelease(criteria_arr[0]);
525 CFRelease(manager);
526 return;
527 }
528
529 CFArrayRef criteria = CFArrayCreate(kCFAllocatorDefault,
530 (const void**)criteria_arr, 2, nullptr);
531 if (!criteria) {
532 CFRelease(criteria_arr[1]);
533 CFRelease(criteria_arr[0]);
534 CFRelease(manager);
535 return;
536 }
537
538 IOHIDManagerSetDeviceMatchingMultiple(manager, criteria);
539 CFRelease(criteria);
540 CFRelease(criteria_arr[1]);
541 CFRelease(criteria_arr[0]);
542
543 IOHIDManagerRegisterDeviceMatchingCallback(manager, DeviceAddedCallback,
544 this);
545 IOHIDManagerRegisterDeviceRemovalCallback(manager, DeviceRemovedCallback,
546 this);
547 IOHIDManagerRegisterInputValueCallback(manager, InputValueChangedCallback,
548 this);
549 IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(),
550 kCFRunLoopDefaultMode);
551 IOReturn rv = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone);
552 if (rv != kIOReturnSuccess) {
553 CFRelease(manager);
554 return;
555 }
556
557 mManager = manager;
558
559 mIsRunning = true;
560 RunEventLoopOnce();
561 }
562
Startup()563 void DarwinGamepadService::Startup() {
564 mBackgroundThread = NS_GetCurrentThread();
565 Unused << NS_NewNamedThread("Gamepad", getter_AddRefs(mMonitorThread),
566 new DarwinGamepadServiceStartupRunnable(this));
567 }
568
Shutdown()569 void DarwinGamepadService::Shutdown() {
570 // Flipping this flag will stop the eventloop in Monitor thread
571 // and dispatch a task destroying and cleaning up resources in
572 // Background thread
573 mIsRunning = false;
574 }
575
576 } // namespace
577
578 namespace mozilla {
579 namespace dom {
580
StartGamepadMonitoring()581 void StartGamepadMonitoring() {
582 AssertIsOnBackgroundThread();
583 if (gService) {
584 return;
585 }
586
587 gService = new DarwinGamepadService();
588 gService->Startup();
589 }
590
StopGamepadMonitoring()591 void StopGamepadMonitoring() {
592 AssertIsOnBackgroundThread();
593 if (!gService) {
594 return;
595 }
596
597 // Calling Shutdown() will delete gService as well
598 gService->Shutdown();
599 }
600
601 } // namespace dom
602 } // namespace mozilla
603