зеркало из https://github.com/mozilla/gecko-dev.git
1110 строки
34 KiB
C++
1110 строки
34 KiB
C++
/* -*- 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 "mozilla/ArrayUtils.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, 0x30 = X
|
|
const uint32_t kAxisMinimumUsageNumber = 0x30;
|
|
// USB HID usage tables, page 1 (Hat switch)
|
|
const uint32_t kDpadMinimumUsageNumber = 0x39;
|
|
const uint32_t kAxesLengthCap = 16;
|
|
|
|
// USB HID usage tables
|
|
const uint32_t kDesktopUsagePage = 0x1;
|
|
const uint32_t kGameControlsUsagePage = 0x5;
|
|
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.
|
|
// 50ms is arbitrarily chosen.
|
|
const uint32_t kWindowsGamepadPollInterval = 50;
|
|
|
|
const UINT kRawInputError = (UINT)-1;
|
|
|
|
#ifndef XUSER_MAX_COUNT
|
|
# define XUSER_MAX_COUNT 4
|
|
#endif
|
|
|
|
const struct {
|
|
int usagePage;
|
|
int usage;
|
|
} kUsagePages[] = {
|
|
// USB HID usage tables, page 1
|
|
{kDesktopUsagePage, 4}, // Joystick
|
|
{kDesktopUsagePage, 5} // Gamepad
|
|
};
|
|
|
|
const struct {
|
|
WORD button;
|
|
int mapped;
|
|
} kXIButtonMap[] = {{XINPUT_GAMEPAD_DPAD_UP, 12},
|
|
{XINPUT_GAMEPAD_DPAD_DOWN, 13},
|
|
{XINPUT_GAMEPAD_DPAD_LEFT, 14},
|
|
{XINPUT_GAMEPAD_DPAD_RIGHT, 15},
|
|
{XINPUT_GAMEPAD_START, 9},
|
|
{XINPUT_GAMEPAD_BACK, 8},
|
|
{XINPUT_GAMEPAD_LEFT_THUMB, 10},
|
|
{XINPUT_GAMEPAD_RIGHT_THUMB, 11},
|
|
{XINPUT_GAMEPAD_LEFT_SHOULDER, 4},
|
|
{XINPUT_GAMEPAD_RIGHT_SHOULDER, 5},
|
|
{XINPUT_GAMEPAD_A, 0},
|
|
{XINPUT_GAMEPAD_B, 1},
|
|
{XINPUT_GAMEPAD_X, 2},
|
|
{XINPUT_GAMEPAD_Y, 3}};
|
|
const size_t kNumMappings = ArrayLength(kXIButtonMap);
|
|
|
|
enum GamepadType { kNoGamepad = 0, kRawInputGamepad, kXInputGamepad };
|
|
|
|
class WindowsGamepadService;
|
|
// This pointer holds a windows gamepad backend service,
|
|
// it will be created and destroyed by background thread and
|
|
// used by gMonitorThread
|
|
WindowsGamepadService* MOZ_NON_OWNING_REF gService = nullptr;
|
|
nsCOMPtr<nsIThread> gMonitorThread = nullptr;
|
|
static bool sIsShutdown = false;
|
|
|
|
class Gamepad {
|
|
public:
|
|
GamepadType type;
|
|
|
|
// Handle to raw input device
|
|
HANDLE handle;
|
|
|
|
// XInput Index of the user's controller. Passed to XInputGetState.
|
|
DWORD userIndex;
|
|
|
|
// Last-known state of the controller.
|
|
XINPUT_STATE state;
|
|
|
|
// ID from the GamepadService, also used as the index into
|
|
// WindowsGamepadService::mGamepads.
|
|
int id;
|
|
|
|
// Information about the physical device.
|
|
unsigned numAxes;
|
|
unsigned numButtons;
|
|
|
|
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.
|
|
typedef void(WINAPI* XInputEnable_func)(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"));
|
|
mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>(
|
|
GetProcAddress(module, "XInputGetState"));
|
|
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(uint32_t aControllerIdx, uint32_t aLightIndex,
|
|
uint8_t aRed, uint8_t aGreen, 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(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.id = 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].id);
|
|
mGamepads.RemoveElementAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void WindowsGamepadService::PollXInput() {
|
|
for (unsigned int i = 0; i < mGamepads.Length(); i++) {
|
|
if (mGamepads[i].type != kXInputGamepad) {
|
|
continue;
|
|
}
|
|
|
|
XINPUT_STATE state = {};
|
|
DWORD res = mXInput.mXInputGetState(mGamepads[i].userIndex, &state);
|
|
if (res == ERROR_SUCCESS &&
|
|
state.dwPacketNumber != mGamepads[i].state.dwPacketNumber) {
|
|
CheckXInputChanges(mGamepads[i], state);
|
|
}
|
|
}
|
|
}
|
|
|
|
void WindowsGamepadService::CheckXInputChanges(Gamepad& gamepad,
|
|
XINPUT_STATE& state) {
|
|
RefPtr<GamepadPlatformService> service =
|
|
GamepadPlatformService::GetParentService();
|
|
if (!service) {
|
|
return;
|
|
}
|
|
// Handle digital buttons first
|
|
for (size_t b = 0; b < kNumMappings; b++) {
|
|
if (state.Gamepad.wButtons & kXIButtonMap[b].button &&
|
|
!(gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button)) {
|
|
// Button pressed
|
|
service->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, true);
|
|
} else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) &&
|
|
gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) {
|
|
// Button released
|
|
service->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, false);
|
|
}
|
|
}
|
|
|
|
// Then triggers
|
|
if (state.Gamepad.bLeftTrigger != gamepad.state.Gamepad.bLeftTrigger) {
|
|
const 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) {
|
|
const 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) {
|
|
const float div = state.Gamepad.sThumbLX > 0 ? 32767.0 : 32768.0;
|
|
service->NewAxisMoveEvent(gamepad.id, 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.id, 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.id, 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.id, 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.id = 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.id, 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->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) {
|
|
gamepad->remapper->RemapAxisMoveEvent(gamepad->id, i, new_value);
|
|
gamepad->axes[i].value = new_value;
|
|
}
|
|
}
|
|
|
|
BYTE* rawData = raw->data.hid.bRawData;
|
|
gamepad->remapper->ProcessTouchData(gamepad->id, rawData);
|
|
|
|
return true;
|
|
}
|
|
|
|
void WindowsGamepadService::SetLightIndicatorColor(uint32_t aControllerIdx,
|
|
uint32_t aLightColorIndex,
|
|
uint8_t aRed, uint8_t aGreen,
|
|
uint8_t aBlue) {
|
|
// We get aControllerIdx from GamepadPlatformService::AddGamepad(),
|
|
// It begins from 1 and is stored at Gamepad.id.
|
|
const Gamepad* gamepad = nullptr;
|
|
for (const auto& pad : mGamepads) {
|
|
if (pad.id == aControllerIdx) {
|
|
gamepad = &pad;
|
|
break;
|
|
}
|
|
}
|
|
if (!gamepad) {
|
|
MOZ_ASSERT(false);
|
|
return;
|
|
}
|
|
|
|
RefPtr<GamepadRemapper> remapper = gamepad->remapper;
|
|
if (!remapper || 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 {
|
|
namespace 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(uint32_t aControllerIdx,
|
|
uint32_t aLightColorIndex, uint8_t aRed,
|
|
uint8_t aGreen, uint8_t aBlue) {
|
|
MOZ_ASSERT(gService);
|
|
if (!gService) {
|
|
return;
|
|
}
|
|
gService->SetLightIndicatorColor(aControllerIdx, aLightColorIndex, aRed,
|
|
aGreen, aBlue);
|
|
}
|
|
|
|
} // namespace dom
|
|
} // namespace mozilla
|