/* -*- 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 #include #ifndef UNICODE # define UNICODE #endif #include #include #include #include #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 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 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 axes; RefPtr 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( GetProcAddress(module, "XInputEnable")); // Checking if `XInputGetStateEx` is available. If not, // we will fallback to use `XInputGetState`. mXInputGetState = reinterpret_cast( GetProcAddress(module, kXInputGetStateExOrdinal)); if (!mXInputGetState) { mXInputGetState = reinterpret_cast( 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& 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( GetProcAddress(mModule, "HidD_GetProductString")); mHidP_GetCaps = reinterpret_cast( GetProcAddress(mModule, "HidP_GetCaps")); mHidP_GetButtonCaps = reinterpret_cast( GetProcAddress(mModule, "HidP_GetButtonCaps")); mHidP_GetValueCaps = reinterpret_cast( GetProcAddress(mModule, "HidP_GetValueCaps")); mHidP_GetUsages = reinterpret_cast( GetProcAddress(mModule, "HidP_GetUsages")); mHidP_GetUsageValue = reinterpret_cast( GetProcAddress(mModule, "HidP_GetUsageValue")); mHidP_GetScaledUsageValue = reinterpret_cast( 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& aGamepadHandle, const Tainted& aLightColorIndex, const uint8_t& aRed, const uint8_t& aGreen, const uint8_t& aBlue); size_t WriteOutputReport(const std::vector& 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 mGamepads; HIDLoader mHID; nsAutoHandle mHidHandle; XInputLoader mXInput; nsCOMPtr mDirectInputTimer; nsCOMPtr mXInputTimer; nsCOMPtr mDeviceChangeTimer; }; void WindowsGamepadService::ScanForRawInputDevices() { if (!mHID) { return; } UINT numDevices; if (GetRawInputDeviceList(nullptr, &numDevices, sizeof(RAWINPUTDEVICELIST)) == kRawInputError) { return; } nsTArray 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(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(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 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 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 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 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 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 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 preparsedbytes; if (!GetPreparsedData(handle, preparsedbytes)) { return false; } PHIDP_PREPARSED_DATA parsed = reinterpret_cast(preparsedbytes.Elements()); HIDP_CAPS caps; if (mHID.mHidP_GetCaps(parsed, &caps) != HIDP_STATUS_SUCCESS) { return false; } // Enumerate buttons. USHORT count = caps.NumberInputButtonCaps; nsTArray 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 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 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 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 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 service = GamepadPlatformService::GetParentService(); if (!service) { return false; } // First, get data from the handle UINT size; GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)); nsTArray data(size); data.SetLength(size); if (GetRawInputData(handle, RID_INPUT, data.Elements(), &size, sizeof(RAWINPUTHEADER)) == kRawInputError) { return false; } PRAWINPUT raw = reinterpret_cast(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 parsedbytes; if (!GetPreparsedData(raw->header.hDevice, parsedbytes)) { return false; } PHIDP_PREPARSED_DATA parsed = reinterpret_cast(parsedbytes.Elements()); // Get all the pressed buttons. nsTArray 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 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& aGamepadHandle, const Tainted& 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 remapper = gamepad->remapper; if (!remapper || MOZ_IS_VALID(aLightColorIndex, remapper->GetLightIndicatorCount() <= aLightColorIndex)) { MOZ_ASSERT(false); return; } std::vector report; remapper->GetLightColorReport(aRed, aGreen, aBlue, report); WriteOutputReport(report); } size_t WindowsGamepadService::WriteOutputReport( const std::vector& aReport) { DCHECK(static_cast(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(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 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(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& aGamepadHandle, const Tainted& 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