diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/gamepad/windows/WindowsGamepad.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/gamepad/windows/WindowsGamepad.cpp')
-rw-r--r-- | dom/gamepad/windows/WindowsGamepad.cpp | 1136 |
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 |