diff options
Diffstat (limited to 'dom/gamepad/cocoa')
-rw-r--r-- | dom/gamepad/cocoa/CocoaGamepad.cpp | 589 |
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 |