summaryrefslogtreecommitdiffstats
path: root/dom/gamepad/windows
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/gamepad/windows
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/gamepad/windows')
-rw-r--r--dom/gamepad/windows/WindowsGamepad.cpp1136
1 files changed, 1136 insertions, 0 deletions
diff --git a/dom/gamepad/windows/WindowsGamepad.cpp b/dom/gamepad/windows/WindowsGamepad.cpp
new file mode 100644
index 0000000000..d5584eaea8
--- /dev/null
+++ b/dom/gamepad/windows/WindowsGamepad.cpp
@@ -0,0 +1,1136 @@
+/* -*- 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/. */
+
+#include <algorithm>
+#include <cstddef>
+
+#ifndef UNICODE
+# define UNICODE
+#endif
+#include <windows.h>
+#include <hidsdi.h>
+#include <stdio.h>
+#include <xinput.h>
+
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+#include "nsWindowsHelpers.h"
+
+#include "mozilla/ArrayUtils.h"
+
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/dom/GamepadPlatformService.h"
+#include "mozilla/dom/GamepadRemapping.h"
+#include "Gamepad.h"
+
+namespace {
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using mozilla::ArrayLength;
+
+// USB HID usage tables, page 1, 0x30 = X
+const uint32_t kAxisMinimumUsageNumber = 0x30;
+// USB HID usage tables, page 1 (Hat switch)
+const uint32_t kAxesLengthCap = 16;
+
+// USB HID usage tables
+const uint32_t kDesktopUsagePage = 0x1;
+const uint32_t kButtonUsagePage = 0x9;
+
+// 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;
+// Both DirectInput and XInput are polling-driven here,
+// so we need to poll it periodically.
+// 4ms, or 250 Hz, is consistent with Chrome's gamepad implementation.
+const uint32_t kWindowsGamepadPollInterval = 4;
+
+const UINT kRawInputError = (UINT)-1;
+
+// In XInputGetState, we can't get the state of Xbox Guide button.
+// We need to go through the undocumented XInputGetStateEx method
+// to get that button's state.
+const LPCSTR kXInputGetStateExOrdinal = (LPCSTR)100;
+// Bitmask for the Guide button in XInputGamepadEx.wButtons.
+const int XINPUT_GAMEPAD_Guide = 0x0400;
+
+#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_Guide, 16},
+ {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, kXInputGamepad };
+
+class WindowsGamepadService;
+// This pointer holds a windows gamepad backend service,
+// it will be created and destroyed by background thread and
+// used by gMonitorThread
+WindowsGamepadService* MOZ_NON_OWNING_REF gService = nullptr;
+nsCOMPtr<nsIThread> gMonitorThread = nullptr;
+static bool sIsShutdown = false;
+
+class Gamepad {
+ public:
+ 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;
+
+ // Handle from the GamepadService
+ GamepadHandle gamepadHandle;
+
+ // Information about the physical device.
+ unsigned numAxes;
+ unsigned numButtons;
+
+ nsTArray<bool> buttons;
+ struct axisValue {
+ HIDP_VALUE_CAPS caps;
+ double value;
+ bool active;
+
+ axisValue() : value(0.0f), active(false) {}
+ explicit axisValue(const HIDP_VALUE_CAPS& aCaps)
+ : caps(aCaps), value(0.0f), active(true) {}
+ };
+ nsTArray<axisValue> axes;
+
+ RefPtr<GamepadRemapper> remapper;
+
+ // Used during rescan to find devices that were disconnected.
+ bool present;
+
+ Gamepad(uint32_t aNumAxes, uint32_t aNumButtons, GamepadType aType)
+ : type(aType), numAxes(aNumAxes), numButtons(aNumButtons), present(true) {
+ buttons.SetLength(numButtons);
+ axes.SetLength(numAxes);
+ }
+
+ private:
+ Gamepad() {}
+};
+
+// Drop this in favor of decltype when we require a new enough SDK.
+using XInputEnable_func = void(WINAPI*)(BOOL);
+
+// RAII class to wrap loading the XInput DLL
+class XInputLoader {
+ public:
+ XInputLoader()
+ : module(nullptr), mXInputGetState(nullptr), mXInputEnable(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"));
+ // Checking if `XInputGetStateEx` is available. If not,
+ // we will fallback to use `XInputGetState`.
+ mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>(
+ GetProcAddress(module, kXInputGetStateExOrdinal));
+ if (!mXInputGetState) {
+ mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>(
+ GetProcAddress(module, "XInputGetState"));
+ }
+ MOZ_ASSERT(mXInputGetState &&
+ "XInputGetState must be linked successfully.");
+
+ if (mXInputEnable) {
+ mXInputEnable(TRUE);
+ }
+ break;
+ }
+ }
+ }
+
+ ~XInputLoader() {
+ mXInputEnable = nullptr;
+ mXInputGetState = nullptr;
+
+ if (module) {
+ FreeLibrary(module);
+ }
+ }
+
+ explicit 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) ==
+ kRawInputError) {
+ return false;
+ }
+ data.SetLength(size);
+ return GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, data.Elements(),
+ &size) > 0;
+}
+
+/*
+ * Given an axis value and a minimum and maximum range,
+ * scale it to be in the range -1.0 .. 1.0.
+ */
+double ScaleAxis(ULONG value, LONG min, LONG max) {
+ return 2.0 * (value - min) / (max - min) - 1.0;
+}
+
+/*
+ * Return true if this USB HID usage page and usage are of a type we
+ * know how to handle.
+ */
+bool SupportedUsage(USHORT page, USHORT usage) {
+ for (unsigned i = 0; i < ArrayLength(kUsagePages); i++) {
+ if (page == kUsagePages[i].usagePage && usage == kUsagePages[i].usage) {
+ return true;
+ }
+ }
+ return false;
+}
+
+class HIDLoader {
+ public:
+ HIDLoader()
+ : mHidD_GetProductString(nullptr),
+ mHidP_GetCaps(nullptr),
+ mHidP_GetButtonCaps(nullptr),
+ mHidP_GetValueCaps(nullptr),
+ mHidP_GetUsages(nullptr),
+ mHidP_GetUsageValue(nullptr),
+ mHidP_GetScaledUsageValue(nullptr),
+ mModule(LoadLibraryW(L"hid.dll")) {
+ if (mModule) {
+ mHidD_GetProductString =
+ reinterpret_cast<decltype(HidD_GetProductString)*>(
+ GetProcAddress(mModule, "HidD_GetProductString"));
+ mHidP_GetCaps = reinterpret_cast<decltype(HidP_GetCaps)*>(
+ GetProcAddress(mModule, "HidP_GetCaps"));
+ mHidP_GetButtonCaps = reinterpret_cast<decltype(HidP_GetButtonCaps)*>(
+ GetProcAddress(mModule, "HidP_GetButtonCaps"));
+ mHidP_GetValueCaps = reinterpret_cast<decltype(HidP_GetValueCaps)*>(
+ GetProcAddress(mModule, "HidP_GetValueCaps"));
+ mHidP_GetUsages = reinterpret_cast<decltype(HidP_GetUsages)*>(
+ GetProcAddress(mModule, "HidP_GetUsages"));
+ mHidP_GetUsageValue = reinterpret_cast<decltype(HidP_GetUsageValue)*>(
+ GetProcAddress(mModule, "HidP_GetUsageValue"));
+ mHidP_GetScaledUsageValue =
+ reinterpret_cast<decltype(HidP_GetScaledUsageValue)*>(
+ GetProcAddress(mModule, "HidP_GetScaledUsageValue"));
+ }
+ }
+
+ ~HIDLoader() {
+ if (mModule) {
+ FreeLibrary(mModule);
+ }
+ }
+
+ explicit operator bool() {
+ return mModule && mHidD_GetProductString && mHidP_GetCaps &&
+ mHidP_GetButtonCaps && mHidP_GetValueCaps && mHidP_GetUsages &&
+ mHidP_GetUsageValue && mHidP_GetScaledUsageValue;
+ }
+
+ decltype(HidD_GetProductString)* mHidD_GetProductString;
+ decltype(HidP_GetCaps)* mHidP_GetCaps;
+ decltype(HidP_GetButtonCaps)* mHidP_GetButtonCaps;
+ decltype(HidP_GetValueCaps)* mHidP_GetValueCaps;
+ decltype(HidP_GetUsages)* mHidP_GetUsages;
+ decltype(HidP_GetUsageValue)* mHidP_GetUsageValue;
+ decltype(HidP_GetScaledUsageValue)* mHidP_GetScaledUsageValue;
+
+ private:
+ HMODULE mModule;
+};
+
+HWND sHWnd = nullptr;
+
+static void DirectInputMessageLoopOnceCallback(nsITimer* aTimer,
+ void* aClosure) {
+ MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
+ MSG msg;
+ while (PeekMessageW(&msg, sHWnd, 0, 0, PM_REMOVE) > 0) {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ aTimer->Cancel();
+ if (!sIsShutdown) {
+ aTimer->InitWithNamedFuncCallback(DirectInputMessageLoopOnceCallback,
+ nullptr, kWindowsGamepadPollInterval,
+ nsITimer::TYPE_ONE_SHOT,
+ "DirectInputMessageLoopOnceCallback");
+ }
+}
+
+class WindowsGamepadService {
+ public:
+ WindowsGamepadService() {
+ mDirectInputTimer = NS_NewTimer();
+ mXInputTimer = NS_NewTimer();
+ mDeviceChangeTimer = NS_NewTimer();
+ }
+ virtual ~WindowsGamepadService() { Cleanup(); }
+
+ void DevicesChanged(bool aIsStablizing);
+
+ void StartMessageLoop() {
+ MOZ_ASSERT(mDirectInputTimer);
+ mDirectInputTimer->InitWithNamedFuncCallback(
+ DirectInputMessageLoopOnceCallback, nullptr,
+ kWindowsGamepadPollInterval, nsITimer::TYPE_ONE_SHOT,
+ "DirectInputMessageLoopOnceCallback");
+ }
+
+ void Startup();
+ void Shutdown();
+ // Parse gamepad input from a WM_INPUT message.
+ bool HandleRawInput(HRAWINPUT handle);
+ void SetLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle,
+ const Tainted<uint32_t>& aLightColorIndex,
+ const uint8_t& aRed, const uint8_t& aGreen,
+ const uint8_t& aBlue);
+ size_t WriteOutputReport(const std::vector<uint8_t>& aReport);
+ static void XInputMessageLoopOnceCallback(nsITimer* aTimer, void* aClosure);
+ static void DevicesChangeCallback(nsITimer* aTimer, void* aService);
+
+ private:
+ void ScanForDevices();
+ // Look for connected raw input devices.
+ void ScanForRawInputDevices();
+ // Look for connected XInput devices.
+ bool ScanForXInputDevices();
+ bool HaveXInputGamepad(unsigned int userIndex);
+
+ bool mIsXInputMonitoring;
+ 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;
+
+ HIDLoader mHID;
+ nsAutoHandle mHidHandle;
+ XInputLoader mXInput;
+
+ nsCOMPtr<nsITimer> mDirectInputTimer;
+ nsCOMPtr<nsITimer> mXInputTimer;
+ nsCOMPtr<nsITimer> mDeviceChangeTimer;
+};
+
+void WindowsGamepadService::ScanForRawInputDevices() {
+ if (!mHID) {
+ return;
+ }
+
+ UINT numDevices;
+ if (GetRawInputDeviceList(nullptr, &numDevices, sizeof(RAWINPUTDEVICELIST)) ==
+ kRawInputError) {
+ return;
+ }
+ nsTArray<RAWINPUTDEVICELIST> devices(numDevices);
+ devices.SetLength(numDevices);
+ if (GetRawInputDeviceList(devices.Elements(), &numDevices,
+ sizeof(RAWINPUTDEVICELIST)) == kRawInputError) {
+ return;
+ }
+
+ for (unsigned i = 0; i < devices.Length(); i++) {
+ if (devices[i].dwType == RIM_TYPEHID) {
+ GetRawGamepad(devices[i].hDevice);
+ }
+ }
+}
+
+// static
+void WindowsGamepadService::XInputMessageLoopOnceCallback(nsITimer* aTimer,
+ void* aService) {
+ MOZ_ASSERT(aService);
+ WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService);
+ self->PollXInput();
+ if (self->mIsXInputMonitoring) {
+ aTimer->Cancel();
+ aTimer->InitWithNamedFuncCallback(
+ XInputMessageLoopOnceCallback, self, kWindowsGamepadPollInterval,
+ nsITimer::TYPE_ONE_SHOT, "XInputMessageLoopOnceCallback");
+ }
+}
+
+// static
+void WindowsGamepadService::DevicesChangeCallback(nsITimer* aTimer,
+ void* aService) {
+ MOZ_ASSERT(aService);
+ WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService);
+ self->DevicesChanged(false);
+}
+
+bool WindowsGamepadService::HaveXInputGamepad(unsigned 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!");
+
+ bool found = false;
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return found;
+ }
+
+ for (unsigned int i = 0; i < XUSER_MAX_COUNT; i++) {
+ XINPUT_STATE state = {};
+
+ if (!mXInput.mXInputGetState ||
+ 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(kStandardGamepadAxes, kStandardGamepadButtons,
+ kXInputGamepad);
+ gamepad.userIndex = i;
+ gamepad.state = state;
+ gamepad.gamepadHandle = service->AddGamepad(
+ "xinput", GamepadMappingType::Standard, GamepadHand::_empty,
+ kStandardGamepadButtons, kStandardGamepadAxes, 0, 0,
+ 0); // TODO: Bug 680289, implement gamepad haptics for Windows.
+ mGamepads.AppendElement(std::move(gamepad));
+ }
+
+ return found;
+}
+
+void WindowsGamepadService::ScanForDevices() {
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return;
+ }
+
+ for (int i = mGamepads.Length() - 1; i >= 0; i--) {
+ mGamepads[i].present = false;
+ }
+
+ if (mHID) {
+ ScanForRawInputDevices();
+ }
+ if (mXInput) {
+ mXInputTimer->Cancel();
+ if (ScanForXInputDevices()) {
+ mIsXInputMonitoring = true;
+ mXInputTimer->InitWithNamedFuncCallback(
+ XInputMessageLoopOnceCallback, this, kWindowsGamepadPollInterval,
+ nsITimer::TYPE_ONE_SHOT, "XInputMessageLoopOnceCallback");
+ } else {
+ mIsXInputMonitoring = false;
+ }
+ }
+
+ // Look for devices that are no longer present and remove them.
+ for (int i = mGamepads.Length() - 1; i >= 0; i--) {
+ if (!mGamepads[i].present) {
+ service->RemoveGamepad(mGamepads[i].gamepadHandle);
+ mGamepads.RemoveElementAt(i);
+ }
+ }
+}
+
+void WindowsGamepadService::PollXInput() {
+ for (unsigned int i = 0; i < mGamepads.Length(); i++) {
+ if (mGamepads[i].type != kXInputGamepad) {
+ continue;
+ }
+
+ XINPUT_STATE state = {};
+
+ if (!mXInput.mXInputGetState ||
+ mXInput.mXInputGetState(i, &state) != ERROR_SUCCESS) {
+ continue;
+ }
+
+ if (state.dwPacketNumber != mGamepads[i].state.dwPacketNumber) {
+ CheckXInputChanges(mGamepads[i], state);
+ }
+ }
+}
+
+void WindowsGamepadService::CheckXInputChanges(Gamepad& gamepad,
+ XINPUT_STATE& state) {
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return;
+ }
+ // 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
+ service->NewButtonEvent(gamepad.gamepadHandle, kXIButtonMap[b].mapped,
+ true);
+ } else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) &&
+ gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) {
+ // Button released
+ service->NewButtonEvent(gamepad.gamepadHandle, kXIButtonMap[b].mapped,
+ false);
+ }
+ }
+
+ // Then triggers
+ if (state.Gamepad.bLeftTrigger != gamepad.state.Gamepad.bLeftTrigger) {
+ const bool pressed =
+ state.Gamepad.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
+ service->NewButtonEvent(gamepad.gamepadHandle, kButtonLeftTrigger, pressed,
+ state.Gamepad.bLeftTrigger / 255.0);
+ }
+ if (state.Gamepad.bRightTrigger != gamepad.state.Gamepad.bRightTrigger) {
+ const bool pressed =
+ state.Gamepad.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
+ service->NewButtonEvent(gamepad.gamepadHandle, 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) {
+ const float div = state.Gamepad.sThumbLX > 0 ? 32767.0 : 32768.0;
+ service->NewAxisMoveEvent(gamepad.gamepadHandle, kLeftStickXAxis,
+ state.Gamepad.sThumbLX / div);
+ }
+ if (state.Gamepad.sThumbLY != gamepad.state.Gamepad.sThumbLY) {
+ const float div = state.Gamepad.sThumbLY > 0 ? 32767.0 : 32768.0;
+ service->NewAxisMoveEvent(gamepad.gamepadHandle, kLeftStickYAxis,
+ -1.0 * state.Gamepad.sThumbLY / div);
+ }
+ if (state.Gamepad.sThumbRX != gamepad.state.Gamepad.sThumbRX) {
+ const float div = state.Gamepad.sThumbRX > 0 ? 32767.0 : 32768.0;
+ service->NewAxisMoveEvent(gamepad.gamepadHandle, kRightStickXAxis,
+ state.Gamepad.sThumbRX / div);
+ }
+ if (state.Gamepad.sThumbRY != gamepad.state.Gamepad.sThumbRY) {
+ const float div = state.Gamepad.sThumbRY > 0 ? 32767.0 : 32768.0;
+ service->NewAxisMoveEvent(gamepad.gamepadHandle, kRightStickYAxis,
+ -1.0 * state.Gamepad.sThumbRY / div);
+ }
+ gamepad.state = state;
+}
+
+// Used to sort a list of axes by HID usage.
+class HidValueComparator {
+ public:
+ bool Equals(const Gamepad::axisValue& c1,
+ const Gamepad::axisValue& c2) const {
+ return c1.caps.UsagePage == c2.caps.UsagePage &&
+ c1.caps.Range.UsageMin == c2.caps.Range.UsageMin;
+ }
+ bool LessThan(const Gamepad::axisValue& c1,
+ const Gamepad::axisValue& c2) const {
+ if (c1.caps.UsagePage == c2.caps.UsagePage) {
+ return c1.caps.Range.UsageMin < c2.caps.Range.UsageMin;
+ }
+ return c1.caps.UsagePage < c2.caps.UsagePage;
+ }
+};
+
+// GetRawGamepad() processes its raw data from HID and
+// then trying to remapping buttons and axes based on
+// the mapping rules that are defined for different gamepad products.
+bool WindowsGamepadService::GetRawGamepad(HANDLE handle) {
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return true;
+ }
+
+ if (!mHID) {
+ return false;
+ }
+
+ for (unsigned i = 0; i < mGamepads.Length(); i++) {
+ if (mGamepads[i].type == kRawInputGamepad &&
+ mGamepads[i].handle == handle) {
+ mGamepads[i].present = true;
+ return true;
+ }
+ }
+
+ RID_DEVICE_INFO rdi = {};
+ UINT size = rdi.cbSize = sizeof(RID_DEVICE_INFO);
+ if (GetRawInputDeviceInfo(handle, RIDI_DEVICEINFO, &rdi, &size) ==
+ kRawInputError) {
+ return false;
+ }
+ // Ensure that this is a device we care about
+ if (!SupportedUsage(rdi.hid.usUsagePage, rdi.hid.usUsage)) {
+ return false;
+ }
+
+ // Device name is a mostly-opaque string.
+ if (GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, nullptr, &size) ==
+ kRawInputError) {
+ return false;
+ }
+
+ nsTArray<wchar_t> devname(size);
+ devname.SetLength(size);
+ if (GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, devname.Elements(),
+ &size) == kRawInputError) {
+ return false;
+ }
+
+ // Per http://msdn.microsoft.com/en-us/library/windows/desktop/ee417014.aspx
+ // device names containing "IG_" are XInput controllers. Ignore those
+ // devices since we'll handle them with XInput.
+ if (wcsstr(devname.Elements(), L"IG_")) {
+ return false;
+ }
+
+ // Product string is a human-readable name.
+ // Per
+ // http://msdn.microsoft.com/en-us/library/windows/hardware/ff539681%28v=vs.85%29.aspx
+ // "For USB devices, the maximum string length is 126 wide characters (not
+ // including the terminating NULL character)."
+ wchar_t name[128] = {0};
+ size = sizeof(name);
+ nsTArray<char> gamepad_name;
+ // Creating this file with FILE_FLAG_OVERLAPPED to perform
+ // an asynchronous request in WriteOutputReport.
+ mHidHandle.own(CreateFile(devname.Elements(), GENERIC_READ | GENERIC_WRITE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
+ OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr));
+ if (mHidHandle != INVALID_HANDLE_VALUE) {
+ if (mHID.mHidD_GetProductString(mHidHandle, &name, size)) {
+ int bytes = WideCharToMultiByte(CP_UTF8, 0, name, -1, nullptr, 0, nullptr,
+ nullptr);
+ gamepad_name.SetLength(bytes);
+ WideCharToMultiByte(CP_UTF8, 0, name, -1, gamepad_name.Elements(), bytes,
+ nullptr, nullptr);
+ }
+ }
+ if (gamepad_name.Length() == 0 || !gamepad_name[0]) {
+ const char kUnknown[] = "Unknown Gamepad";
+ gamepad_name.SetLength(ArrayLength(kUnknown));
+ strcpy_s(gamepad_name.Elements(), gamepad_name.Length(), kUnknown);
+ }
+
+ char gamepad_id[256] = {0};
+ _snprintf_s(gamepad_id, _TRUNCATE, "%04x-%04x-%s", rdi.hid.dwVendorId,
+ rdi.hid.dwProductId, gamepad_name.Elements());
+
+ nsTArray<uint8_t> preparsedbytes;
+ if (!GetPreparsedData(handle, preparsedbytes)) {
+ return false;
+ }
+
+ PHIDP_PREPARSED_DATA parsed =
+ reinterpret_cast<PHIDP_PREPARSED_DATA>(preparsedbytes.Elements());
+ HIDP_CAPS caps;
+ if (mHID.mHidP_GetCaps(parsed, &caps) != HIDP_STATUS_SUCCESS) {
+ return false;
+ }
+
+ // Enumerate buttons.
+ USHORT count = caps.NumberInputButtonCaps;
+ nsTArray<HIDP_BUTTON_CAPS> buttonCaps(count);
+ buttonCaps.SetLength(count);
+ if (mHID.mHidP_GetButtonCaps(HidP_Input, buttonCaps.Elements(), &count,
+ parsed) != HIDP_STATUS_SUCCESS) {
+ return false;
+ }
+ uint32_t numButtons = 0;
+ for (unsigned i = 0; i < count; i++) {
+ // Each buttonCaps is typically a range of buttons.
+ numButtons +=
+ buttonCaps[i].Range.UsageMax - buttonCaps[i].Range.UsageMin + 1;
+ }
+
+ // Enumerate value caps, which represent axes and d-pads.
+ count = caps.NumberInputValueCaps;
+ nsTArray<HIDP_VALUE_CAPS> axisCaps(count);
+ axisCaps.SetLength(count);
+ if (mHID.mHidP_GetValueCaps(HidP_Input, axisCaps.Elements(), &count,
+ parsed) != HIDP_STATUS_SUCCESS) {
+ return false;
+ }
+
+ size_t numAxes = 0;
+ nsTArray<Gamepad::axisValue> axes(kAxesLengthCap);
+ // We store these value caps and handle the dpad info in GamepadRemapper
+ // later.
+ axes.SetLength(kAxesLengthCap);
+
+ // Looking for the exisiting ramapping rule.
+ bool defaultRemapper = false;
+ RefPtr<GamepadRemapper> remapper = GetGamepadRemapper(
+ rdi.hid.dwVendorId, rdi.hid.dwProductId, defaultRemapper);
+ MOZ_ASSERT(remapper);
+
+ for (size_t i = 0; i < count; i++) {
+ const size_t axisIndex =
+ axisCaps[i].Range.UsageMin - kAxisMinimumUsageNumber;
+ if (axisIndex < kAxesLengthCap && !axes[axisIndex].active) {
+ axes[axisIndex].caps = axisCaps[i];
+ axes[axisIndex].active = true;
+ numAxes = std::max(numAxes, axisIndex + 1);
+ }
+ }
+
+ // Not already present, add it.
+
+ remapper->SetAxisCount(numAxes);
+ remapper->SetButtonCount(numButtons);
+ Gamepad gamepad(numAxes, numButtons, kRawInputGamepad);
+ gamepad.handle = handle;
+
+ for (unsigned i = 0; i < gamepad.numAxes; i++) {
+ gamepad.axes[i] = axes[i];
+ }
+
+ gamepad.remapper = remapper.forget();
+ // TODO: Bug 680289, implement gamepad haptics for Windows.
+ gamepad.gamepadHandle = service->AddGamepad(
+ gamepad_id, gamepad.remapper->GetMappingType(), GamepadHand::_empty,
+ gamepad.remapper->GetButtonCount(), gamepad.remapper->GetAxisCount(), 0,
+ gamepad.remapper->GetLightIndicatorCount(),
+ gamepad.remapper->GetTouchEventCount());
+
+ nsTArray<GamepadLightIndicatorType> lightTypes;
+ gamepad.remapper->GetLightIndicators(lightTypes);
+ for (uint32_t i = 0; i < lightTypes.Length(); ++i) {
+ if (lightTypes[i] != GamepadLightIndicator::DefaultType()) {
+ service->NewLightIndicatorTypeEvent(gamepad.gamepadHandle, i,
+ lightTypes[i]);
+ }
+ }
+
+ mGamepads.AppendElement(std::move(gamepad));
+ return true;
+}
+
+bool WindowsGamepadService::HandleRawInput(HRAWINPUT handle) {
+ if (!mHID) {
+ return false;
+ }
+
+ RefPtr<GamepadPlatformService> service =
+ GamepadPlatformService::GetParentService();
+ if (!service) {
+ return false;
+ }
+
+ // First, get data from the handle
+ UINT size;
+ GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER));
+ nsTArray<uint8_t> data(size);
+ data.SetLength(size);
+ if (GetRawInputData(handle, RID_INPUT, data.Elements(), &size,
+ sizeof(RAWINPUTHEADER)) == kRawInputError) {
+ return false;
+ }
+ PRAWINPUT raw = reinterpret_cast<PRAWINPUT>(data.Elements());
+
+ Gamepad* gamepad = nullptr;
+ for (unsigned i = 0; i < mGamepads.Length(); i++) {
+ if (mGamepads[i].type == kRawInputGamepad &&
+ mGamepads[i].handle == raw->header.hDevice) {
+ gamepad = &mGamepads[i];
+ break;
+ }
+ }
+ if (gamepad == nullptr) {
+ return false;
+ }
+
+ // Second, get the preparsed data
+ nsTArray<uint8_t> parsedbytes;
+ if (!GetPreparsedData(raw->header.hDevice, parsedbytes)) {
+ return false;
+ }
+ PHIDP_PREPARSED_DATA parsed =
+ reinterpret_cast<PHIDP_PREPARSED_DATA>(parsedbytes.Elements());
+
+ // Get all the pressed buttons.
+ nsTArray<USAGE> usages(gamepad->numButtons);
+ usages.SetLength(gamepad->numButtons);
+ ULONG usageLength = gamepad->numButtons;
+ if (mHID.mHidP_GetUsages(HidP_Input, kButtonUsagePage, 0, usages.Elements(),
+ &usageLength, parsed, (PCHAR)raw->data.hid.bRawData,
+ raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
+ return false;
+ }
+
+ nsTArray<bool> buttons(gamepad->numButtons);
+ buttons.SetLength(gamepad->numButtons);
+ // If we don't zero out the buttons array first, sometimes it can reuse
+ // values.
+ memset(buttons.Elements(), 0, gamepad->numButtons * sizeof(bool));
+
+ for (unsigned i = 0; i < usageLength; i++) {
+ // The button index in usages may be larger than what we detected when
+ // enumerating gamepads. If so, warn and continue.
+ //
+ // Usage ID of 0 is reserved, so it should always be 1 or higher.
+ if (NS_WARN_IF((usages[i] - 1u) >= buttons.Length())) {
+ continue;
+ }
+ buttons[usages[i] - 1u] = true;
+ }
+
+ for (unsigned i = 0; i < gamepad->numButtons; i++) {
+ if (gamepad->buttons[i] != buttons[i]) {
+ gamepad->remapper->RemapButtonEvent(gamepad->gamepadHandle, i,
+ buttons[i]);
+ gamepad->buttons[i] = buttons[i];
+ }
+ }
+
+ // Get all axis values.
+ for (unsigned i = 0; i < gamepad->numAxes; i++) {
+ double new_value;
+ if (gamepad->axes[i].caps.LogicalMin < 0) {
+ LONG value;
+ if (mHID.mHidP_GetScaledUsageValue(
+ HidP_Input, gamepad->axes[i].caps.UsagePage, 0,
+ gamepad->axes[i].caps.Range.UsageMin, &value, parsed,
+ (PCHAR)raw->data.hid.bRawData,
+ raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
+ continue;
+ }
+ new_value = ScaleAxis(value, gamepad->axes[i].caps.LogicalMin,
+ gamepad->axes[i].caps.LogicalMax);
+ } else {
+ ULONG value;
+ if (mHID.mHidP_GetUsageValue(
+ HidP_Input, gamepad->axes[i].caps.UsagePage, 0,
+ gamepad->axes[i].caps.Range.UsageMin, &value, parsed,
+ (PCHAR)raw->data.hid.bRawData,
+ raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
+ continue;
+ }
+
+ new_value = ScaleAxis(value, gamepad->axes[i].caps.LogicalMin,
+ gamepad->axes[i].caps.LogicalMax);
+ }
+ if (gamepad->axes[i].value != new_value) {
+ gamepad->remapper->RemapAxisMoveEvent(gamepad->gamepadHandle, i,
+ new_value);
+ gamepad->axes[i].value = new_value;
+ }
+ }
+
+ BYTE* rawData = raw->data.hid.bRawData;
+ gamepad->remapper->ProcessTouchData(gamepad->gamepadHandle, rawData);
+
+ return true;
+}
+
+void WindowsGamepadService::SetLightIndicatorColor(
+ const Tainted<GamepadHandle>& aGamepadHandle,
+ const Tainted<uint32_t>& aLightColorIndex, const uint8_t& aRed,
+ const uint8_t& aGreen, const uint8_t& aBlue) {
+ // We get aControllerIdx from GamepadPlatformService::AddGamepad(),
+ // It begins from 1 and is stored at Gamepad.id.
+ const Gamepad* gamepad = (MOZ_FIND_AND_VALIDATE(
+ aGamepadHandle, list_item.gamepadHandle == aGamepadHandle, mGamepads));
+ if (!gamepad) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ RefPtr<GamepadRemapper> remapper = gamepad->remapper;
+ if (!remapper ||
+ MOZ_IS_VALID(aLightColorIndex,
+ remapper->GetLightIndicatorCount() <= aLightColorIndex)) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ std::vector<uint8_t> report;
+ remapper->GetLightColorReport(aRed, aGreen, aBlue, report);
+ WriteOutputReport(report);
+}
+
+size_t WindowsGamepadService::WriteOutputReport(
+ const std::vector<uint8_t>& aReport) {
+ DCHECK(static_cast<const void*>(aReport.data()));
+ DCHECK_GE(aReport.size(), 1U);
+ if (!mHidHandle) return 0;
+
+ nsAutoHandle eventHandle(::CreateEvent(nullptr, FALSE, FALSE, nullptr));
+ OVERLAPPED overlapped = {0};
+ overlapped.hEvent = eventHandle;
+
+ // Doing an asynchronous write to allows us to time out
+ // if the write takes too long.
+ DWORD bytesWritten = 0;
+ BOOL writeSuccess =
+ ::WriteFile(mHidHandle, static_cast<const void*>(aReport.data()),
+ aReport.size(), &bytesWritten, &overlapped);
+ if (!writeSuccess) {
+ DWORD error = ::GetLastError();
+ if (error == ERROR_IO_PENDING) {
+ // Wait for the write to complete. This causes WriteOutputReport to behave
+ // synchronously but with a timeout.
+ DWORD wait_object = ::WaitForSingleObject(overlapped.hEvent, 100);
+ if (wait_object == WAIT_OBJECT_0) {
+ if (!::GetOverlappedResult(mHidHandle, &overlapped, &bytesWritten,
+ TRUE)) {
+ return 0;
+ }
+ } else {
+ // Wait failed, or the timeout was exceeded before the write completed.
+ // Cancel the write request.
+ if (::CancelIo(mHidHandle)) {
+ wait_object = ::WaitForSingleObject(overlapped.hEvent, INFINITE);
+ MOZ_ASSERT(wait_object == WAIT_OBJECT_0);
+ }
+ }
+ }
+ }
+ return writeSuccess ? bytesWritten : 0;
+}
+
+void WindowsGamepadService::Startup() { ScanForDevices(); }
+
+void WindowsGamepadService::Shutdown() { Cleanup(); }
+
+void WindowsGamepadService::Cleanup() {
+ mIsXInputMonitoring = false;
+ if (mDirectInputTimer) {
+ mDirectInputTimer->Cancel();
+ }
+ if (mXInputTimer) {
+ mXInputTimer->Cancel();
+ }
+ if (mDeviceChangeTimer) {
+ mDeviceChangeTimer->Cancel();
+ }
+
+ mGamepads.Clear();
+}
+
+void WindowsGamepadService::DevicesChanged(bool aIsStablizing) {
+ if (aIsStablizing) {
+ mDeviceChangeTimer->Cancel();
+ mDeviceChangeTimer->InitWithNamedFuncCallback(
+ DevicesChangeCallback, this, kDevicesChangedStableDelay,
+ nsITimer::TYPE_ONE_SHOT, "DevicesChangeCallback");
+ } else {
+ ScanForDevices();
+ }
+}
+
+bool RegisterRawInput(HWND hwnd, bool enable) {
+ nsTArray<RAWINPUTDEVICE> rid(ArrayLength(kUsagePages));
+ rid.SetLength(ArrayLength(kUsagePages));
+
+ for (unsigned i = 0; i < rid.Length(); i++) {
+ rid[i].usUsagePage = kUsagePages[i].usagePage;
+ rid[i].usUsage = kUsagePages[i].usage;
+ rid[i].dwFlags =
+ enable ? RIDEV_EXINPUTSINK | RIDEV_DEVNOTIFY : RIDEV_REMOVE;
+ rid[i].hwndTarget = hwnd;
+ }
+
+ if (!RegisterRawInputDevices(rid.Elements(), rid.Length(),
+ sizeof(RAWINPUTDEVICE))) {
+ return false;
+ }
+ return true;
+}
+
+static LRESULT CALLBACK GamepadWindowProc(HWND hwnd, UINT msg, WPARAM wParam,
+ LPARAM lParam) {
+ const unsigned int DBT_DEVICEARRIVAL = 0x8000;
+ const unsigned int DBT_DEVICEREMOVECOMPLETE = 0x8004;
+ const unsigned int DBT_DEVNODES_CHANGED = 0x7;
+
+ switch (msg) {
+ case WM_DEVICECHANGE:
+ if (wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE ||
+ wParam == DBT_DEVNODES_CHANGED) {
+ if (gService) {
+ gService->DevicesChanged(true);
+ }
+ }
+ break;
+ case WM_INPUT:
+ if (gService) {
+ gService->HandleRawInput(reinterpret_cast<HRAWINPUT>(lParam));
+ }
+ break;
+ }
+ return DefWindowProc(hwnd, msg, wParam, lParam);
+}
+
+class StartWindowsGamepadServiceRunnable final : public Runnable {
+ public:
+ StartWindowsGamepadServiceRunnable()
+ : Runnable("StartWindowsGamepadServiceRunnable") {}
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
+ gService = new WindowsGamepadService();
+ gService->Startup();
+
+ if (sHWnd == nullptr) {
+ WNDCLASSW wc;
+ HMODULE hSelf = GetModuleHandle(nullptr);
+
+ if (!GetClassInfoW(hSelf, L"MozillaGamepadClass", &wc)) {
+ ZeroMemory(&wc, sizeof(WNDCLASSW));
+ wc.hInstance = hSelf;
+ wc.lpfnWndProc = GamepadWindowProc;
+ wc.lpszClassName = L"MozillaGamepadClass";
+ RegisterClassW(&wc);
+ }
+
+ sHWnd = CreateWindowW(L"MozillaGamepadClass", L"Gamepad Watcher", 0, 0, 0,
+ 0, 0, nullptr, nullptr, hSelf, nullptr);
+ RegisterRawInput(sHWnd, true);
+ }
+
+ // Explicitly start the message loop
+ gService->StartMessageLoop();
+
+ return NS_OK;
+ }
+
+ private:
+ ~StartWindowsGamepadServiceRunnable() {}
+};
+
+class StopWindowsGamepadServiceRunnable final : public Runnable {
+ public:
+ StopWindowsGamepadServiceRunnable()
+ : Runnable("StopWindowsGamepadServiceRunnable") {}
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
+ if (sHWnd) {
+ RegisterRawInput(sHWnd, false);
+ DestroyWindow(sHWnd);
+ sHWnd = nullptr;
+ }
+
+ gService->Shutdown();
+ delete gService;
+ gService = nullptr;
+
+ return NS_OK;
+ }
+
+ private:
+ ~StopWindowsGamepadServiceRunnable() {}
+};
+
+} // namespace
+
+namespace mozilla::dom {
+
+using namespace mozilla::ipc;
+
+void StartGamepadMonitoring() {
+ AssertIsOnBackgroundThread();
+
+ if (gMonitorThread || gService) {
+ return;
+ }
+ sIsShutdown = false;
+ NS_NewNamedThread("Gamepad", getter_AddRefs(gMonitorThread));
+ gMonitorThread->Dispatch(new StartWindowsGamepadServiceRunnable(),
+ NS_DISPATCH_NORMAL);
+}
+
+void StopGamepadMonitoring() {
+ AssertIsOnBackgroundThread();
+
+ if (sIsShutdown) {
+ return;
+ }
+ sIsShutdown = true;
+ gMonitorThread->Dispatch(new StopWindowsGamepadServiceRunnable(),
+ NS_DISPATCH_NORMAL);
+ gMonitorThread->Shutdown();
+ gMonitorThread = nullptr;
+}
+
+void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle,
+ const Tainted<uint32_t>& aLightColorIndex,
+ const uint8_t& aRed, const uint8_t& aGreen,
+ const uint8_t& aBlue) {
+ MOZ_ASSERT(gService);
+ if (!gService) {
+ return;
+ }
+ gService->SetLightIndicatorColor(aGamepadHandle, aLightColorIndex, aRed,
+ aGreen, aBlue);
+}
+
+} // namespace mozilla::dom