diff options
Diffstat (limited to 'dom/gamepad/windows/WindowsGamepad.cpp')
-rw-r--r-- | dom/gamepad/windows/WindowsGamepad.cpp | 1096 |
1 files changed, 1096 insertions, 0 deletions
diff --git a/dom/gamepad/windows/WindowsGamepad.cpp b/dom/gamepad/windows/WindowsGamepad.cpp new file mode 100644 index 0000000000..e1965c00c8 --- /dev/null +++ b/dom/gamepad/windows/WindowsGamepad.cpp @@ -0,0 +1,1096 @@ +/* -*- 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 "nsIComponentManager.h" +#include "nsITimer.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Services.h" + +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/dom/GamepadPlatformService.h" + +namespace { + +using namespace mozilla; +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 +const unsigned kDesktopUsagePage = 0x1; +const unsigned 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. +// 50ms is arbitrarily chosen. +const uint32_t kWindowsGamepadPollInterval = 50; + +const UINT kRawInputError = (UINT)-1; + +#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, + 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; + + // 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; + HIDP_VALUE_CAPS dpadCaps; + + nsTArray<bool> buttons; + struct axisValue { + HIDP_VALUE_CAPS caps; + double value; + }; + nsTArray<axisValue> axes; + + // Used during rescan to find devices that were disconnected. + bool present; + + Gamepad(uint32_t aNumAxes, + uint32_t aNumButtons, + bool aHasDpad, + GamepadType aType) : + numAxes(aNumAxes), + numButtons(aNumButtons), + hasDpad(aHasDpad), + type(aType), + present(true) + { + buttons.SetLength(numButtons); + axes.SetLength(numAxes); + } +private: + Gamepad() {} +}; + +// 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) == 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; +} + +/* + * Given a value from a d-pad (POV hat in USB HID terminology), + * represent it as 4 buttons, one for each cardinal direction. + */ +void +UnpackDpad(LONG dpad_value, const Gamepad* gamepad, nsTArray<bool>& buttons) +{ + const unsigned kUp = gamepad->numButtons - 4; + const unsigned kDown = gamepad->numButtons - 3; + const unsigned kLeft = gamepad->numButtons - 2; + const unsigned kRight = gamepad->numButtons - 1; + + // Different controllers have different ways of representing + // "nothing is pressed", but they're all outside the range of values. + if (dpad_value < gamepad->dpadCaps.LogicalMin + || dpad_value > gamepad->dpadCaps.LogicalMax) { + // Nothing is pressed. + return; + } + + // Normalize value to start at 0. + int value = dpad_value - gamepad->dpadCaps.LogicalMin; + + // 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; + } +} + +/* + * 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() : mModule(LoadLibraryW(L"hid.dll")), + mHidD_GetProductString(nullptr), + mHidP_GetCaps(nullptr), + mHidP_GetButtonCaps(nullptr), + mHidP_GetValueCaps(nullptr), + mHidP_GetUsages(nullptr), + mHidP_GetUsageValue(nullptr), + mHidP_GetScaledUsageValue(nullptr) + { + 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); + } + } + + 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->InitWithFuncCallback(DirectInputMessageLoopOnceCallback, + nullptr, kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT); + } +} + +class WindowsGamepadService +{ + public: + WindowsGamepadService() + { + mDirectInputTimer = do_CreateInstance("@mozilla.org/timer;1"); + mXInputTimer = do_CreateInstance("@mozilla.org/timer;1"); + mDeviceChangeTimer = do_CreateInstance("@mozilla.org/timer;1"); + } + virtual ~WindowsGamepadService() + { + Cleanup(); + } + + void DevicesChanged(bool aIsStablizing); + + void StartMessageLoop() + { + MOZ_ASSERT(mDirectInputTimer); + mDirectInputTimer->InitWithFuncCallback(DirectInputMessageLoopOnceCallback, + nullptr, kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT); + } + + void Startup(); + void Shutdown(); + // Parse gamepad input from a WM_INPUT message. + bool HandleRawInput(HRAWINPUT handle); + + 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(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; + 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->InitWithFuncCallback(XInputMessageLoopOnceCallback, self, + kWindowsGamepadPollInterval, nsITimer::TYPE_ONE_SHOT); + } +} + +// static +void +WindowsGamepadService::DevicesChangeCallback(nsITimer *aTimer, void* aService) +{ + MOZ_ASSERT(aService); + WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService); + self->DevicesChanged(false); +} + +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!"); + + bool found = false; + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return found; + } + + 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(kStandardGamepadAxes, + kStandardGamepadButtons, + true, + kXInputGamepad); + gamepad.userIndex = i; + gamepad.state = state; + gamepad.id = service->AddGamepad("xinput", + GamepadMappingType::Standard, + kStandardGamepadButtons, + kStandardGamepadAxes); + mGamepads.AppendElement(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->InitWithFuncCallback(XInputMessageLoopOnceCallback, this, + kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT); + } 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].id); + mGamepads.RemoveElementAt(i); + } + } +} + +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) { + 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.id, kXIButtonMap[b].mapped, true); + } else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) && + gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) { + // Button released + service->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; + service->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; + service->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) { + service->NewAxisMoveEvent(gamepad.id, kLeftStickXAxis, + state.Gamepad.sThumbLX / 32767.0); + } + if (state.Gamepad.sThumbLY != gamepad.state.Gamepad.sThumbLY) { + service->NewAxisMoveEvent(gamepad.id, kLeftStickYAxis, + -1.0 * state.Gamepad.sThumbLY / 32767.0); + } + if (state.Gamepad.sThumbRX != gamepad.state.Gamepad.sThumbRX) { + service->NewAxisMoveEvent(gamepad.id, kRightStickXAxis, + state.Gamepad.sThumbRX / 32767.0); + } + if (state.Gamepad.sThumbRY != gamepad.state.Gamepad.sThumbRY) { + service->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 + { + if (c1.UsagePage == c2.UsagePage) { + return c1.Range.UsageMin < c2.Range.UsageMin; + } + return c1.UsagePage < c2.UsagePage; + } +}; + +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; + HANDLE hid_handle = CreateFile(devname.Elements(), GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); + if (hid_handle) { + if (mHID.mHidD_GetProductString(hid_handle, &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); + } + CloseHandle(hid_handle); + } + 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> valueCaps(count); + valueCaps.SetLength(count); + if (mHID.mHidP_GetValueCaps(HidP_Input, valueCaps.Elements(), &count, parsed) + != HIDP_STATUS_SUCCESS) { + return false; + } + nsTArray<HIDP_VALUE_CAPS> axes; + // Sort the axes by usagePage and usage to expose a consistent ordering. + bool hasDpad; + HIDP_VALUE_CAPS dpadCaps; + + HidValueComparator comparator; + for (unsigned i = 0; i < count; i++) { + if (valueCaps[i].UsagePage == kDesktopUsagePage + && valueCaps[i].Range.UsageMin == kUsageDpad + // Don't know how to handle d-pads that return weird values. + && valueCaps[i].LogicalMax - valueCaps[i].LogicalMin == 7) { + // d-pad gets special handling. + // Ostensibly HID devices can expose multiple d-pads, but this + // doesn't happen in practice. + hasDpad = true; + dpadCaps = valueCaps[i]; + // Expose d-pad as 4 additional buttons. + numButtons += 4; + } else { + axes.InsertElementSorted(valueCaps[i], comparator); + } + } + + uint32_t numAxes = axes.Length(); + + // Not already present, add it. + Gamepad gamepad(numAxes, + numButtons, + true, + kRawInputGamepad); + + gamepad.handle = handle; + + for (unsigned i = 0; i < gamepad.numAxes; i++) { + gamepad.axes[i].caps = axes[i]; + } + + gamepad.id = service->AddGamepad(gamepad_id, + GamepadMappingType::_empty, + gamepad.numButtons, + gamepad.numAxes); + mGamepads.AppendElement(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++) { + buttons[usages[i] - 1] = true; + } + + if (gamepad->hasDpad) { + // Get d-pad position as 4 buttons. + ULONG value; + if (mHID.mHidP_GetUsageValue(HidP_Input, gamepad->dpadCaps.UsagePage, 0, gamepad->dpadCaps.Range.UsageMin, &value, parsed, (PCHAR)raw->data.hid.bRawData, raw->data.hid.dwSizeHid) == HIDP_STATUS_SUCCESS) { + UnpackDpad(static_cast<LONG>(value), gamepad, buttons); + } + } + + for (unsigned i = 0; i < gamepad->numButtons; i++) { + if (gamepad->buttons[i] != buttons[i]) { + service->NewButtonEvent(gamepad->id, 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) { + service->NewAxisMoveEvent(gamepad->id, i, new_value); + gamepad->axes[i].value = new_value; + } + } + + return true; +} + +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->InitWithFuncCallback(DevicesChangeCallback, this, + kDevicesChangedStableDelay, + nsITimer::TYPE_ONE_SHOT); + } 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() {} + + 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() {} + + 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 { +namespace dom { + +using namespace mozilla::ipc; + +void +StartGamepadMonitoring() +{ + AssertIsOnBackgroundThread(); + + if (gMonitorThread || gService) { + return; + } + sIsShutdown = false; + NS_NewThread(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; +} + +} // namespace dom +} // namespace mozilla |