/* -*- 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 "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 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 buttons; struct axisValue { HIDP_VALUE_CAPS caps; double value; }; nsTArray 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( GetProcAddress(module, "XInputEnable")); mXInputGetState = reinterpret_cast( 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& 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& 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(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); } } 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 mGamepads; HIDLoader mHID; 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->InitWithFuncCallback(XInputMessageLoopOnceCallback, self, kWindowsGamepadPollInterval, nsITimer::TYPE_ONE_SHOT); } } // static void WindowsGamepadService::DevicesChangeCallback(nsITimer *aTimer, void* aService) { MOZ_ASSERT(aService); WindowsGamepadService* self = static_cast(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 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 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 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 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; 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 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 valueCaps(count); valueCaps.SetLength(count); if (mHID.mHidP_GetValueCaps(HidP_Input, valueCaps.Elements(), &count, parsed) != HIDP_STATUS_SUCCESS) { return false; } nsTArray 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 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++) { 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(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 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() {} 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