Bug 1107801 - Improve gamepad support on MacOS. r=ted
☠☠ backed out by 812ea3cdd997 ☠ ☠
authorRandall Barker <rbarker@mozilla.com>
Mon, 23 Mar 2015 09:21:00 -0400
changeset 265440 03eeae95700809d6bbbee9120ff1f31ef9b1751d
parent 265439 0d4e6b99f0c4ab87d0362ba6a17849403782fb6b
child 265441 b6b20e7ca1189b93d0c68d25c9c159f19fee60f9
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted
bugs1107801
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1107801 - Improve gamepad support on MacOS. r=ted
hal/cocoa/CocoaGamepad.cpp
--- a/hal/cocoa/CocoaGamepad.cpp
+++ b/hal/cocoa/CocoaGamepad.cpp
@@ -18,105 +18,184 @@
 namespace {
 
 using mozilla::dom::GamepadService;
 
 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
-#define GENERIC_DESKTOP_USAGE_PAGE 0x01
-#define JOYSTICK_USAGE_NUMBER 0x04
-#define GAMEPAD_USAGE_NUMBER 0x05
-#define AXIS_MIN_USAGE_NUMBER 0x30
-#define AXIS_MAX_USAGE_NUMBER 0x35
-#define BUTTON_USAGE_PAGE 0x09
+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;
-  vector<Button> buttons;
-  vector<Axis> axes;
+  nsTArray<Button> buttons;
+  nsTArray<Axis> axes;
+  IOHIDElementRef mDpad;
+  dpad_buttons mDpadState;
 
  public:
-  Gamepad() : mDevice(nullptr), mSuperIndex(-1) {}
+  Gamepad() : mDevice(nullptr), mDpad(nullptr), mSuperIndex(-1) {}
   bool operator==(IOHIDDeviceRef device) const { return mDevice == device; }
   bool empty() const { return mDevice == nullptr; }
-  void clear() {
+  void clear()
+  {
     mDevice = nullptr;
-    buttons.clear();
-    axes.clear();
+    buttons.Clear();
+    axes.Clear();
+    mDpad = nullptr;
     mSuperIndex = -1;
   }
   void init(IOHIDDeviceRef device);
-  size_t numButtons() { return buttons.size(); }
-  size_t numAxes() { return axes.size(); }
+  size_t numButtons() { return buttons.Length() + (mDpad ? 4 : 0); }
+  size_t numAxes() { return axes.Length(); }
 
   // Index given by our superclass.
   uint32_t mSuperIndex;
 
-  const Button* lookupButton(IOHIDElementRef element) const {
-    for (size_t i = 0; i < buttons.size(); i++) {
+  const 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 (size_t i = 0; i < axes.size(); i++) {
+  const Axis* lookupAxis(IOHIDElementRef element) const
+  {
+    for (unsigned i = 0; i < axes.Length(); i++) {
       if (axes[i].element == element)
         return &axes[i];
     }
     return nullptr;
   }
 };
 
-void Gamepad::init(IOHIDDeviceRef device) {
+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 == GENERIC_DESKTOP_USAGE_PAGE &&
-        usage >= AXIS_MIN_USAGE_NUMBER &&
-        usage <= AXIS_MAX_USAGE_NUMBER)
+    if (usagePage == kDesktopUsagePage &&
+        usage >= kAxisUsageMin &&
+        usage <= kAxisUsageMax)
     {
-      Axis axis = { int(axes.size()),
+      Axis axis = { int(axes.Length()),
                     element,
+                    usagePage,
+                    usage,
                     IOHIDElementGetLogicalMin(element),
                     IOHIDElementGetLogicalMax(element) };
-      axes.push_back(axis);
-    } else if (usagePage == BUTTON_USAGE_PAGE) {
-      Button button = { int(usage) - 1, element };
-      buttons.push_back(button);
+      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;
 
   static void DeviceAddedCallback(void* data, IOReturn result,
@@ -184,33 +263,100 @@ DarwinGamepadService::DeviceRemoved(IOHI
     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)
 {
+  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;
+  }
   nsRefPtr<GamepadService> service(GamepadService::GetService());
   IOHIDElementRef element = IOHIDValueGetElement(value);
   IOHIDDeviceRef device = IOHIDElementGetDevice(element);
-  for (size_t i = 0; i < mGamepads.size(); i++) {
-    const Gamepad &gamepad = mGamepads[i];
+  for (unsigned i = 0; i < mGamepads.size(); i++) {
+    Gamepad &gamepad = mGamepads[i];
     if (gamepad == device) {
-      if (const Axis* axis = gamepad.lookupAxis(element)) {
+      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(i, 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(i, axis->id, v);
       } else if (const Button* button = gamepad.lookupButton(element)) {
-        bool pressed = IOHIDValueGetIntegerValue(value) != 0;
-        service->NewButtonEvent(i, button->id, pressed);
+        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(i, button->id, pressed, v);
       }
       return;
     }
   }
 }
 
 void
 DarwinGamepadService::DeviceAddedCallback(void* data, IOReturn result,
@@ -281,25 +427,25 @@ void DarwinGamepadService::Startup()
 {
   if (mManager != nullptr)
     return;
 
   IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault,
                                                kIOHIDOptionsTypeNone);
 
   CFMutableDictionaryRef criteria_arr[2];
-  criteria_arr[0] = MatchingDictionary(GENERIC_DESKTOP_USAGE_PAGE,
-                                       JOYSTICK_USAGE_NUMBER);
+  criteria_arr[0] = MatchingDictionary(kDesktopUsagePage,
+                                       kJoystickUsage);
   if (!criteria_arr[0]) {
     CFRelease(manager);
     return;
   }
 
-  criteria_arr[1] = MatchingDictionary(GENERIC_DESKTOP_USAGE_PAGE,
-                                       GAMEPAD_USAGE_NUMBER);
+  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);