summaryrefslogtreecommitdiffstats
path: root/dom/gamepad/cocoa
diff options
context:
space:
mode:
Diffstat (limited to 'dom/gamepad/cocoa')
-rw-r--r--dom/gamepad/cocoa/CocoaGamepad.cpp589
1 files changed, 589 insertions, 0 deletions
diff --git a/dom/gamepad/cocoa/CocoaGamepad.cpp b/dom/gamepad/cocoa/CocoaGamepad.cpp
new file mode 100644
index 000000000..e7c986e22
--- /dev/null
+++ b/dom/gamepad/cocoa/CocoaGamepad.cpp
@@ -0,0 +1,589 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// mostly derived from the Allegro source code at:
+// http://alleg.svn.sourceforge.net/viewvc/alleg/allegro/branches/4.9/src/macosx/hidjoy.m?revision=13760&view=markup
+
+#include "mozilla/dom/GamepadPlatformService.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsThreadUtils.h"
+#include <CoreFoundation/CoreFoundation.h>
+#include <IOKit/hid/IOHIDBase.h>
+#include <IOKit/hid/IOHIDKeys.h>
+#include <IOKit/hid/IOHIDManager.h>
+
+#include <stdio.h>
+#include <vector>
+
+namespace {
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using std::vector;
+
+struct Button {
+ int id;
+ bool analog;
+ IOHIDElementRef element;
+ CFIndex min;
+ CFIndex max;
+
+ Button(int aId, IOHIDElementRef aElement, CFIndex aMin, CFIndex aMax) :
+ id(aId),
+ analog((aMax - aMin) > 1),
+ element(aElement),
+ min(aMin),
+ max(aMax) {}
+};
+
+struct Axis {
+ int id;
+ IOHIDElementRef element;
+ uint32_t usagePage;
+ uint32_t usage;
+ CFIndex min;
+ CFIndex max;
+};
+
+typedef bool dpad_buttons[4];
+
+// These values can be found in the USB HID Usage Tables:
+// http://www.usb.org/developers/hidpage
+const unsigned kDesktopUsagePage = 0x01;
+const unsigned kSimUsagePage = 0x02;
+const unsigned kAcceleratorUsage = 0xC4;
+const unsigned kBrakeUsage = 0xC5;
+const unsigned kJoystickUsage = 0x04;
+const unsigned kGamepadUsage = 0x05;
+const unsigned kAxisUsageMin = 0x30;
+const unsigned kAxisUsageMax = 0x35;
+const unsigned kDpadUsage = 0x39;
+const unsigned kButtonUsagePage = 0x09;
+const unsigned kConsumerPage = 0x0C;
+const unsigned kHomeUsage = 0x223;
+const unsigned kBackUsage = 0x224;
+
+
+class Gamepad {
+ private:
+ IOHIDDeviceRef mDevice;
+ nsTArray<Button> buttons;
+ nsTArray<Axis> axes;
+ IOHIDElementRef mDpad;
+ dpad_buttons mDpadState;
+
+ public:
+ Gamepad() : mDevice(nullptr), mDpad(nullptr), mSuperIndex(-1) {}
+ bool operator==(IOHIDDeviceRef device) const { return mDevice == device; }
+ bool empty() const { return mDevice == nullptr; }
+ void clear()
+ {
+ mDevice = nullptr;
+ buttons.Clear();
+ axes.Clear();
+ mDpad = nullptr;
+ mSuperIndex = -1;
+ }
+ void init(IOHIDDeviceRef device);
+ size_t numButtons() { return buttons.Length() + (mDpad ? 4 : 0); }
+ size_t numAxes() { return axes.Length(); }
+
+ // Index given by our superclass.
+ uint32_t mSuperIndex;
+
+ bool isDpad(IOHIDElementRef element) const
+ {
+ return element == mDpad;
+ }
+
+ const dpad_buttons& getDpadState() const
+ {
+ return mDpadState;
+ }
+
+ void setDpadState(const dpad_buttons& dpadState)
+ {
+ for (unsigned i = 0; i < ArrayLength(mDpadState); i++) {
+ mDpadState[i] = dpadState[i];
+ }
+ }
+
+ const Button* lookupButton(IOHIDElementRef element) const
+ {
+ for (unsigned i = 0; i < buttons.Length(); i++) {
+ if (buttons[i].element == element)
+ return &buttons[i];
+ }
+ return nullptr;
+ }
+
+ const Axis* lookupAxis(IOHIDElementRef element) const
+ {
+ for (unsigned i = 0; i < axes.Length(); i++) {
+ if (axes[i].element == element)
+ return &axes[i];
+ }
+ return nullptr;
+ }
+};
+
+class AxisComparator {
+public:
+ bool Equals(const Axis& a1, const Axis& a2) const
+ {
+ return a1.usagePage == a2.usagePage && a1.usage == a2.usage;
+ }
+ bool LessThan(const Axis& a1, const Axis& a2) const
+ {
+ if (a1.usagePage == a2.usagePage) {
+ return a1.usage < a2.usage;
+ }
+ return a1.usagePage < a2.usagePage;
+ }
+};
+
+void Gamepad::init(IOHIDDeviceRef device)
+{
+ clear();
+ mDevice = device;
+
+ CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device,
+ nullptr,
+ kIOHIDOptionsTypeNone);
+ CFIndex n = CFArrayGetCount(elements);
+ for (CFIndex i = 0; i < n; i++) {
+ IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements,
+ i);
+ uint32_t usagePage = IOHIDElementGetUsagePage(element);
+ uint32_t usage = IOHIDElementGetUsage(element);
+
+ if (usagePage == kDesktopUsagePage &&
+ usage >= kAxisUsageMin &&
+ usage <= kAxisUsageMax)
+ {
+ Axis axis = { int(axes.Length()),
+ element,
+ usagePage,
+ usage,
+ IOHIDElementGetLogicalMin(element),
+ IOHIDElementGetLogicalMax(element) };
+ axes.AppendElement(axis);
+ } else if (usagePage == kDesktopUsagePage && usage == kDpadUsage &&
+ // Don't know how to handle d-pads that return weird values.
+ IOHIDElementGetLogicalMax(element) - IOHIDElementGetLogicalMin(element) == 7) {
+ mDpad = element;
+ } else if ((usagePage == kSimUsagePage &&
+ (usage == kAcceleratorUsage ||
+ usage == kBrakeUsage)) ||
+ (usagePage == kButtonUsagePage) ||
+ (usagePage == kConsumerPage &&
+ (usage == kHomeUsage ||
+ usage == kBackUsage))) {
+ Button button(int(buttons.Length()), element, IOHIDElementGetLogicalMin(element), IOHIDElementGetLogicalMax(element));
+ buttons.AppendElement(button);
+ } else {
+ //TODO: handle other usage pages
+ }
+ }
+
+ AxisComparator comparator;
+ axes.Sort(comparator);
+ for (unsigned i = 0; i < axes.Length(); i++) {
+ axes[i].id = i;
+ }
+}
+
+class DarwinGamepadService {
+ private:
+ IOHIDManagerRef mManager;
+ vector<Gamepad> mGamepads;
+
+ //Workaround to support running in background thread
+ CFRunLoopRef mMonitorRunLoop;
+ nsCOMPtr<nsIThread> mMonitorThread;
+
+ static void DeviceAddedCallback(void* data, IOReturn result,
+ void* sender, IOHIDDeviceRef device);
+ static void DeviceRemovedCallback(void* data, IOReturn result,
+ void* sender, IOHIDDeviceRef device);
+ static void InputValueChangedCallback(void* data, IOReturn result,
+ void* sender, IOHIDValueRef newValue);
+
+ void DeviceAdded(IOHIDDeviceRef device);
+ void DeviceRemoved(IOHIDDeviceRef device);
+ void InputValueChanged(IOHIDValueRef value);
+ void StartupInternal();
+
+ public:
+ DarwinGamepadService();
+ ~DarwinGamepadService();
+ void Startup();
+ void Shutdown();
+ friend class DarwinGamepadServiceStartupRunnable;
+};
+
+class DarwinGamepadServiceStartupRunnable final : public Runnable
+{
+ private:
+ ~DarwinGamepadServiceStartupRunnable() {}
+ // This Runnable schedules startup of DarwinGamepadService
+ // in a new thread, pointer to DarwinGamepadService is only
+ // used by this Runnable within its thread.
+ DarwinGamepadService MOZ_NON_OWNING_REF *mService;
+ public:
+ explicit DarwinGamepadServiceStartupRunnable(DarwinGamepadService *service)
+ : mService(service) {}
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(mService);
+ mService->StartupInternal();
+ return NS_OK;
+ }
+};
+
+void
+DarwinGamepadService::DeviceAdded(IOHIDDeviceRef device)
+{
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return;
+ }
+
+ size_t slot = size_t(-1);
+ for (size_t i = 0; i < mGamepads.size(); i++) {
+ if (mGamepads[i] == device)
+ return;
+ if (slot == size_t(-1) && mGamepads[i].empty())
+ slot = i;
+ }
+
+ if (slot == size_t(-1)) {
+ slot = mGamepads.size();
+ mGamepads.push_back(Gamepad());
+ }
+ mGamepads[slot].init(device);
+
+ // Gather some identifying information
+ CFNumberRef vendorIdRef =
+ (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey));
+ CFNumberRef productIdRef =
+ (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey));
+ CFStringRef productRef =
+ (CFStringRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
+ int vendorId, productId;
+ CFNumberGetValue(vendorIdRef, kCFNumberIntType, &vendorId);
+ CFNumberGetValue(productIdRef, kCFNumberIntType, &productId);
+ char product_name[128];
+ CFStringGetCString(productRef, product_name,
+ sizeof(product_name), kCFStringEncodingASCII);
+ char buffer[256];
+ sprintf(buffer, "%x-%x-%s", vendorId, productId, product_name);
+ uint32_t index = service->AddGamepad(buffer,
+ mozilla::dom::GamepadMappingType::_empty,
+ (int)mGamepads[slot].numButtons(),
+ (int)mGamepads[slot].numAxes());
+ mGamepads[slot].mSuperIndex = index;
+}
+
+void
+DarwinGamepadService::DeviceRemoved(IOHIDDeviceRef device)
+{
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return;
+ }
+ for (size_t i = 0; i < mGamepads.size(); i++) {
+ if (mGamepads[i] == device) {
+ service->RemoveGamepad(mGamepads[i].mSuperIndex);
+ mGamepads[i].clear();
+ return;
+ }
+ }
+}
+
+/*
+ * Given a value from a d-pad (POV hat in USB HID terminology),
+ * represent it as 4 buttons, one for each cardinal direction.
+ */
+static void
+UnpackDpad(int dpad_value, int min, int max, dpad_buttons& buttons)
+{
+ const unsigned kUp = 0;
+ const unsigned kDown = 1;
+ const unsigned kLeft = 2;
+ const unsigned kRight = 3;
+
+ // Different controllers have different ways of representing
+ // "nothing is pressed", but they're all outside the range of values.
+ if (dpad_value < min || dpad_value > max) {
+ // Nothing is pressed.
+ return;
+ }
+
+ // Normalize value to start at 0.
+ int value = dpad_value - min;
+
+ // Value will be in the range 0-7. The value represents the
+ // position of the d-pad around a circle, with 0 being straight up,
+ // 2 being right, 4 being straight down, and 6 being left.
+ if (value < 2 || value > 6) {
+ buttons[kUp] = true;
+ }
+ if (value > 2 && value < 6) {
+ buttons[kDown] = true;
+ }
+ if (value > 4) {
+ buttons[kLeft] = true;
+ }
+ if (value > 0 && value < 4) {
+ buttons[kRight] = true;
+ }
+}
+
+void
+DarwinGamepadService::InputValueChanged(IOHIDValueRef value)
+{
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return;
+ }
+
+ uint32_t value_length = IOHIDValueGetLength(value);
+ if (value_length > 4) {
+ // Workaround for bizarre issue with PS3 controllers that try to return
+ // massive (30+ byte) values and crash IOHIDValueGetIntegerValue
+ return;
+ }
+ IOHIDElementRef element = IOHIDValueGetElement(value);
+ IOHIDDeviceRef device = IOHIDElementGetDevice(element);
+
+ for (unsigned i = 0; i < mGamepads.size(); i++) {
+ Gamepad &gamepad = mGamepads[i];
+ if (gamepad == device) {
+ if (gamepad.isDpad(element)) {
+ const dpad_buttons& oldState = gamepad.getDpadState();
+ dpad_buttons newState = { false, false, false, false };
+ UnpackDpad(IOHIDValueGetIntegerValue(value),
+ IOHIDElementGetLogicalMin(element),
+ IOHIDElementGetLogicalMax(element),
+ newState);
+ const int numButtons = gamepad.numButtons();
+ for (unsigned b = 0; b < ArrayLength(newState); b++) {
+ if (newState[b] != oldState[b]) {
+ service->NewButtonEvent(gamepad.mSuperIndex,
+ numButtons - 4 + b,
+ newState[b]);
+ }
+ }
+ gamepad.setDpadState(newState);
+ } else if (const Axis* axis = gamepad.lookupAxis(element)) {
+ double d = IOHIDValueGetIntegerValue(value);
+ double v = 2.0f * (d - axis->min) /
+ (double)(axis->max - axis->min) - 1.0f;
+ service->NewAxisMoveEvent(gamepad.mSuperIndex, axis->id, v);
+ } else if (const Button* button = gamepad.lookupButton(element)) {
+ int iv = IOHIDValueGetIntegerValue(value);
+ bool pressed = iv != 0;
+ double v = 0;
+ if (button->analog) {
+ double dv = iv;
+ v = (dv - button->min) / (double)(button->max - button->min);
+ } else {
+ v = pressed ? 1.0 : 0.0;
+ }
+ service->NewButtonEvent(gamepad.mSuperIndex, button->id, pressed, v);
+ }
+ return;
+ }
+ }
+}
+
+void
+DarwinGamepadService::DeviceAddedCallback(void* data, IOReturn result,
+ void* sender, IOHIDDeviceRef device)
+{
+ DarwinGamepadService* service = (DarwinGamepadService*)data;
+ service->DeviceAdded(device);
+}
+
+void
+DarwinGamepadService::DeviceRemovedCallback(void* data, IOReturn result,
+ void* sender, IOHIDDeviceRef device)
+{
+ DarwinGamepadService* service = (DarwinGamepadService*)data;
+ service->DeviceRemoved(device);
+}
+
+void
+DarwinGamepadService::InputValueChangedCallback(void* data,
+ IOReturn result,
+ void* sender,
+ IOHIDValueRef newValue)
+{
+ DarwinGamepadService* service = (DarwinGamepadService*)data;
+ service->InputValueChanged(newValue);
+}
+
+static CFMutableDictionaryRef
+MatchingDictionary(UInt32 inUsagePage, UInt32 inUsage)
+{
+ CFMutableDictionaryRef dict =
+ CFDictionaryCreateMutable(kCFAllocatorDefault,
+ 0,
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+ if (!dict)
+ return nullptr;
+ CFNumberRef number = CFNumberCreate(kCFAllocatorDefault,
+ kCFNumberIntType,
+ &inUsagePage);
+ if (!number) {
+ CFRelease(dict);
+ return nullptr;
+ }
+ CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsagePageKey), number);
+ CFRelease(number);
+
+ number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &inUsage);
+ if (!number) {
+ CFRelease(dict);
+ return nullptr;
+ }
+ CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsageKey), number);
+ CFRelease(number);
+
+ return dict;
+}
+
+DarwinGamepadService::DarwinGamepadService() : mManager(nullptr) {}
+
+DarwinGamepadService::~DarwinGamepadService()
+{
+ if (mManager != nullptr)
+ CFRelease(mManager);
+}
+
+void
+DarwinGamepadService::StartupInternal()
+{
+ if (mManager != nullptr)
+ return;
+
+ IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault,
+ kIOHIDOptionsTypeNone);
+
+ CFMutableDictionaryRef criteria_arr[2];
+ criteria_arr[0] = MatchingDictionary(kDesktopUsagePage,
+ kJoystickUsage);
+ if (!criteria_arr[0]) {
+ CFRelease(manager);
+ return;
+ }
+
+ criteria_arr[1] = MatchingDictionary(kDesktopUsagePage,
+ kGamepadUsage);
+ if (!criteria_arr[1]) {
+ CFRelease(criteria_arr[0]);
+ CFRelease(manager);
+ return;
+ }
+
+ CFArrayRef criteria =
+ CFArrayCreate(kCFAllocatorDefault, (const void**)criteria_arr, 2, nullptr);
+ if (!criteria) {
+ CFRelease(criteria_arr[1]);
+ CFRelease(criteria_arr[0]);
+ CFRelease(manager);
+ return;
+ }
+
+ IOHIDManagerSetDeviceMatchingMultiple(manager, criteria);
+ CFRelease(criteria);
+ CFRelease(criteria_arr[1]);
+ CFRelease(criteria_arr[0]);
+
+ IOHIDManagerRegisterDeviceMatchingCallback(manager,
+ DeviceAddedCallback,
+ this);
+ IOHIDManagerRegisterDeviceRemovalCallback(manager,
+ DeviceRemovedCallback,
+ this);
+ IOHIDManagerRegisterInputValueCallback(manager,
+ InputValueChangedCallback,
+ this);
+ IOHIDManagerScheduleWithRunLoop(manager,
+ CFRunLoopGetCurrent(),
+ kCFRunLoopDefaultMode);
+ IOReturn rv = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone);
+ if (rv != kIOReturnSuccess) {
+ CFRelease(manager);
+ return;
+ }
+
+ mManager = manager;
+
+ // We held the handle of the CFRunLoop to make sure we
+ // can shut it down explicitly by CFRunLoopStop in another
+ // thread.
+ mMonitorRunLoop = CFRunLoopGetCurrent();
+
+ // CFRunLoopRun() is a blocking message loop when it's called in
+ // non-main thread so this thread cannot receive any other runnables
+ // and nsITimer timeout events after it's called.
+ CFRunLoopRun();
+}
+
+void DarwinGamepadService::Startup()
+{
+ Unused << NS_NewThread(getter_AddRefs(mMonitorThread),
+ new DarwinGamepadServiceStartupRunnable(this));
+}
+
+void DarwinGamepadService::Shutdown()
+{
+ IOHIDManagerRef manager = (IOHIDManagerRef)mManager;
+ CFRunLoopStop(mMonitorRunLoop);
+ if (manager) {
+ IOHIDManagerClose(manager, 0);
+ CFRelease(manager);
+ mManager = nullptr;
+ }
+ mMonitorThread->Shutdown();
+}
+
+} // namespace
+
+namespace mozilla {
+namespace dom {
+
+DarwinGamepadService* gService = nullptr;
+
+void StartGamepadMonitoring()
+{
+ if (gService) {
+ return;
+ }
+
+ gService = new DarwinGamepadService();
+ gService->Startup();
+}
+
+void StopGamepadMonitoring()
+{
+ if (!gService) {
+ return;
+ }
+
+ gService->Shutdown();
+ delete gService;
+ gService = nullptr;
+}
+
+} // namespace dom
+} // namespace mozilla