bug 690937 - Add XInput support to the Windows gamepad backend. r=jimm
authorTed Mielczarek <ted@mielczarek.org>
Wed, 16 Apr 2014 19:43:39 -0400
changeset 181095 2462b87f45bbe13836511cdfe7de387d4f1406ad
parent 181094 79c804f8760d3d0a54fc3dfcaed9f7cc194f4598
child 181096 c0c933d36ddd7960d79b09d32526b2f1fc5c0085
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersjimm
bugs690937
milestone32.0a1
bug 690937 - Add XInput support to the Windows gamepad backend. r=jimm
dom/gamepad/Gamepad.h
hal/windows/WindowsGamepad.cpp
--- a/dom/gamepad/Gamepad.h
+++ b/dom/gamepad/Gamepad.h
@@ -17,16 +17,29 @@ namespace mozilla {
 namespace dom {
 
 enum GamepadMappingType
 {
   NoMapping = 0,
   StandardMapping = 1
 };
 
+// Per spec:
+// https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html#remapping
+const int kStandardGamepadButtons = 17;
+const int kStandardGamepadAxes = 4;
+
+const int kButtonLeftTrigger = 6;
+const int kButtonRightTrigger = 7;
+
+const int kLeftStickXAxis = 0;
+const int kLeftStickYAxis = 1;
+const int kRightStickXAxis = 2;
+const int kRightStickYAxis = 3;
+
 class Gamepad : public nsISupports,
                 public nsWrapperCache
 {
 public:
   Gamepad(nsISupports* aParent,
           const nsAString& aID, uint32_t aIndex,
           GamepadMappingType aMapping,
           uint32_t aNumButtons, uint32_t aNumAxes);
--- a/hal/windows/WindowsGamepad.cpp
+++ b/hal/windows/WindowsGamepad.cpp
@@ -7,29 +7,30 @@
 #include <cstddef>
 
 #ifndef UNICODE
 #define UNICODE
 #endif
 #include <windows.h>
 #include <hidsdi.h>
 #include <stdio.h>
+#include <Xinput.h>
 
 #include "nsIComponentManager.h"
 #include "nsIObserver.h"
 #include "nsIObserverService.h"
 #include "nsITimer.h"
 #include "nsTArray.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/dom/GamepadService.h"
 #include "mozilla/Services.h"
 
 namespace {
 
-using mozilla::dom::GamepadService;
+using namespace mozilla::dom;
 using mozilla::ArrayLength;
 
 // USB HID usage tables, page 1 (Hat switch)
 const unsigned kUsageDpad = 0x39;
 // USB HID usage tables, page 1, 0x30 = X
 const unsigned kFirstAxis = 0x30;
 
 // USB HID usage tables
@@ -40,40 +41,75 @@ const unsigned kButtonUsagePage = 0x9;
 const unsigned kMaxButtons = 32;
 const unsigned kMaxAxes = 32;
 
 // Multiple devices-changed notifications can be sent when a device
 // is connected, because USB devices consist of multiple logical devices.
 // Therefore, we wait a bit after receiving one before looking for
 // device changes.
 const uint32_t kDevicesChangedStableDelay = 200;
+// XInput is a purely polling-driven API, so we need to
+// poll it periodically. 50ms is arbitrarily chosen.
+const uint32_t kXInputPollInterval = 50;
+
+#ifndef XUSER_MAX_COUNT
+#define XUSER_MAX_COUNT 4
+#endif
 
 const struct {
   int usagePage;
   int usage;
 } kUsagePages[] = {
   // USB HID usage tables, page 1
   { kDesktopUsagePage, 4 },  // Joystick
   { kDesktopUsagePage, 5 }   // Gamepad
 };
 
+const struct {
+  WORD button;
+  int mapped;
+} kXIButtonMap[] = {
+  { XINPUT_GAMEPAD_DPAD_UP, 12 },
+  { XINPUT_GAMEPAD_DPAD_DOWN, 13 },
+  { XINPUT_GAMEPAD_DPAD_LEFT, 14 },
+  { XINPUT_GAMEPAD_DPAD_RIGHT, 15 },
+  { XINPUT_GAMEPAD_START, 9 },
+  { XINPUT_GAMEPAD_BACK, 8 },
+  { XINPUT_GAMEPAD_LEFT_THUMB, 10 },
+  { XINPUT_GAMEPAD_RIGHT_THUMB, 11 },
+  { XINPUT_GAMEPAD_LEFT_SHOULDER, 4 },
+  { XINPUT_GAMEPAD_RIGHT_SHOULDER, 5 },
+  { XINPUT_GAMEPAD_A, 0 },
+  { XINPUT_GAMEPAD_B, 1 },
+  { XINPUT_GAMEPAD_X, 2 },
+  { XINPUT_GAMEPAD_Y, 3 }
+};
+const size_t kNumMappings = ArrayLength(kXIButtonMap);
+
 enum GamepadType {
   kNoGamepad = 0,
-  kRawInputGamepad
+  kRawInputGamepad,
+  kXInputGamepad
 };
 
 class WindowsGamepadService;
 WindowsGamepadService* gService = nullptr;
 
 struct Gamepad {
   GamepadType type;
 
   // Handle to raw input device
   HANDLE handle;
 
+  // XInput Index of the user's controller. Passed to XInputGetState.
+  DWORD userIndex;
+
+  // Last-known state of the controller.
+  XINPUT_STATE state;
+
   // ID from the GamepadService, also used as the index into
   // WindowsGamepadService::mGamepads.
   int id;
 
   // Information about the physical device.
   unsigned numAxes;
   unsigned numButtons;
   bool hasDpad;
@@ -84,16 +120,65 @@ struct Gamepad {
     HIDP_VALUE_CAPS caps;
     double value;
   } axes[kMaxAxes];
 
   // Used during rescan to find devices that were disconnected.
   bool present;
 };
 
+// Drop this in favor of decltype when we require a new enough SDK.
+typedef void (WINAPI *XInputEnable_func)(BOOL);
+
+// RAII class to wrap loading the XInput DLL
+class XInputLoader {
+public:
+  XInputLoader() : module(nullptr),
+                   mXInputEnable(nullptr),
+                   mXInputGetState(nullptr) {
+    // xinput1_4.dll exists on Windows 8
+    // xinput9_1_0.dll exists on Windows 7 and Vista
+    // xinput1_3.dll shipped with the DirectX SDK
+    const wchar_t* dlls[] = {L"xinput1_4.dll",
+                             L"xinput9_1_0.dll",
+                             L"xinput1_3.dll"};
+    const size_t kNumDLLs = ArrayLength(dlls);
+    for (size_t i = 0; i < kNumDLLs; ++i) {
+      module = LoadLibraryW(dlls[i]);
+      if (module) {
+        mXInputEnable = reinterpret_cast<XInputEnable_func>(
+         GetProcAddress(module, "XInputEnable"));
+        mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>(
+         GetProcAddress(module, "XInputGetState"));
+        if (mXInputEnable) {
+          mXInputEnable(TRUE);
+        }
+        break;
+      }
+    }
+  }
+
+  ~XInputLoader() {
+    //mXInputEnable = nullptr;
+    mXInputGetState = nullptr;
+
+    if (module) {
+      FreeLibrary(module);
+    }
+  }
+
+  operator bool() {
+    return module && mXInputGetState;
+  }
+
+  HMODULE module;
+  decltype(XInputGetState) *mXInputGetState;
+  XInputEnable_func mXInputEnable;
+};
+
 bool
 GetPreparsedData(HANDLE handle, nsTArray<uint8_t>& data)
 {
   UINT size;
   if (GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, nullptr, &size) < 0) {
     return false;
   }
   data.SetLength(size);
@@ -288,31 +373,44 @@ public:
   void Shutdown();
   // Parse gamepad input from a WM_INPUT message.
   bool HandleRawInput(HRAWINPUT handle);
 
 private:
   void ScanForDevices();
   // Look for connected raw input devices.
   void ScanForRawInputDevices();
+  // Look for connected XInput devices.
+  bool ScanForXInputDevices();
+  bool HaveXInputGamepad(int userIndex);
+
+  // Timer callback for XInput polling
+  static void XInputPollTimerCallback(nsITimer* aTimer, void* aClosure);
+  void PollXInput();
+  void CheckXInputChanges(Gamepad& gamepad, XINPUT_STATE& state);
+
   // Get information about a raw input gamepad.
   bool GetRawGamepad(HANDLE handle);
   void Cleanup();
 
   // List of connected devices.
   nsTArray<Gamepad> mGamepads;
 
   nsRefPtr<Observer> mObserver;
+  nsCOMPtr<nsITimer> mXInputPollTimer;
 
   HIDLoader mHID;
+  XInputLoader mXInput;
 };
 
 
 WindowsGamepadService::WindowsGamepadService()
 {
+  nsresult rv;
+  mXInputPollTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
   mObserver = new Observer(*this);
 }
 
 void
 WindowsGamepadService::ScanForRawInputDevices()
 {
   if (!mHID) {
     return;
@@ -332,40 +430,180 @@ WindowsGamepadService::ScanForRawInputDe
 
   for (unsigned i = 0; i < devices.Length(); i++) {
     if (devices[i].dwType == RIM_TYPEHID) {
       GetRawGamepad(devices[i].hDevice);
     }
   }
 }
 
+bool
+WindowsGamepadService::HaveXInputGamepad(int userIndex)
+{
+  for (unsigned int i = 0; i < mGamepads.Length(); i++) {
+    if (mGamepads[i].type == kXInputGamepad
+        && mGamepads[i].userIndex == userIndex) {
+      mGamepads[i].present = true;
+      return true;
+    }
+  }
+  return false;
+}
+
+bool
+WindowsGamepadService::ScanForXInputDevices()
+{
+  MOZ_ASSERT(mXInput, "XInput should be present!");
+
+  nsRefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
+  if (!gamepadsvc) {
+    return false;
+  }
+
+  bool found = false;
+  for (int i = 0; i < XUSER_MAX_COUNT; i++) {
+    XINPUT_STATE state = {};
+    if (mXInput.mXInputGetState(i, &state) != ERROR_SUCCESS) {
+      continue;
+    }
+    found = true;
+    // See if this device is already present in our list.
+    if (HaveXInputGamepad(i)) {
+      continue;
+    }
+
+    // Not already present, add it.
+    Gamepad gamepad = {};
+    gamepad.type = kXInputGamepad;
+    gamepad.present = true;
+    gamepad.state = state;
+    gamepad.userIndex = i;
+    gamepad.numButtons = kStandardGamepadButtons;
+    gamepad.numAxes = kStandardGamepadAxes;
+    gamepad.id = gamepadsvc->AddGamepad("xinput",
+                                        StandardMapping,
+                                        kStandardGamepadButtons,
+                                        kStandardGamepadAxes);
+    mGamepads.AppendElement(gamepad);
+  }
+
+  return found;
+}
+
 void
 WindowsGamepadService::ScanForDevices()
 {
   for (int i = mGamepads.Length() - 1; i >= 0; i--) {
     mGamepads[i].present = false;
   }
 
   if (mHID) {
     ScanForRawInputDevices();
   }
+  if (mXInput) {
+    mXInputPollTimer->Cancel();
+    if (ScanForXInputDevices()) {
+      mXInputPollTimer->InitWithFuncCallback(XInputPollTimerCallback,
+                                             this,
+                                             kXInputPollInterval,
+                                             nsITimer::TYPE_REPEATING_SLACK);
+    }
+  }
 
   nsRefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
   if (!gamepadsvc) {
     return;
   }
   // Look for devices that are no longer present and remove them.
   for (int i = mGamepads.Length() - 1; i >= 0; i--) {
     if (!mGamepads[i].present) {
       gamepadsvc->RemoveGamepad(mGamepads[i].id);
       mGamepads.RemoveElementAt(i);
     }
   }
 }
 
+// static
+void
+WindowsGamepadService::XInputPollTimerCallback(nsITimer* aTimer,
+                                               void* aClosure)
+{
+  WindowsGamepadService* self =
+    reinterpret_cast<WindowsGamepadService*>(aClosure);
+  self->PollXInput();
+}
+
+void
+WindowsGamepadService::PollXInput()
+{
+  for (unsigned int i = 0; i < mGamepads.Length(); i++) {
+    if (mGamepads[i].type != kXInputGamepad) {
+      continue;
+    }
+
+    XINPUT_STATE state = {};
+    DWORD res = mXInput.mXInputGetState(mGamepads[i].userIndex, &state);
+    if (res == ERROR_SUCCESS
+        && state.dwPacketNumber != mGamepads[i].state.dwPacketNumber) {
+        CheckXInputChanges(mGamepads[i], state);
+    }
+  }
+}
+
+void WindowsGamepadService::CheckXInputChanges(Gamepad& gamepad,
+                                               XINPUT_STATE& state) {
+  nsRefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
+  // Handle digital buttons first
+  for (size_t b = 0; b < kNumMappings; b++) {
+    if (state.Gamepad.wButtons & kXIButtonMap[b].button &&
+        !(gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button)) {
+      // Button pressed
+      gamepadsvc->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, true);
+    } else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) &&
+               gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) {
+      // Button released
+      gamepadsvc->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, false);
+    }
+  }
+
+  // Then triggers
+  if (state.Gamepad.bLeftTrigger != gamepad.state.Gamepad.bLeftTrigger) {
+    bool pressed =
+      state.Gamepad.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
+    gamepadsvc->NewButtonEvent(gamepad.id, kButtonLeftTrigger,
+                               pressed, state.Gamepad.bLeftTrigger / 255.0);
+  }
+  if (state.Gamepad.bRightTrigger != gamepad.state.Gamepad.bRightTrigger) {
+    bool pressed =
+      state.Gamepad.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
+    gamepadsvc->NewButtonEvent(gamepad.id, kButtonRightTrigger,
+                               pressed, state.Gamepad.bRightTrigger / 255.0);
+  }
+
+  // Finally deal with analog sticks
+  // TODO: bug 1001955 - Support deadzones.
+  if (state.Gamepad.sThumbLX != gamepad.state.Gamepad.sThumbLX) {
+    gamepadsvc->NewAxisMoveEvent(gamepad.id, kLeftStickXAxis,
+                                 state.Gamepad.sThumbLX / 32767.0);
+  }
+  if (state.Gamepad.sThumbLY != gamepad.state.Gamepad.sThumbLY) {
+    gamepadsvc->NewAxisMoveEvent(gamepad.id, kLeftStickYAxis,
+                                 -1.0 * state.Gamepad.sThumbLY / 32767.0);
+  }
+  if (state.Gamepad.sThumbRX != gamepad.state.Gamepad.sThumbRX) {
+    gamepadsvc->NewAxisMoveEvent(gamepad.id, kRightStickXAxis,
+                                 state.Gamepad.sThumbRX / 32767.0);
+  }
+  if (state.Gamepad.sThumbRY != gamepad.state.Gamepad.sThumbRY) {
+    gamepadsvc->NewAxisMoveEvent(gamepad.id, kRightStickYAxis,
+                                 -1.0 * state.Gamepad.sThumbRY / 32767.0);
+  }
+  gamepad.state = state;
+}
+
 // Used to sort a list of axes by HID usage.
 class HidValueComparator {
 public:
   bool Equals(const HIDP_VALUE_CAPS& c1, const HIDP_VALUE_CAPS& c2) const
   {
     return c1.UsagePage == c2.UsagePage && c1.Range.UsageMin == c2.Range.UsageMin;
   }
   bool LessThan(const HIDP_VALUE_CAPS& c1, const HIDP_VALUE_CAPS& c2) const
@@ -519,17 +757,17 @@ WindowsGamepadService::GetRawGamepad(HAN
   gamepad.present = true;
 
   nsRefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
   if (!gamepadsvc) {
     return false;
   }
 
   gamepad.id = gamepadsvc->AddGamepad(gamepad_id,
-                                      mozilla::dom::NoMapping,
+                                      NoMapping,
                                       gamepad.numButtons,
                                       gamepad.numAxes);
   mGamepads.AppendElement(gamepad);
   return true;
 }
 
 bool
 WindowsGamepadService::HandleRawInput(HRAWINPUT handle)
@@ -651,16 +889,19 @@ void
 WindowsGamepadService::Shutdown()
 {
   Cleanup();
 }
 
 void
 WindowsGamepadService::Cleanup()
 {
+  if (mXInputPollTimer) {
+    mXInputPollTimer->Cancel();
+  }
   mGamepads.Clear();
 }
 
 void
 WindowsGamepadService::DevicesChanged(DeviceChangeType type)
 {
   if (type == DeviceChangeNotification) {
     mObserver->SetDeviceChangeTimer();