Bug 1945643 - Update to mozilla-nimbus-schemas 2025.1.1 r=chumphreys
[gecko.git] / dom / gamepad / windows / WindowsGamepad.cpp
blob2bf76406fa5b474932b2336dc8cfb2831d2d41d8
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include <algorithm>
8 #include <cstddef>
10 #ifndef UNICODE
11 # define UNICODE
12 #endif
13 #include <windows.h>
14 #include <hidsdi.h>
15 #include <stdio.h>
16 #include <xinput.h>
18 #include "nsITimer.h"
19 #include "nsTArray.h"
20 #include "nsThreadUtils.h"
21 #include "nsWindowsHelpers.h"
23 #include "mozilla/ArrayUtils.h"
25 #include "mozilla/ipc/BackgroundParent.h"
26 #include "mozilla/dom/GamepadPlatformService.h"
27 #include "mozilla/dom/GamepadRemapping.h"
28 #include "Gamepad.h"
30 namespace {
32 using namespace mozilla;
33 using namespace mozilla::dom;
35 // USB HID usage tables, page 1, 0x30 = X
36 const uint32_t kAxisMinimumUsageNumber = 0x30;
37 // USB HID usage tables, page 1 (Hat switch)
38 const uint32_t kAxesLengthCap = 16;
40 // USB HID usage tables
41 const uint32_t kDesktopUsagePage = 0x1;
42 const uint32_t kButtonUsagePage = 0x9;
44 // Multiple devices-changed notifications can be sent when a device
45 // is connected, because USB devices consist of multiple logical devices.
46 // Therefore, we wait a bit after receiving one before looking for
47 // device changes.
48 const uint32_t kDevicesChangedStableDelay = 200;
49 // Both DirectInput and XInput are polling-driven here,
50 // so we need to poll it periodically.
51 // 4ms, or 250 Hz, is consistent with Chrome's gamepad implementation.
52 const uint32_t kWindowsGamepadPollInterval = 4;
54 const UINT kRawInputError = (UINT)-1;
56 // In XInputGetState, we can't get the state of Xbox Guide button.
57 // We need to go through the undocumented XInputGetStateEx method
58 // to get that button's state.
59 const LPCSTR kXInputGetStateExOrdinal = (LPCSTR)100;
60 // Bitmask for the Guide button in XInputGamepadEx.wButtons.
61 const int XINPUT_GAMEPAD_Guide = 0x0400;
63 #ifndef XUSER_MAX_COUNT
64 # define XUSER_MAX_COUNT 4
65 #endif
67 const struct {
68 int usagePage;
69 int usage;
70 } kUsagePages[] = {
71 // USB HID usage tables, page 1
72 {kDesktopUsagePage, 4}, // Joystick
73 {kDesktopUsagePage, 5} // Gamepad
76 const struct {
77 WORD button;
78 int mapped;
79 } kXIButtonMap[] = {{XINPUT_GAMEPAD_DPAD_UP, 12},
80 {XINPUT_GAMEPAD_DPAD_DOWN, 13},
81 {XINPUT_GAMEPAD_DPAD_LEFT, 14},
82 {XINPUT_GAMEPAD_DPAD_RIGHT, 15},
83 {XINPUT_GAMEPAD_START, 9},
84 {XINPUT_GAMEPAD_BACK, 8},
85 {XINPUT_GAMEPAD_LEFT_THUMB, 10},
86 {XINPUT_GAMEPAD_RIGHT_THUMB, 11},
87 {XINPUT_GAMEPAD_LEFT_SHOULDER, 4},
88 {XINPUT_GAMEPAD_RIGHT_SHOULDER, 5},
89 {XINPUT_GAMEPAD_Guide, 16},
90 {XINPUT_GAMEPAD_A, 0},
91 {XINPUT_GAMEPAD_B, 1},
92 {XINPUT_GAMEPAD_X, 2},
93 {XINPUT_GAMEPAD_Y, 3}};
94 const size_t kNumMappings = std::size(kXIButtonMap);
96 enum GamepadType { kNoGamepad = 0, kRawInputGamepad, kXInputGamepad };
98 class WindowsGamepadService;
99 // This pointer holds a windows gamepad backend service,
100 // it will be created and destroyed by background thread and
101 // used by gMonitorThread
102 WindowsGamepadService* MOZ_NON_OWNING_REF gService = nullptr;
103 MOZ_RUNINIT nsCOMPtr<nsIThread> gMonitorThread = nullptr;
104 static bool sIsShutdown = false;
106 class Gamepad {
107 public:
108 GamepadType type;
110 // Handle to raw input device
111 HANDLE handle;
113 // XInput Index of the user's controller. Passed to XInputGetState.
114 DWORD userIndex;
116 // Last-known state of the controller.
117 XINPUT_STATE state;
119 // Handle from the GamepadService
120 GamepadHandle gamepadHandle;
122 // Information about the physical device.
123 unsigned numAxes;
124 unsigned numButtons;
126 nsTArray<bool> buttons;
127 struct axisValue {
128 HIDP_VALUE_CAPS caps;
129 double value;
130 bool active;
132 axisValue() : value(0.0f), active(false) {}
133 explicit axisValue(const HIDP_VALUE_CAPS& aCaps)
134 : caps(aCaps), value(0.0f), active(true) {}
136 nsTArray<axisValue> axes;
138 RefPtr<GamepadRemapper> remapper;
140 // Used during rescan to find devices that were disconnected.
141 bool present;
143 Gamepad(uint32_t aNumAxes, uint32_t aNumButtons, GamepadType aType)
144 : type(aType), numAxes(aNumAxes), numButtons(aNumButtons), present(true) {
145 buttons.SetLength(numButtons);
146 axes.SetLength(numAxes);
149 private:
150 Gamepad() {}
153 // Drop this in favor of decltype when we require a new enough SDK.
154 using XInputEnable_func = void(WINAPI*)(BOOL);
156 // RAII class to wrap loading the XInput DLL
157 class XInputLoader {
158 public:
159 XInputLoader()
160 : module(nullptr), mXInputGetState(nullptr), mXInputEnable(nullptr) {
161 // xinput1_4.dll exists on Windows 8
162 // xinput9_1_0.dll exists on Windows 7 and Vista
163 // xinput1_3.dll shipped with the DirectX SDK
164 const wchar_t* dlls[] = {L"xinput1_4.dll", L"xinput9_1_0.dll",
165 L"xinput1_3.dll"};
166 const size_t kNumDLLs = std::size(dlls);
167 for (size_t i = 0; i < kNumDLLs; ++i) {
168 module = LoadLibraryW(dlls[i]);
169 if (module) {
170 mXInputEnable = reinterpret_cast<XInputEnable_func>(
171 GetProcAddress(module, "XInputEnable"));
172 // Checking if `XInputGetStateEx` is available. If not,
173 // we will fallback to use `XInputGetState`.
174 mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>(
175 GetProcAddress(module, kXInputGetStateExOrdinal));
176 if (!mXInputGetState) {
177 mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>(
178 GetProcAddress(module, "XInputGetState"));
180 MOZ_ASSERT(mXInputGetState &&
181 "XInputGetState must be linked successfully.");
183 if (mXInputEnable) {
184 mXInputEnable(TRUE);
186 break;
191 ~XInputLoader() {
192 mXInputEnable = nullptr;
193 mXInputGetState = nullptr;
195 if (module) {
196 FreeLibrary(module);
200 explicit operator bool() { return module && mXInputGetState; }
202 HMODULE module;
203 decltype(XInputGetState)* mXInputGetState;
204 XInputEnable_func mXInputEnable;
207 bool GetPreparsedData(HANDLE handle, nsTArray<uint8_t>& data) {
208 UINT size;
209 if (GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, nullptr, &size) ==
210 kRawInputError) {
211 return false;
213 data.SetLength(size);
214 return GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, data.Elements(),
215 &size) > 0;
219 * Given an axis value and a minimum and maximum range,
220 * scale it to be in the range -1.0 .. 1.0.
222 double ScaleAxis(ULONG value, LONG min, LONG max) {
223 return 2.0 * (value - min) / (max - min) - 1.0;
227 * Return true if this USB HID usage page and usage are of a type we
228 * know how to handle.
230 bool SupportedUsage(USHORT page, USHORT usage) {
231 for (unsigned i = 0; i < std::size(kUsagePages); i++) {
232 if (page == kUsagePages[i].usagePage && usage == kUsagePages[i].usage) {
233 return true;
236 return false;
239 class HIDLoader {
240 public:
241 HIDLoader()
242 : mHidD_GetProductString(nullptr),
243 mHidP_GetCaps(nullptr),
244 mHidP_GetButtonCaps(nullptr),
245 mHidP_GetValueCaps(nullptr),
246 mHidP_GetUsages(nullptr),
247 mHidP_GetUsageValue(nullptr),
248 mHidP_GetScaledUsageValue(nullptr),
249 mModule(LoadLibraryW(L"hid.dll")) {
250 if (mModule) {
251 mHidD_GetProductString =
252 reinterpret_cast<decltype(HidD_GetProductString)*>(
253 GetProcAddress(mModule, "HidD_GetProductString"));
254 mHidP_GetCaps = reinterpret_cast<decltype(HidP_GetCaps)*>(
255 GetProcAddress(mModule, "HidP_GetCaps"));
256 mHidP_GetButtonCaps = reinterpret_cast<decltype(HidP_GetButtonCaps)*>(
257 GetProcAddress(mModule, "HidP_GetButtonCaps"));
258 mHidP_GetValueCaps = reinterpret_cast<decltype(HidP_GetValueCaps)*>(
259 GetProcAddress(mModule, "HidP_GetValueCaps"));
260 mHidP_GetUsages = reinterpret_cast<decltype(HidP_GetUsages)*>(
261 GetProcAddress(mModule, "HidP_GetUsages"));
262 mHidP_GetUsageValue = reinterpret_cast<decltype(HidP_GetUsageValue)*>(
263 GetProcAddress(mModule, "HidP_GetUsageValue"));
264 mHidP_GetScaledUsageValue =
265 reinterpret_cast<decltype(HidP_GetScaledUsageValue)*>(
266 GetProcAddress(mModule, "HidP_GetScaledUsageValue"));
270 ~HIDLoader() {
271 if (mModule) {
272 FreeLibrary(mModule);
276 explicit operator bool() {
277 return mModule && mHidD_GetProductString && mHidP_GetCaps &&
278 mHidP_GetButtonCaps && mHidP_GetValueCaps && mHidP_GetUsages &&
279 mHidP_GetUsageValue && mHidP_GetScaledUsageValue;
282 decltype(HidD_GetProductString)* mHidD_GetProductString;
283 decltype(HidP_GetCaps)* mHidP_GetCaps;
284 decltype(HidP_GetButtonCaps)* mHidP_GetButtonCaps;
285 decltype(HidP_GetValueCaps)* mHidP_GetValueCaps;
286 decltype(HidP_GetUsages)* mHidP_GetUsages;
287 decltype(HidP_GetUsageValue)* mHidP_GetUsageValue;
288 decltype(HidP_GetScaledUsageValue)* mHidP_GetScaledUsageValue;
290 private:
291 HMODULE mModule;
294 HWND sHWnd = nullptr;
296 static void DirectInputMessageLoopOnceCallback(nsITimer* aTimer,
297 void* aClosure) {
298 MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
299 MSG msg;
300 while (PeekMessageW(&msg, sHWnd, 0, 0, PM_REMOVE) > 0) {
301 TranslateMessage(&msg);
302 DispatchMessage(&msg);
304 aTimer->Cancel();
305 if (!sIsShutdown) {
306 aTimer->InitWithNamedFuncCallback(DirectInputMessageLoopOnceCallback,
307 nullptr, kWindowsGamepadPollInterval,
308 nsITimer::TYPE_ONE_SHOT,
309 "DirectInputMessageLoopOnceCallback");
313 class WindowsGamepadService {
314 public:
315 WindowsGamepadService() {
316 mDirectInputTimer = NS_NewTimer();
317 mXInputTimer = NS_NewTimer();
318 mDeviceChangeTimer = NS_NewTimer();
320 virtual ~WindowsGamepadService() { Cleanup(); }
322 void DevicesChanged(bool aIsStablizing);
324 void StartMessageLoop() {
325 MOZ_ASSERT(mDirectInputTimer);
326 mDirectInputTimer->InitWithNamedFuncCallback(
327 DirectInputMessageLoopOnceCallback, nullptr,
328 kWindowsGamepadPollInterval, nsITimer::TYPE_ONE_SHOT,
329 "DirectInputMessageLoopOnceCallback");
332 void Startup();
333 void Shutdown();
334 // Parse gamepad input from a WM_INPUT message.
335 bool HandleRawInput(HRAWINPUT handle);
336 void SetLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle,
337 const Tainted<uint32_t>& aLightColorIndex,
338 const uint8_t& aRed, const uint8_t& aGreen,
339 const uint8_t& aBlue);
340 size_t WriteOutputReport(const std::vector<uint8_t>& aReport);
341 static void XInputMessageLoopOnceCallback(nsITimer* aTimer, void* aClosure);
342 static void DevicesChangeCallback(nsITimer* aTimer, void* aService);
344 private:
345 void ScanForDevices();
346 // Look for connected raw input devices.
347 void ScanForRawInputDevices();
348 // Look for connected XInput devices.
349 bool ScanForXInputDevices();
350 bool HaveXInputGamepad(unsigned int userIndex);
352 bool mIsXInputMonitoring;
353 void PollXInput();
354 void CheckXInputChanges(Gamepad& gamepad, XINPUT_STATE& state);
356 // Get information about a raw input gamepad.
357 bool GetRawGamepad(HANDLE handle);
358 void Cleanup();
360 // List of connected devices.
361 nsTArray<Gamepad> mGamepads;
363 HIDLoader mHID;
364 nsAutoHandle mHidHandle;
365 XInputLoader mXInput;
367 nsCOMPtr<nsITimer> mDirectInputTimer;
368 nsCOMPtr<nsITimer> mXInputTimer;
369 nsCOMPtr<nsITimer> mDeviceChangeTimer;
372 void WindowsGamepadService::ScanForRawInputDevices() {
373 if (!mHID) {
374 return;
377 UINT numDevices;
378 if (GetRawInputDeviceList(nullptr, &numDevices, sizeof(RAWINPUTDEVICELIST)) ==
379 kRawInputError) {
380 return;
382 nsTArray<RAWINPUTDEVICELIST> devices(numDevices);
383 devices.SetLength(numDevices);
384 if (GetRawInputDeviceList(devices.Elements(), &numDevices,
385 sizeof(RAWINPUTDEVICELIST)) == kRawInputError) {
386 return;
389 for (unsigned i = 0; i < devices.Length(); i++) {
390 if (devices[i].dwType == RIM_TYPEHID) {
391 GetRawGamepad(devices[i].hDevice);
396 // static
397 void WindowsGamepadService::XInputMessageLoopOnceCallback(nsITimer* aTimer,
398 void* aService) {
399 MOZ_ASSERT(aService);
400 WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService);
401 self->PollXInput();
402 if (self->mIsXInputMonitoring) {
403 aTimer->Cancel();
404 aTimer->InitWithNamedFuncCallback(
405 XInputMessageLoopOnceCallback, self, kWindowsGamepadPollInterval,
406 nsITimer::TYPE_ONE_SHOT, "XInputMessageLoopOnceCallback");
410 // static
411 void WindowsGamepadService::DevicesChangeCallback(nsITimer* aTimer,
412 void* aService) {
413 MOZ_ASSERT(aService);
414 WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService);
415 self->DevicesChanged(false);
418 bool WindowsGamepadService::HaveXInputGamepad(unsigned int userIndex) {
419 for (unsigned int i = 0; i < mGamepads.Length(); i++) {
420 if (mGamepads[i].type == kXInputGamepad &&
421 mGamepads[i].userIndex == userIndex) {
422 mGamepads[i].present = true;
423 return true;
426 return false;
429 bool WindowsGamepadService::ScanForXInputDevices() {
430 MOZ_ASSERT(mXInput, "XInput should be present!");
432 bool found = false;
433 RefPtr<GamepadPlatformService> service =
434 GamepadPlatformService::GetParentService();
435 if (!service) {
436 return found;
439 for (unsigned int i = 0; i < XUSER_MAX_COUNT; i++) {
440 XINPUT_STATE state = {};
442 if (!mXInput.mXInputGetState ||
443 mXInput.mXInputGetState(i, &state) != ERROR_SUCCESS) {
444 continue;
447 found = true;
448 // See if this device is already present in our list.
449 if (HaveXInputGamepad(i)) {
450 continue;
453 // Not already present, add it.
454 Gamepad gamepad(kStandardGamepadAxes, kStandardGamepadButtons,
455 kXInputGamepad);
456 gamepad.userIndex = i;
457 gamepad.state = state;
458 gamepad.gamepadHandle = service->AddGamepad(
459 "xinput", GamepadMappingType::Standard, GamepadHand::_empty,
460 kStandardGamepadButtons, kStandardGamepadAxes, 0, 0,
461 0); // TODO: Bug 680289, implement gamepad haptics for Windows.
462 mGamepads.AppendElement(std::move(gamepad));
465 return found;
468 void WindowsGamepadService::ScanForDevices() {
469 RefPtr<GamepadPlatformService> service =
470 GamepadPlatformService::GetParentService();
471 if (!service) {
472 return;
475 for (int i = mGamepads.Length() - 1; i >= 0; i--) {
476 mGamepads[i].present = false;
479 if (mHID) {
480 ScanForRawInputDevices();
482 if (mXInput) {
483 mXInputTimer->Cancel();
484 if (ScanForXInputDevices()) {
485 mIsXInputMonitoring = true;
486 mXInputTimer->InitWithNamedFuncCallback(
487 XInputMessageLoopOnceCallback, this, kWindowsGamepadPollInterval,
488 nsITimer::TYPE_ONE_SHOT, "XInputMessageLoopOnceCallback");
489 } else {
490 mIsXInputMonitoring = false;
494 // Look for devices that are no longer present and remove them.
495 for (int i = mGamepads.Length() - 1; i >= 0; i--) {
496 if (!mGamepads[i].present) {
497 service->RemoveGamepad(mGamepads[i].gamepadHandle);
498 mGamepads.RemoveElementAt(i);
503 void WindowsGamepadService::PollXInput() {
504 for (unsigned int i = 0; i < mGamepads.Length(); i++) {
505 if (mGamepads[i].type != kXInputGamepad) {
506 continue;
509 XINPUT_STATE state = {};
511 if (!mXInput.mXInputGetState ||
512 mXInput.mXInputGetState(i, &state) != ERROR_SUCCESS) {
513 continue;
516 if (state.dwPacketNumber != mGamepads[i].state.dwPacketNumber) {
517 CheckXInputChanges(mGamepads[i], state);
522 void WindowsGamepadService::CheckXInputChanges(Gamepad& gamepad,
523 XINPUT_STATE& state) {
524 RefPtr<GamepadPlatformService> service =
525 GamepadPlatformService::GetParentService();
526 if (!service) {
527 return;
529 // Handle digital buttons first
530 for (size_t b = 0; b < kNumMappings; b++) {
531 if (state.Gamepad.wButtons & kXIButtonMap[b].button &&
532 !(gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button)) {
533 // Button pressed
534 service->NewButtonEvent(gamepad.gamepadHandle, kXIButtonMap[b].mapped,
535 true);
536 } else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) &&
537 gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) {
538 // Button released
539 service->NewButtonEvent(gamepad.gamepadHandle, kXIButtonMap[b].mapped,
540 false);
544 // Then triggers
545 if (state.Gamepad.bLeftTrigger != gamepad.state.Gamepad.bLeftTrigger) {
546 const bool pressed =
547 state.Gamepad.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
548 service->NewButtonEvent(gamepad.gamepadHandle, kButtonLeftTrigger, pressed,
549 state.Gamepad.bLeftTrigger / 255.0);
551 if (state.Gamepad.bRightTrigger != gamepad.state.Gamepad.bRightTrigger) {
552 const bool pressed =
553 state.Gamepad.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
554 service->NewButtonEvent(gamepad.gamepadHandle, kButtonRightTrigger, pressed,
555 state.Gamepad.bRightTrigger / 255.0);
558 // Finally deal with analog sticks
559 // TODO: bug 1001955 - Support deadzones.
560 if (state.Gamepad.sThumbLX != gamepad.state.Gamepad.sThumbLX) {
561 const float div = state.Gamepad.sThumbLX > 0 ? 32767.0 : 32768.0;
562 service->NewAxisMoveEvent(gamepad.gamepadHandle, kLeftStickXAxis,
563 state.Gamepad.sThumbLX / div);
565 if (state.Gamepad.sThumbLY != gamepad.state.Gamepad.sThumbLY) {
566 const float div = state.Gamepad.sThumbLY > 0 ? 32767.0 : 32768.0;
567 service->NewAxisMoveEvent(gamepad.gamepadHandle, kLeftStickYAxis,
568 -1.0 * state.Gamepad.sThumbLY / div);
570 if (state.Gamepad.sThumbRX != gamepad.state.Gamepad.sThumbRX) {
571 const float div = state.Gamepad.sThumbRX > 0 ? 32767.0 : 32768.0;
572 service->NewAxisMoveEvent(gamepad.gamepadHandle, kRightStickXAxis,
573 state.Gamepad.sThumbRX / div);
575 if (state.Gamepad.sThumbRY != gamepad.state.Gamepad.sThumbRY) {
576 const float div = state.Gamepad.sThumbRY > 0 ? 32767.0 : 32768.0;
577 service->NewAxisMoveEvent(gamepad.gamepadHandle, kRightStickYAxis,
578 -1.0 * state.Gamepad.sThumbRY / div);
580 gamepad.state = state;
583 // Used to sort a list of axes by HID usage.
584 class HidValueComparator {
585 public:
586 bool Equals(const Gamepad::axisValue& c1,
587 const Gamepad::axisValue& c2) const {
588 return c1.caps.UsagePage == c2.caps.UsagePage &&
589 c1.caps.Range.UsageMin == c2.caps.Range.UsageMin;
591 bool LessThan(const Gamepad::axisValue& c1,
592 const Gamepad::axisValue& c2) const {
593 if (c1.caps.UsagePage == c2.caps.UsagePage) {
594 return c1.caps.Range.UsageMin < c2.caps.Range.UsageMin;
596 return c1.caps.UsagePage < c2.caps.UsagePage;
600 // GetRawGamepad() processes its raw data from HID and
601 // then trying to remapping buttons and axes based on
602 // the mapping rules that are defined for different gamepad products.
603 bool WindowsGamepadService::GetRawGamepad(HANDLE handle) {
604 RefPtr<GamepadPlatformService> service =
605 GamepadPlatformService::GetParentService();
606 if (!service) {
607 return true;
610 if (!mHID) {
611 return false;
614 for (unsigned i = 0; i < mGamepads.Length(); i++) {
615 if (mGamepads[i].type == kRawInputGamepad &&
616 mGamepads[i].handle == handle) {
617 mGamepads[i].present = true;
618 return true;
622 RID_DEVICE_INFO rdi = {};
623 UINT size = rdi.cbSize = sizeof(RID_DEVICE_INFO);
624 if (GetRawInputDeviceInfo(handle, RIDI_DEVICEINFO, &rdi, &size) ==
625 kRawInputError) {
626 return false;
628 // Ensure that this is a device we care about
629 if (!SupportedUsage(rdi.hid.usUsagePage, rdi.hid.usUsage)) {
630 return false;
633 // Device name is a mostly-opaque string.
634 if (GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, nullptr, &size) ==
635 kRawInputError) {
636 return false;
639 nsTArray<wchar_t> devname(size);
640 devname.SetLength(size);
641 if (GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, devname.Elements(),
642 &size) == kRawInputError) {
643 return false;
646 // Per http://msdn.microsoft.com/en-us/library/windows/desktop/ee417014.aspx
647 // device names containing "IG_" are XInput controllers. Ignore those
648 // devices since we'll handle them with XInput.
649 if (wcsstr(devname.Elements(), L"IG_")) {
650 return false;
653 // Product string is a human-readable name.
654 // Per
655 // http://msdn.microsoft.com/en-us/library/windows/hardware/ff539681%28v=vs.85%29.aspx
656 // "For USB devices, the maximum string length is 126 wide characters (not
657 // including the terminating NULL character)."
658 wchar_t name[128] = {0};
659 size = sizeof(name);
660 nsTArray<char> gamepad_name;
661 // Creating this file with FILE_FLAG_OVERLAPPED to perform
662 // an asynchronous request in WriteOutputReport.
663 mHidHandle.own(CreateFile(devname.Elements(), GENERIC_READ | GENERIC_WRITE,
664 FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
665 OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr));
666 if (mHidHandle != INVALID_HANDLE_VALUE) {
667 if (mHID.mHidD_GetProductString(mHidHandle, &name, size)) {
668 int bytes = WideCharToMultiByte(CP_UTF8, 0, name, -1, nullptr, 0, nullptr,
669 nullptr);
670 gamepad_name.SetLength(bytes);
671 WideCharToMultiByte(CP_UTF8, 0, name, -1, gamepad_name.Elements(), bytes,
672 nullptr, nullptr);
675 if (gamepad_name.Length() == 0 || !gamepad_name[0]) {
676 const char kUnknown[] = "Unknown Gamepad";
677 gamepad_name.SetLength(std::size(kUnknown));
678 strcpy_s(gamepad_name.Elements(), gamepad_name.Length(), kUnknown);
681 char gamepad_id[256] = {0};
682 _snprintf_s(gamepad_id, _TRUNCATE, "%04x-%04x-%s", rdi.hid.dwVendorId,
683 rdi.hid.dwProductId, gamepad_name.Elements());
685 nsTArray<uint8_t> preparsedbytes;
686 if (!GetPreparsedData(handle, preparsedbytes)) {
687 return false;
690 PHIDP_PREPARSED_DATA parsed =
691 reinterpret_cast<PHIDP_PREPARSED_DATA>(preparsedbytes.Elements());
692 HIDP_CAPS caps;
693 if (mHID.mHidP_GetCaps(parsed, &caps) != HIDP_STATUS_SUCCESS) {
694 return false;
697 // Enumerate buttons.
698 USHORT count = caps.NumberInputButtonCaps;
699 nsTArray<HIDP_BUTTON_CAPS> buttonCaps(count);
700 buttonCaps.SetLength(count);
701 if (mHID.mHidP_GetButtonCaps(HidP_Input, buttonCaps.Elements(), &count,
702 parsed) != HIDP_STATUS_SUCCESS) {
703 return false;
705 uint32_t numButtons = 0;
706 for (unsigned i = 0; i < count; i++) {
707 // Each buttonCaps is typically a range of buttons.
708 numButtons +=
709 buttonCaps[i].Range.UsageMax - buttonCaps[i].Range.UsageMin + 1;
712 // Enumerate value caps, which represent axes and d-pads.
713 count = caps.NumberInputValueCaps;
714 nsTArray<HIDP_VALUE_CAPS> axisCaps(count);
715 axisCaps.SetLength(count);
716 if (mHID.mHidP_GetValueCaps(HidP_Input, axisCaps.Elements(), &count,
717 parsed) != HIDP_STATUS_SUCCESS) {
718 return false;
721 size_t numAxes = 0;
722 nsTArray<Gamepad::axisValue> axes(kAxesLengthCap);
723 // We store these value caps and handle the dpad info in GamepadRemapper
724 // later.
725 axes.SetLength(kAxesLengthCap);
727 // Looking for the exisiting ramapping rule.
728 bool defaultRemapper = false;
729 RefPtr<GamepadRemapper> remapper = GetGamepadRemapper(
730 rdi.hid.dwVendorId, rdi.hid.dwProductId, defaultRemapper);
731 MOZ_ASSERT(remapper);
733 for (size_t i = 0; i < count; i++) {
734 const size_t axisIndex =
735 axisCaps[i].Range.UsageMin - kAxisMinimumUsageNumber;
736 if (axisIndex < kAxesLengthCap && !axes[axisIndex].active) {
737 axes[axisIndex].caps = axisCaps[i];
738 axes[axisIndex].active = true;
739 numAxes = std::max(numAxes, axisIndex + 1);
743 // Not already present, add it.
745 remapper->SetAxisCount(numAxes);
746 remapper->SetButtonCount(numButtons);
747 Gamepad gamepad(numAxes, numButtons, kRawInputGamepad);
748 gamepad.handle = handle;
750 for (unsigned i = 0; i < gamepad.numAxes; i++) {
751 gamepad.axes[i] = axes[i];
754 gamepad.remapper = remapper.forget();
755 // TODO: Bug 680289, implement gamepad haptics for Windows.
756 gamepad.gamepadHandle = service->AddGamepad(
757 gamepad_id, gamepad.remapper->GetMappingType(), GamepadHand::_empty,
758 gamepad.remapper->GetButtonCount(), gamepad.remapper->GetAxisCount(), 0,
759 gamepad.remapper->GetLightIndicatorCount(),
760 gamepad.remapper->GetTouchEventCount());
762 nsTArray<GamepadLightIndicatorType> lightTypes;
763 gamepad.remapper->GetLightIndicators(lightTypes);
764 for (uint32_t i = 0; i < lightTypes.Length(); ++i) {
765 if (lightTypes[i] != GamepadLightIndicator::DefaultType()) {
766 service->NewLightIndicatorTypeEvent(gamepad.gamepadHandle, i,
767 lightTypes[i]);
771 mGamepads.AppendElement(std::move(gamepad));
772 return true;
775 bool WindowsGamepadService::HandleRawInput(HRAWINPUT handle) {
776 if (!mHID) {
777 return false;
780 RefPtr<GamepadPlatformService> service =
781 GamepadPlatformService::GetParentService();
782 if (!service) {
783 return false;
786 // First, get data from the handle
787 UINT size;
788 GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER));
789 nsTArray<uint8_t> data(size);
790 data.SetLength(size);
791 if (GetRawInputData(handle, RID_INPUT, data.Elements(), &size,
792 sizeof(RAWINPUTHEADER)) == kRawInputError) {
793 return false;
795 PRAWINPUT raw = reinterpret_cast<PRAWINPUT>(data.Elements());
797 Gamepad* gamepad = nullptr;
798 for (unsigned i = 0; i < mGamepads.Length(); i++) {
799 if (mGamepads[i].type == kRawInputGamepad &&
800 mGamepads[i].handle == raw->header.hDevice) {
801 gamepad = &mGamepads[i];
802 break;
805 if (gamepad == nullptr) {
806 return false;
809 // Second, get the preparsed data
810 nsTArray<uint8_t> parsedbytes;
811 if (!GetPreparsedData(raw->header.hDevice, parsedbytes)) {
812 return false;
814 PHIDP_PREPARSED_DATA parsed =
815 reinterpret_cast<PHIDP_PREPARSED_DATA>(parsedbytes.Elements());
817 // Get all the pressed buttons.
818 nsTArray<USAGE> usages(gamepad->numButtons);
819 usages.SetLength(gamepad->numButtons);
820 ULONG usageLength = gamepad->numButtons;
821 if (mHID.mHidP_GetUsages(HidP_Input, kButtonUsagePage, 0, usages.Elements(),
822 &usageLength, parsed, (PCHAR)raw->data.hid.bRawData,
823 raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
824 return false;
827 nsTArray<bool> buttons(gamepad->numButtons);
828 buttons.SetLength(gamepad->numButtons);
829 // If we don't zero out the buttons array first, sometimes it can reuse
830 // values.
831 memset(buttons.Elements(), 0, gamepad->numButtons * sizeof(bool));
833 for (unsigned i = 0; i < usageLength; i++) {
834 // The button index in usages may be larger than what we detected when
835 // enumerating gamepads. If so, warn and continue.
837 // Usage ID of 0 is reserved, so it should always be 1 or higher.
838 if (NS_WARN_IF((usages[i] - 1u) >= buttons.Length())) {
839 continue;
841 buttons[usages[i] - 1u] = true;
844 for (unsigned i = 0; i < gamepad->numButtons; i++) {
845 if (gamepad->buttons[i] != buttons[i]) {
846 gamepad->remapper->RemapButtonEvent(gamepad->gamepadHandle, i,
847 buttons[i]);
848 gamepad->buttons[i] = buttons[i];
852 // Get all axis values.
853 for (unsigned i = 0; i < gamepad->numAxes; i++) {
854 double new_value;
855 if (gamepad->axes[i].caps.LogicalMin < 0) {
856 LONG value;
857 if (mHID.mHidP_GetScaledUsageValue(
858 HidP_Input, gamepad->axes[i].caps.UsagePage, 0,
859 gamepad->axes[i].caps.Range.UsageMin, &value, parsed,
860 (PCHAR)raw->data.hid.bRawData,
861 raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
862 continue;
864 new_value = ScaleAxis(value, gamepad->axes[i].caps.LogicalMin,
865 gamepad->axes[i].caps.LogicalMax);
866 } else {
867 ULONG value;
868 if (mHID.mHidP_GetUsageValue(
869 HidP_Input, gamepad->axes[i].caps.UsagePage, 0,
870 gamepad->axes[i].caps.Range.UsageMin, &value, parsed,
871 (PCHAR)raw->data.hid.bRawData,
872 raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
873 continue;
876 new_value = ScaleAxis(value, gamepad->axes[i].caps.LogicalMin,
877 gamepad->axes[i].caps.LogicalMax);
879 if (gamepad->axes[i].value != new_value) {
880 gamepad->remapper->RemapAxisMoveEvent(gamepad->gamepadHandle, i,
881 new_value);
882 gamepad->axes[i].value = new_value;
886 BYTE* rawData = raw->data.hid.bRawData;
887 gamepad->remapper->ProcessTouchData(gamepad->gamepadHandle, rawData);
889 return true;
892 void WindowsGamepadService::SetLightIndicatorColor(
893 const Tainted<GamepadHandle>& aGamepadHandle,
894 const Tainted<uint32_t>& aLightColorIndex, const uint8_t& aRed,
895 const uint8_t& aGreen, const uint8_t& aBlue) {
896 // We get aControllerIdx from GamepadPlatformService::AddGamepad(),
897 // It begins from 1 and is stored at Gamepad.id.
898 const Gamepad* gamepad = (MOZ_FIND_AND_VALIDATE(
899 aGamepadHandle, list_item.gamepadHandle == aGamepadHandle, mGamepads));
900 if (!gamepad) {
901 MOZ_ASSERT(false);
902 return;
905 RefPtr<GamepadRemapper> remapper = gamepad->remapper;
906 if (!remapper ||
907 MOZ_IS_VALID(aLightColorIndex,
908 remapper->GetLightIndicatorCount() <= aLightColorIndex)) {
909 MOZ_ASSERT(false);
910 return;
913 std::vector<uint8_t> report;
914 remapper->GetLightColorReport(aRed, aGreen, aBlue, report);
915 WriteOutputReport(report);
918 size_t WindowsGamepadService::WriteOutputReport(
919 const std::vector<uint8_t>& aReport) {
920 DCHECK(static_cast<const void*>(aReport.data()));
921 DCHECK_GE(aReport.size(), 1U);
922 if (!mHidHandle) return 0;
924 nsAutoHandle eventHandle(::CreateEvent(nullptr, FALSE, FALSE, nullptr));
925 OVERLAPPED overlapped = {0};
926 overlapped.hEvent = eventHandle;
928 // Doing an asynchronous write to allows us to time out
929 // if the write takes too long.
930 DWORD bytesWritten = 0;
931 BOOL writeSuccess =
932 ::WriteFile(mHidHandle, static_cast<const void*>(aReport.data()),
933 aReport.size(), &bytesWritten, &overlapped);
934 if (!writeSuccess) {
935 DWORD error = ::GetLastError();
936 if (error == ERROR_IO_PENDING) {
937 // Wait for the write to complete. This causes WriteOutputReport to behave
938 // synchronously but with a timeout.
939 DWORD wait_object = ::WaitForSingleObject(overlapped.hEvent, 100);
940 if (wait_object == WAIT_OBJECT_0) {
941 if (!::GetOverlappedResult(mHidHandle, &overlapped, &bytesWritten,
942 TRUE)) {
943 return 0;
945 } else {
946 // Wait failed, or the timeout was exceeded before the write completed.
947 // Cancel the write request.
948 if (::CancelIo(mHidHandle)) {
949 wait_object = ::WaitForSingleObject(overlapped.hEvent, INFINITE);
950 MOZ_ASSERT(wait_object == WAIT_OBJECT_0);
955 return writeSuccess ? bytesWritten : 0;
958 void WindowsGamepadService::Startup() { ScanForDevices(); }
960 void WindowsGamepadService::Shutdown() { Cleanup(); }
962 void WindowsGamepadService::Cleanup() {
963 mIsXInputMonitoring = false;
964 if (mDirectInputTimer) {
965 mDirectInputTimer->Cancel();
967 if (mXInputTimer) {
968 mXInputTimer->Cancel();
970 if (mDeviceChangeTimer) {
971 mDeviceChangeTimer->Cancel();
974 mGamepads.Clear();
977 void WindowsGamepadService::DevicesChanged(bool aIsStablizing) {
978 if (aIsStablizing) {
979 mDeviceChangeTimer->Cancel();
980 mDeviceChangeTimer->InitWithNamedFuncCallback(
981 DevicesChangeCallback, this, kDevicesChangedStableDelay,
982 nsITimer::TYPE_ONE_SHOT, "DevicesChangeCallback");
983 } else {
984 ScanForDevices();
988 bool RegisterRawInput(HWND hwnd, bool enable) {
989 nsTArray<RAWINPUTDEVICE> rid(std::size(kUsagePages));
990 rid.SetLength(std::size(kUsagePages));
992 for (unsigned i = 0; i < rid.Length(); i++) {
993 rid[i].usUsagePage = kUsagePages[i].usagePage;
994 rid[i].usUsage = kUsagePages[i].usage;
995 rid[i].dwFlags =
996 enable ? RIDEV_EXINPUTSINK | RIDEV_DEVNOTIFY : RIDEV_REMOVE;
997 rid[i].hwndTarget = hwnd;
1000 if (!RegisterRawInputDevices(rid.Elements(), rid.Length(),
1001 sizeof(RAWINPUTDEVICE))) {
1002 return false;
1004 return true;
1007 static LRESULT CALLBACK GamepadWindowProc(HWND hwnd, UINT msg, WPARAM wParam,
1008 LPARAM lParam) {
1009 const unsigned int DBT_DEVICEARRIVAL = 0x8000;
1010 const unsigned int DBT_DEVICEREMOVECOMPLETE = 0x8004;
1011 const unsigned int DBT_DEVNODES_CHANGED = 0x7;
1013 switch (msg) {
1014 case WM_DEVICECHANGE:
1015 if (wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE ||
1016 wParam == DBT_DEVNODES_CHANGED) {
1017 if (gService) {
1018 gService->DevicesChanged(true);
1021 break;
1022 case WM_INPUT:
1023 if (gService) {
1024 gService->HandleRawInput(reinterpret_cast<HRAWINPUT>(lParam));
1026 break;
1028 return DefWindowProc(hwnd, msg, wParam, lParam);
1031 class StartWindowsGamepadServiceRunnable final : public Runnable {
1032 public:
1033 StartWindowsGamepadServiceRunnable()
1034 : Runnable("StartWindowsGamepadServiceRunnable") {}
1036 NS_IMETHOD Run() override {
1037 MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
1038 gService = new WindowsGamepadService();
1039 gService->Startup();
1041 if (sHWnd == nullptr) {
1042 WNDCLASSW wc;
1043 HMODULE hSelf = GetModuleHandle(nullptr);
1045 if (!GetClassInfoW(hSelf, L"MozillaGamepadClass", &wc)) {
1046 ZeroMemory(&wc, sizeof(WNDCLASSW));
1047 wc.hInstance = hSelf;
1048 wc.lpfnWndProc = GamepadWindowProc;
1049 wc.lpszClassName = L"MozillaGamepadClass";
1050 RegisterClassW(&wc);
1053 sHWnd = CreateWindowW(L"MozillaGamepadClass", L"Gamepad Watcher", 0, 0, 0,
1054 0, 0, nullptr, nullptr, hSelf, nullptr);
1055 RegisterRawInput(sHWnd, true);
1058 // Explicitly start the message loop
1059 gService->StartMessageLoop();
1061 return NS_OK;
1064 private:
1065 ~StartWindowsGamepadServiceRunnable() {}
1068 class StopWindowsGamepadServiceRunnable final : public Runnable {
1069 public:
1070 StopWindowsGamepadServiceRunnable()
1071 : Runnable("StopWindowsGamepadServiceRunnable") {}
1073 NS_IMETHOD Run() override {
1074 MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
1075 if (sHWnd) {
1076 RegisterRawInput(sHWnd, false);
1077 DestroyWindow(sHWnd);
1078 sHWnd = nullptr;
1081 gService->Shutdown();
1082 delete gService;
1083 gService = nullptr;
1085 return NS_OK;
1088 private:
1089 ~StopWindowsGamepadServiceRunnable() {}
1092 } // namespace
1094 namespace mozilla::dom {
1096 using namespace mozilla::ipc;
1098 void StartGamepadMonitoring() {
1099 AssertIsOnBackgroundThread();
1101 if (gMonitorThread || gService) {
1102 return;
1104 sIsShutdown = false;
1105 NS_NewNamedThread("Gamepad", getter_AddRefs(gMonitorThread));
1106 gMonitorThread->Dispatch(new StartWindowsGamepadServiceRunnable(),
1107 NS_DISPATCH_NORMAL);
1110 void StopGamepadMonitoring() {
1111 AssertIsOnBackgroundThread();
1113 if (sIsShutdown) {
1114 return;
1116 sIsShutdown = true;
1117 gMonitorThread->Dispatch(new StopWindowsGamepadServiceRunnable(),
1118 NS_DISPATCH_NORMAL);
1119 gMonitorThread->Shutdown();
1120 gMonitorThread = nullptr;
1123 void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle,
1124 const Tainted<uint32_t>& aLightColorIndex,
1125 const uint8_t& aRed, const uint8_t& aGreen,
1126 const uint8_t& aBlue) {
1127 MOZ_ASSERT(gService);
1128 if (!gService) {
1129 return;
1131 gService->SetLightIndicatorColor(aGamepadHandle, aLightColorIndex, aRed,
1132 aGreen, aBlue);
1135 } // namespace mozilla::dom