From 71e2d8d3afe42ef96dfa59c9810e01fd851eacfc Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Wed, 3 Mar 2021 18:26:48 +0000 Subject: [PATCH] Bug 1658419 - Use shared memory for gamepad state on Windows r=handyman Finally, use the primitives from the previous change to deliver gamepad changes. If the shared memory shortcut is available, all gamepad changes will be delivered over it. When the children receive the signal, they will diff their last-known state against the new state and generate events to update JS. Differential Revision: https://phabricator.services.mozilla.com/D105129 --- dom/gamepad/GamepadHandle.h | 2 + dom/gamepad/GamepadManager.cpp | 15 + dom/gamepad/GamepadPlatformService.cpp | 96 +++- dom/gamepad/GamepadPlatformService.h | 2 + dom/gamepad/GamepadStateBroadcaster.h | 36 ++ dom/gamepad/GamepadStateReceiver.h | 31 ++ .../fallback/GamepadStateBroadcaster.cpp | 42 ++ dom/gamepad/fallback/GamepadStateReceiver.cpp | 14 + .../TestGamepadStateBroadcastWindows.cpp | 451 ++++++++++++++++++ dom/gamepad/tests/gtest/moz.build | 1 + .../windows/GamepadStateBroadcaster.cpp | 184 ++++++- dom/gamepad/windows/GamepadStateLayout.h | 78 ++- dom/gamepad/windows/GamepadStateReceiver.cpp | 220 ++++++++- 13 files changed, 1143 insertions(+), 29 deletions(-) create mode 100644 dom/gamepad/tests/gtest/TestGamepadStateBroadcastWindows.cpp diff --git a/dom/gamepad/GamepadHandle.h b/dom/gamepad/GamepadHandle.h index bd3e67f857cb..b42d2ba1a2a2 100644 --- a/dom/gamepad/GamepadHandle.h +++ b/dom/gamepad/GamepadHandle.h @@ -37,6 +37,7 @@ namespace mozilla::dom { class GamepadPlatformService; class GamepadServiceTest; +class GamepadTestHelper; class XRInputSource; // The "kind" of a gamepad handle is based on which provider created it @@ -73,6 +74,7 @@ class GamepadHandle { // create new handles and inspect their actual value friend class mozilla::dom::GamepadPlatformService; friend class mozilla::dom::GamepadServiceTest; + friend class mozilla::dom::GamepadTestHelper; friend class mozilla::dom::XRInputSource; friend class mozilla::gfx::VRDisplayClient; friend class mozilla::gfx::VRManager; diff --git a/dom/gamepad/GamepadManager.cpp b/dom/gamepad/GamepadManager.cpp index 306cb4bbd03d..8a90f42d60a4 100644 --- a/dom/gamepad/GamepadManager.cpp +++ b/dom/gamepad/GamepadManager.cpp @@ -109,6 +109,7 @@ void GamepadManager::BeginShutdown() { mShuttingDown = true; StopMonitoring(); if (mMaybeGamepadStateReceiver) { + mMaybeGamepadStateReceiver->StopMonitoringThread(); mMaybeGamepadStateReceiver = Nothing{}; } // Don't let windows call back to unregister during shutdown @@ -667,5 +668,19 @@ already_AddRefed GamepadManager::SetLightIndicatorColor( void GamepadManager::SetupRemoteInfo( const GamepadStateBroadcastReceiverInfo& aReceiverInfo) { mMaybeGamepadStateReceiver = GamepadStateReceiver::Create(aReceiverInfo); + + if (mMaybeGamepadStateReceiver) { + RefPtr thisRefPtr(this); + bool threadStarted = mMaybeGamepadStateReceiver->StartMonitoringThread( + [thisRefPtr](const GamepadChangeEvent& e) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "GamepadStateReceiver::MonitoringThread", + [thisRefPtr, e] { thisRefPtr->Update(e); })); + }); + + if (!threadStarted) { + mMaybeGamepadStateReceiver = Nothing{}; + } + } } } // namespace mozilla::dom diff --git a/dom/gamepad/GamepadPlatformService.cpp b/dom/gamepad/GamepadPlatformService.cpp index 364c17f00015..f19fcf36bcd1 100644 --- a/dom/gamepad/GamepadPlatformService.cpp +++ b/dom/gamepad/GamepadPlatformService.cpp @@ -80,6 +80,9 @@ void GamepadMonitoringState::Set(bool aIsMonitoring) { } } +// Note - If GamepadStateBroadcaster::Create() fails (either because the +// platform doesn't support it, or if something unexpected goes wrong), +// everything in this class will silently fall back to using IPDL GamepadPlatformService::GamepadPlatformService() : mNextGamepadHandleValue(1), mMutex("mozilla::dom::GamepadPlatformService"), @@ -112,6 +115,12 @@ void GamepadPlatformService::NotifyGamepadChange(GamepadHandle aHandle, MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(!NS_IsMainThread()); + // This function (which uses IPDL) should only ever be used if there + // is no GamepadStateBroadcaster, either because the platform doesn't + // support it (currently only Windows does), or-else because it failed to + // initialize for whatever reason. + MOZ_ASSERT(!mMaybeGamepadStateBroadcaster); + GamepadChangeEventBody body(aInfo); GamepadChangeEvent e(aHandle, body); @@ -136,13 +145,20 @@ GamepadHandle GamepadPlatformService::AddGamepad( GamepadHandle gamepadHandle{mNextGamepadHandleValue++, GamepadHandleKind::GamepadPlatformManager}; - // Only VR controllers has displayID, we give 0 to the general gamepads. - GamepadAdded a(NS_ConvertUTF8toUTF16(nsDependentCString(aID)), aMapping, - aHand, 0, aNumButtons, aNumAxes, aHaptics, aNumLightIndicator, - aNumTouchEvents); + if (mMaybeGamepadStateBroadcaster) { + mMaybeGamepadStateBroadcaster->AddGamepad( + gamepadHandle, aID, aMapping, aHand, aNumButtons, aNumAxes, aHaptics, + aNumLightIndicator, aNumTouchEvents); + } else { + // Only VR controllers has displayID, we give 0 to the general gamepads. + GamepadAdded a(NS_ConvertUTF8toUTF16(nsDependentCString(aID)), aMapping, + aHand, 0, aNumButtons, aNumAxes, aHaptics, + aNumLightIndicator, aNumTouchEvents); + + mGamepadAdded.emplace(gamepadHandle, a); + NotifyGamepadChange(gamepadHandle, a); + } - mGamepadAdded.emplace(gamepadHandle, a); - NotifyGamepadChange(gamepadHandle, a); return gamepadHandle; } @@ -151,9 +167,14 @@ void GamepadPlatformService::RemoveGamepad(GamepadHandle aHandle) { // platform-dependent backends MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(!NS_IsMainThread()); - GamepadRemoved a; - NotifyGamepadChange(aHandle, a); - mGamepadAdded.erase(aHandle); + + if (mMaybeGamepadStateBroadcaster) { + mMaybeGamepadStateBroadcaster->RemoveGamepad(aHandle); + } else { + GamepadRemoved a; + NotifyGamepadChange(aHandle, a); + mGamepadAdded.erase(aHandle); + } } void GamepadPlatformService::NewButtonEvent(GamepadHandle aHandle, @@ -163,8 +184,14 @@ void GamepadPlatformService::NewButtonEvent(GamepadHandle aHandle, // platform-dependent backends MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(!NS_IsMainThread()); - GamepadButtonInformation a(aButton, aValue, aPressed, aTouched); - NotifyGamepadChange(aHandle, a); + + if (mMaybeGamepadStateBroadcaster) { + mMaybeGamepadStateBroadcaster->NewButtonEvent(aHandle, aButton, aPressed, + aTouched, aValue); + } else { + GamepadButtonInformation a(aButton, aValue, aPressed, aTouched); + NotifyGamepadChange(aHandle, a); + } } void GamepadPlatformService::NewButtonEvent(GamepadHandle aHandle, @@ -205,8 +232,12 @@ void GamepadPlatformService::NewAxisMoveEvent(GamepadHandle aHandle, // platform-dependent backends MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(!NS_IsMainThread()); - GamepadAxisInformation a(aAxis, aValue); - NotifyGamepadChange(aHandle, a); + if (mMaybeGamepadStateBroadcaster) { + mMaybeGamepadStateBroadcaster->NewAxisMoveEvent(aHandle, aAxis, aValue); + } else { + GamepadAxisInformation a(aAxis, aValue); + NotifyGamepadChange(aHandle, a); + } } void GamepadPlatformService::NewLightIndicatorTypeEvent( @@ -215,8 +246,13 @@ void GamepadPlatformService::NewLightIndicatorTypeEvent( // platform-dependent backends MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(!NS_IsMainThread()); - GamepadLightIndicatorTypeInformation a(aLight, aType); - NotifyGamepadChange(aHandle, a); + if (mMaybeGamepadStateBroadcaster) { + mMaybeGamepadStateBroadcaster->NewLightIndicatorTypeEvent(aHandle, aLight, + aType); + } else { + GamepadLightIndicatorTypeInformation a(aLight, aType); + NotifyGamepadChange(aHandle, a); + } } void GamepadPlatformService::NewPoseEvent(GamepadHandle aHandle, @@ -225,8 +261,13 @@ void GamepadPlatformService::NewPoseEvent(GamepadHandle aHandle, // platform-dependent backends MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(!NS_IsMainThread()); - GamepadPoseInformation a(aState); - NotifyGamepadChange(aHandle, a); + + if (mMaybeGamepadStateBroadcaster) { + mMaybeGamepadStateBroadcaster->NewPoseEvent(aHandle, aState); + } else { + GamepadPoseInformation a(aState); + NotifyGamepadChange(aHandle, a); + } } void GamepadPlatformService::NewMultiTouchEvent( @@ -237,8 +278,13 @@ void GamepadPlatformService::NewMultiTouchEvent( MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(!NS_IsMainThread()); - GamepadTouchInformation a(aTouchArrayIndex, aState); - NotifyGamepadChange(aHandle, a); + if (mMaybeGamepadStateBroadcaster) { + mMaybeGamepadStateBroadcaster->NewMultiTouchEvent(aHandle, aTouchArrayIndex, + aState); + } else { + GamepadTouchInformation a(aTouchArrayIndex, aState); + NotifyGamepadChange(aHandle, a); + } } void GamepadPlatformService::ResetGamepadIndexes() { @@ -273,11 +319,13 @@ void GamepadPlatformService::AddChannelParent( // For a new GamepadEventChannel, we have to send the exising GamepadAdded // to it to make it can have the same amount of gamepads with others. - if (mChannelParents.Length() > 1) { - for (const auto& evt : mGamepadAdded) { - GamepadChangeEventBody body(evt.second); - GamepadChangeEvent e(evt.first, body); - aParent->DispatchUpdateEvent(e); + if (!mMaybeGamepadStateBroadcaster) { + if (mChannelParents.Length() > 1) { + for (const auto& evt : mGamepadAdded) { + GamepadChangeEventBody body(evt.second); + GamepadChangeEvent e(evt.first, body); + aParent->DispatchUpdateEvent(e); + } } } } diff --git a/dom/gamepad/GamepadPlatformService.h b/dom/gamepad/GamepadPlatformService.h index 04ead31afb10..2bfeeb12898f 100644 --- a/dom/gamepad/GamepadPlatformService.h +++ b/dom/gamepad/GamepadPlatformService.h @@ -146,6 +146,8 @@ class GamepadPlatformService final { std::map mGamepadAdded; + // This variable may contain the shared-memory "GamepadStateBroadcaster" if + // it is available. If not, everything will just fall back to using IPDL Maybe mMaybeGamepadStateBroadcaster; }; diff --git a/dom/gamepad/GamepadStateBroadcaster.h b/dom/gamepad/GamepadStateBroadcaster.h index ce2b1940089e..a6eadb88cd7e 100644 --- a/dom/gamepad/GamepadStateBroadcaster.h +++ b/dom/gamepad/GamepadStateBroadcaster.h @@ -37,6 +37,9 @@ class IProtocol; namespace mozilla::dom { +class GamepadChangeEvent; +class GamepadTestHelper; + // IPDL structure that knows how to initialize a receiver class GamepadStateBroadcastReceiverInfo; @@ -63,6 +66,9 @@ class GamepadStateBroadcastReceiverInfo; // result = aIPCActor->SendBroadcasterInfo(remoteInfo); // MOZ_ASSERT(result); // +// maybeBroadcaster->AddGamepad(aHandle); +// maybeBroadcaster->NewAxisMoveEvent(aHandle, 0 /*axisId*/, 0.0 /*value*/); +// class GamepadStateBroadcaster { public: // Create a new broadcaster @@ -89,6 +95,33 @@ class GamepadStateBroadcaster { // broadcaster side. void RemoveReceiver(const mozilla::ipc::IProtocol* aActor); + // Adds a new gamepad to shared memory region + void AddGamepad(GamepadHandle aHandle, const char* aID, + GamepadMappingType aMapping, GamepadHand aHand, + uint32_t aNumButtons, uint32_t aNumAxes, uint32_t aNumHaptics, + uint32_t aNumLights, uint32_t aNumTouches); + + // Removes a gamepad from the shared memory region + void RemoveGamepad(GamepadHandle aHandle); + + // Update a gamepad axis in the shared memory region + void NewAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, double aValue); + + // Update a gamepad button in the shared memory region + void NewButtonEvent(GamepadHandle aHandle, uint32_t aButton, bool aPressed, + bool aTouched, double aValue); + + // Update a gamepad light indicator in the shared memory region + void NewLightIndicatorTypeEvent(GamepadHandle aHandle, uint32_t aLight, + GamepadLightIndicatorType aType); + + // Update a gamepad pose state in the shared memory region + void NewPoseEvent(GamepadHandle aHandle, const GamepadPoseState& aState); + + // Update a gamepad multi-touch state in the shared memory region + void NewMultiTouchEvent(GamepadHandle aHandle, uint32_t aTouchArrayIndex, + const GamepadTouchState& aState); + // Allow move GamepadStateBroadcaster(GamepadStateBroadcaster&& aOther); GamepadStateBroadcaster& operator=(GamepadStateBroadcaster&& aOther); @@ -106,6 +139,9 @@ class GamepadStateBroadcaster { explicit GamepadStateBroadcaster(UniquePtr aImpl); UniquePtr mImpl; + + friend class GamepadTestHelper; + void SendTestCommand(uint32_t aCommandId); }; } // namespace mozilla::dom diff --git a/dom/gamepad/GamepadStateReceiver.h b/dom/gamepad/GamepadStateReceiver.h index 30591cf32047..d5a6ca8c5cd2 100644 --- a/dom/gamepad/GamepadStateReceiver.h +++ b/dom/gamepad/GamepadStateReceiver.h @@ -38,6 +38,9 @@ class IProtocol; namespace mozilla::dom { +class GamepadChangeEvent; +class GamepadTestHelper; + // IPDL structure that knows how to initialize a receiver class GamepadStateBroadcastReceiverInfo; @@ -54,6 +57,12 @@ class GamepadStateBroadcastReceiverInfo; // Maybe maybeReceiver = // GamepadStateReceiver::Create(aReceiverInfo); // MOZ_ASSERT(maybeReceiver); +// +// // This thread will probably be running async, so be sure to capture by +// // value and not reference on most things +// maybeReceiver->StartMonitoringThread([=](const GamepadChangeEvent& e) { +// this->handleEvent(e); +// }); // } // class GamepadStateReceiver { @@ -66,6 +75,23 @@ class GamepadStateReceiver { static Maybe Create( const GamepadStateBroadcastReceiverInfo& aReceiverInfo); + // Start a thread that monitors the shared memory for changes + // + // The thread will call `aFn` with an event generated for each gamepad + // value that has changed since the last time it checked. Note that this + // means that multiple changes to the same value will appear as a single + // change if they happen quickly enough. This generally doesn't matter for + // gamepads. + bool StartMonitoringThread( + const std::function& aFn); + + // Stop the monitoring thread + // + // If the broadcaster continues sending messages after this is called, they + // will be missed. It is okay to restart the monitoring thread and even + // to pass in a different function. + void StopMonitoringThread(); + // Allow move GamepadStateReceiver(GamepadStateReceiver&& aOther); GamepadStateReceiver& operator=(GamepadStateReceiver&& aOther); @@ -83,6 +109,11 @@ class GamepadStateReceiver { explicit GamepadStateReceiver(UniquePtr aImpl); UniquePtr mImpl; + + friend class GamepadTestHelper; + bool StartMonitoringThreadForTesting( + const std::function& aMonitorFn, + const std::function& aTestCommandFn); }; } // namespace mozilla::dom diff --git a/dom/gamepad/fallback/GamepadStateBroadcaster.cpp b/dom/gamepad/fallback/GamepadStateBroadcaster.cpp index f310e7fbd722..118f6adf1e5d 100644 --- a/dom/gamepad/fallback/GamepadStateBroadcaster.cpp +++ b/dom/gamepad/fallback/GamepadStateBroadcaster.cpp @@ -32,6 +32,48 @@ void GamepadStateBroadcaster::RemoveReceiver( MOZ_CRASH("Should never be called"); } +void GamepadStateBroadcaster::AddGamepad( + GamepadHandle aHandle, const char* aID, GamepadMappingType aMapping, + GamepadHand aHand, uint32_t aNumButtons, uint32_t aNumAxes, + uint32_t aNumHaptics, uint32_t aNumLights, uint32_t aNumTouches) { + MOZ_CRASH("Should never be called"); +} + +void GamepadStateBroadcaster::RemoveGamepad(GamepadHandle aHandle) { + MOZ_CRASH("Should never be called"); +} + +void GamepadStateBroadcaster::NewAxisMoveEvent(GamepadHandle aHandle, + uint32_t aAxis, double aValue) { + MOZ_CRASH("Should never be called"); +} + +void GamepadStateBroadcaster::NewButtonEvent(GamepadHandle aHandle, + uint32_t aButton, bool aPressed, + bool aTouched, double aValue) { + MOZ_CRASH("Should never be called"); +} + +void GamepadStateBroadcaster::NewLightIndicatorTypeEvent( + GamepadHandle aHandle, uint32_t aLight, GamepadLightIndicatorType aType) { + MOZ_CRASH("Should never be called"); +} + +void GamepadStateBroadcaster::NewPoseEvent(GamepadHandle aHandle, + const GamepadPoseState& aState) { + MOZ_CRASH("Should never be called"); +} + +void GamepadStateBroadcaster::NewMultiTouchEvent( + GamepadHandle aHandle, uint32_t aTouchArrayIndex, + const GamepadTouchState& aState) { + MOZ_CRASH("Should never be called"); +} + +void GamepadStateBroadcaster::SendTestCommand(uint32_t aCommandId) { + MOZ_CRASH("Should never be called"); +} + GamepadStateBroadcaster::GamepadStateBroadcaster( GamepadStateBroadcaster&& aOther) = default; diff --git a/dom/gamepad/fallback/GamepadStateReceiver.cpp b/dom/gamepad/fallback/GamepadStateReceiver.cpp index 1a76b4b06ff8..eaf8c5e384ab 100644 --- a/dom/gamepad/fallback/GamepadStateReceiver.cpp +++ b/dom/gamepad/fallback/GamepadStateReceiver.cpp @@ -32,4 +32,18 @@ GamepadStateReceiver::GamepadStateReceiver() = default; GamepadStateReceiver::GamepadStateReceiver(UniquePtr aImpl) : mImpl(std::move(aImpl)) {} +bool GamepadStateReceiver::StartMonitoringThread( + const std::function& aFn) { + MOZ_CRASH("Should never be called"); +} +bool GamepadStateReceiver::StartMonitoringThreadForTesting( + const std::function& aMonitorFn, + const std::function& aTestCommandFn) { + MOZ_CRASH("Should never be called"); +} + +void GamepadStateReceiver::StopMonitoringThread() { + MOZ_CRASH("Should never be called"); +} + } // namespace mozilla::dom diff --git a/dom/gamepad/tests/gtest/TestGamepadStateBroadcastWindows.cpp b/dom/gamepad/tests/gtest/TestGamepadStateBroadcastWindows.cpp new file mode 100644 index 000000000000..8707e6cbe2c8 --- /dev/null +++ b/dom/gamepad/tests/gtest/TestGamepadStateBroadcastWindows.cpp @@ -0,0 +1,451 @@ +/* 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 "mozilla/dom/GamepadStateBroadcaster.h" +#include "mozilla/dom/GamepadStateReceiver.h" +#include "mozilla/dom/GamepadStateBroadcastReceiverInfo.h" +#include "mozilla/dom/GamepadEventTypes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +#define MYASSERT(x) \ + do { \ + if (!(x)) { \ + ADD_FAILURE() << "MYASSERT FAILED: `" #x "`\n"; \ + abort(); \ + } \ + } while (false) + +namespace mozilla::dom { + +// This class has friend access to GamepadHandle and can assist in creating +// them +class GamepadTestHelper { + public: + static void SendTestCommand(GamepadStateBroadcaster* p, uint32_t commandId) { + p->SendTestCommand(commandId); + } + static bool StartMonitoringThreadForTesting( + GamepadStateReceiver* p, + std::function aMonitorFn, + std::function aTestCommandFn) { + return p->StartMonitoringThreadForTesting(aMonitorFn, aTestCommandFn); + } + static GamepadHandle GetTestHandle(uint32_t value) { + return GamepadHandle(value, GamepadHandleKind::GamepadPlatformManager); + } +}; + +namespace { + +template +class EventQueue { + public: + void Enqueue(T t) { + std::unique_lock lock(mMutex); + mList.emplace_back(std::move(t)); + mCond.notify_one(); + } + T Dequeue() { + std::unique_lock lock(mMutex); + while (mList.empty()) { + mCond.wait(lock); + } + T result = std::move(mList.front()); + mList.pop_front(); + return result; + } + + EventQueue() = default; + ~EventQueue() = default; + + // Disallow copy and move + EventQueue(const EventQueue&) = delete; + EventQueue& operator=(const EventQueue&) = delete; + + EventQueue(EventQueue&&) = delete; + EventQueue& operator=(EventQueue&&) = delete; + + private: + std::mutex mMutex{}; + std::condition_variable mCond{}; + std::list mList{}; +}; + +class Signal { + public: + void SetSignal() { + std::unique_lock lock(mMutex); + mSignalled = true; + mCond.notify_one(); + } + void WaitForSignal() { + std::unique_lock lock(mMutex); + while (!mSignalled) { + mCond.wait(lock); + } + mSignalled = false; + } + + Signal() = default; + ~Signal() = default; + + // Disallow copy and move + Signal(const Signal&) = delete; + Signal& operator=(const Signal&) = delete; + + Signal(Signal&&) = delete; + Signal& operator=(Signal&&) = delete; + + private: + std::mutex mMutex{}; + std::condition_variable mCond{}; + bool mSignalled{}; +}; + +constexpr uint32_t kMaxButtons = 8; +constexpr uint32_t kMaxAxes = 4; + +constexpr uint32_t kCommandSelfCheck = 1; +constexpr uint32_t kCommandQuit = 2; + +struct GamepadButton { + double value{}; + bool pressed{}; + bool touched{}; +}; + +struct GamepadInfo { + nsString id{}; + GamepadMappingType mapping{}; + GamepadHand hand{}; + uint32_t numButtons{}; + uint32_t numAxes{}; + std::array buttons{}; + std::array axes{}; +}; + +static void ExpectEqual(const GamepadButton& a, const GamepadButton& b) { + EXPECT_EQ(a.value, b.value); + EXPECT_EQ(a.pressed, b.pressed); + EXPECT_EQ(a.touched, b.touched); +} + +static void ExpectEqual(const GamepadInfo& a, const GamepadInfo& b) { + EXPECT_EQ(a.id, b.id); + EXPECT_EQ(a.mapping, b.mapping); + EXPECT_EQ(a.hand, b.hand); + EXPECT_EQ(a.numButtons, b.numButtons); + EXPECT_EQ(a.numAxes, b.numAxes); + + for (size_t i = 0; i < a.numButtons; ++i) { + ExpectEqual(a.buttons[i], b.buttons[i]); + } + + for (size_t i = 0; i < a.numAxes; ++i) { + EXPECT_EQ(a.axes[i], b.axes[i]); + } +} + +static void ExpectEqual(const std::map& a, + const std::map& b) { + for (auto& pair : a) { + EXPECT_TRUE(b.count(pair.first) == 1); + ExpectEqual(a.at(pair.first), b.at(pair.first)); + } +} + +class TestThread { + public: + TestThread() = default; + ~TestThread() = default; + + void Run(GamepadStateBroadcastReceiverInfo remoteInfo) { + mThread = std::thread(&TestThread::ThreadFunc, this, remoteInfo); + } + + void Join() { mThread.join(); } + + void SetExpectedState(std::map expectedState) { + std::unique_lock lock(mExpectedStateMutex); + mExpectedState = std::move(expectedState); + } + + void WaitCommandProcessed() { mCommandProcessed.WaitForSignal(); } + + // Disallow move and copy + TestThread(const TestThread&) = delete; + TestThread& operator=(const TestThread&) = delete; + + TestThread(TestThread&&) = delete; + TestThread& operator=(TestThread&&) = delete; + + private: + void HandleGamepadEvent(std::map& gamepads, + const GamepadChangeEvent& event) { + GamepadHandle handle = event.handle(); + + switch (event.body().type()) { + case GamepadChangeEventBody::TGamepadAdded: { + MYASSERT(gamepads.count(handle) == 0); + + const GamepadAdded& x = event.body().get_GamepadAdded(); + + GamepadInfo info{}; + info.id = x.id(); + info.mapping = x.mapping(); + info.hand = x.hand(); + info.numButtons = x.num_buttons(); + info.numAxes = x.num_axes(); + + gamepads.insert(std::make_pair(handle, info)); + + break; + } + case GamepadChangeEventBody::TGamepadRemoved: { + MYASSERT(gamepads.count(handle) == 1); + gamepads.erase(handle); + break; + } + case GamepadChangeEventBody::TGamepadAxisInformation: { + const GamepadAxisInformation& x = + event.body().get_GamepadAxisInformation(); + + gamepads[handle].axes[x.axis()] = x.value(); + + break; + } + case GamepadChangeEventBody::TGamepadButtonInformation: { + const GamepadButtonInformation& x = + event.body().get_GamepadButtonInformation(); + + gamepads[handle].buttons[x.button()].value = x.value(); + gamepads[handle].buttons[x.button()].pressed = x.pressed(); + gamepads[handle].buttons[x.button()].touched = x.touched(); + + break; + } + case GamepadChangeEventBody::T__None: + case GamepadChangeEventBody::TGamepadHandInformation: + case GamepadChangeEventBody::TGamepadLightIndicatorTypeInformation: + case GamepadChangeEventBody::TGamepadPoseInformation: + case GamepadChangeEventBody::TGamepadTouchInformation: { + MYASSERT(false); + break; + } + } + } + + bool ProcessCommand(std::map& gamepads, + uint32_t commandId) { + if (commandId == kCommandQuit) { + return true; + } + + MYASSERT(commandId == kCommandSelfCheck); + + std::unique_lock lock(mExpectedStateMutex); + ExpectEqual(mExpectedState, gamepads); + + return false; + } + + void ThreadFunc(GamepadStateBroadcastReceiverInfo remoteInfo) { + std::random_device rd; + std::mt19937 mersenne_twister(rd()); + std::uniform_int_distribution randomDelayGenerator(1, 1000); + + Maybe maybeReceiver = + GamepadStateReceiver::Create(remoteInfo); + MYASSERT(maybeReceiver); + + EventQueue> eventQueue{}; + + bool result = GamepadTestHelper::StartMonitoringThreadForTesting( + &*maybeReceiver, + [&](const GamepadChangeEvent& e) { + // Simulate that sometimes there will be thread noise and make sure + // that we still end up with the correct state anyway + uint32_t randomDelay = randomDelayGenerator(mersenne_twister); + std::this_thread::sleep_for(std::chrono::microseconds(randomDelay)); + eventQueue.Enqueue(e); + }, + [&](uint32_t x) { eventQueue.Enqueue(x); }); + MYASSERT(result); + + std::map gamepads{}; + + bool quit = false; + while (!quit) { + auto event = eventQueue.Dequeue(); + + if (std::holds_alternative(event)) { + HandleGamepadEvent(gamepads, std::get(event)); + } else { + quit = ProcessCommand(gamepads, std::get(event)); + mCommandProcessed.SetSignal(); + } + } + + maybeReceiver->StopMonitoringThread(); + } + + std::thread mThread{}; + + std::mutex mExpectedStateMutex{}; + std::map mExpectedState{}; + + Signal mCommandProcessed{}; +}; + +} // anonymous namespace + +TEST(GamepadStateBroadcastTest, Multithreaded) +{ + constexpr uint8_t kNumThreads = 4; + + const GamepadHandle testHandle0 = GamepadTestHelper::GetTestHandle(1234); + const GamepadHandle testHandle1 = GamepadTestHelper::GetTestHandle(2345); + const GamepadHandle testHandle2 = GamepadTestHelper::GetTestHandle(3456); + + std::map expected{}; + + Maybe maybeBroadcaster = + GamepadStateBroadcaster::Create(); + MYASSERT(maybeBroadcaster); + + std::array remoteThreads{}; + + for (uint8_t i = 0; i < kNumThreads; ++i) { + GamepadStateBroadcastReceiverInfo remoteInfo{}; + bool result = maybeBroadcaster->AddReceiverAndGenerateRemoteInfo( + nullptr, &remoteInfo); + MYASSERT(result); + + remoteThreads[i].Run(remoteInfo); + } + + // This function can be used at any time to tell the threads to verify + // that their state matches `expected` after they've finished processing + // all previous updates + auto checkExpectedState = [&] { + for (uint8_t i = 0; i < kNumThreads; ++i) { + remoteThreads[i].SetExpectedState(expected); + } + + GamepadTestHelper::SendTestCommand(&*maybeBroadcaster, kCommandSelfCheck); + + for (uint8_t i = 0; i < kNumThreads; ++i) { + remoteThreads[i].WaitCommandProcessed(); + } + }; + + // Simple one - Just add a gamepad and verify it shows up + + maybeBroadcaster->AddGamepad(testHandle0, "TestId0", + GamepadMappingType::Standard, GamepadHand::Left, + kMaxButtons, kMaxAxes, 0, 0, 0); + + GamepadInfo* info0 = &expected[testHandle0]; + info0->id = nsString(u"TestId0"); + info0->mapping = GamepadMappingType::Standard; + info0->hand = GamepadHand::Left; + info0->numButtons = kMaxButtons; + info0->numAxes = kMaxAxes; + + checkExpectedState(); + + // Try adding 2 more and see that they also show up + maybeBroadcaster->AddGamepad(testHandle1, "TestId1", + GamepadMappingType::Standard, GamepadHand::Left, + 2, 2, 0, 0, 0); + maybeBroadcaster->AddGamepad(testHandle2, "TestId2", + GamepadMappingType::Standard, GamepadHand::Left, + 3, 2, 0, 0, 0); + + GamepadInfo* info1 = &expected[testHandle1]; + info1->id = nsString(u"TestId1"); + info1->mapping = GamepadMappingType::Standard; + info1->hand = GamepadHand::Left; + info1->numButtons = 2; + info1->numAxes = 2; + + GamepadInfo* info2 = &expected[testHandle2]; + info2->id = nsString(u"TestId2"); + info2->mapping = GamepadMappingType::Standard; + info2->hand = GamepadHand::Left; + info2->numButtons = 3; + info2->numAxes = 2; + + checkExpectedState(); + + // Press some buttons, move some axes, unplug one of the gamepads + + maybeBroadcaster->NewButtonEvent(testHandle1, 0, true, true, 0.5); + maybeBroadcaster->NewButtonEvent(testHandle2, 0, true, false, 0.25); + maybeBroadcaster->NewButtonEvent(testHandle2, 1, true, true, 0.35); + maybeBroadcaster->NewAxisMoveEvent(testHandle1, 1, 0.5); + maybeBroadcaster->NewAxisMoveEvent(testHandle1, 1, 0.75); + maybeBroadcaster->NewButtonEvent(testHandle0, 0, true, true, 0.1); + maybeBroadcaster->NewAxisMoveEvent(testHandle1, 1, 0.8); + maybeBroadcaster->NewAxisMoveEvent(testHandle2, 0, 1.0); + maybeBroadcaster->NewButtonEvent(testHandle2, 0, false, false, 0.0); + maybeBroadcaster->RemoveGamepad(testHandle0); + + info0 = nullptr; + expected.erase(testHandle0); + + info1->buttons[0].pressed = true; + info1->buttons[0].touched = true; + info1->buttons[0].value = 0.5; + info1->axes[1] = 0.8; + + info2->buttons[0].pressed = false; + info2->buttons[0].touched = false; + info2->buttons[0].value = 0.0; + info2->buttons[1].pressed = true; + info2->buttons[1].touched = true; + info2->buttons[1].value = 0.35; + info2->axes[0] = 1.0; + + checkExpectedState(); + + // Re-add gamepad0 and ensure it starts off in a null state + maybeBroadcaster->AddGamepad(testHandle0, "TestId0", + GamepadMappingType::Standard, GamepadHand::Left, + kMaxButtons, kMaxAxes, 0, 0, 0); + + info0 = &expected[testHandle0]; + info0->id = nsString(u"TestId0"); + info0->mapping = GamepadMappingType::Standard; + info0->hand = GamepadHand::Left; + info0->numButtons = kMaxButtons; + info0->numAxes = kMaxAxes; + + checkExpectedState(); + + // Tell the receiver to shut everything down and exit its thread + GamepadTestHelper::SendTestCommand(&*maybeBroadcaster, kCommandQuit); + + for (uint8_t i = 0; i < kNumThreads; ++i) { + remoteThreads[i].WaitCommandProcessed(); + } + + for (uint8_t i = 0; i < kNumThreads; ++i) { + remoteThreads[i].Join(); + } +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/tests/gtest/moz.build b/dom/gamepad/tests/gtest/moz.build index b4ca995a8555..9a69039d4173 100644 --- a/dom/gamepad/tests/gtest/moz.build +++ b/dom/gamepad/tests/gtest/moz.build @@ -4,6 +4,7 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": SOURCES = [ + "TestGamepadStateBroadcastWindows.cpp", "TestSynchronizedSharedMemoryWindows.cpp", ] else: diff --git a/dom/gamepad/windows/GamepadStateBroadcaster.cpp b/dom/gamepad/windows/GamepadStateBroadcaster.cpp index e6f8603ddd4c..e13e3da7c007 100644 --- a/dom/gamepad/windows/GamepadStateBroadcaster.cpp +++ b/dom/gamepad/windows/GamepadStateBroadcaster.cpp @@ -98,6 +98,109 @@ class GamepadStateBroadcaster::Impl { mBroadcastEventHandles.popBack(); } + void AddGamepad(GamepadHandle aHandle, const char* aID, + GamepadMappingType aMapping, GamepadHand aHand, + uint32_t aNumButtons, uint32_t aNumAxes, uint32_t aNumHaptics, + uint32_t aNumLights, uint32_t aNumTouches) { + size_t lenId = strlen(aID); + MOZ_RELEASE_ASSERT(lenId <= kMaxGamepadIdLength); + MOZ_RELEASE_ASSERT(aNumButtons <= kMaxButtonsPerGamepad); + MOZ_RELEASE_ASSERT(aNumAxes <= kMaxAxesPerGamepad); + MOZ_RELEASE_ASSERT(aNumLights <= kMaxLightsPerGamepad); + MOZ_RELEASE_ASSERT(aNumTouches <= kMaxNumMultiTouches); + + // We pass an empty handle as the first argument, which tells + // ModifyGamepadSlot to run the lambda on the first empty slot that's + // found. + // + // If there are no empty slots, the following lambda won't be run and + // the add will fail silently (which is preferable to crashing) + ModifyGamepadSlot(GamepadHandle{}, [&](GamepadSlot& slot) { + slot.handle = aHandle; + memcpy(&slot.props.id[0], aID, lenId); + slot.props.id[lenId] = 0; + slot.props.mapping = aMapping; + slot.props.hand = aHand; + slot.props.numButtons = aNumButtons; + slot.props.numAxes = aNumAxes; + slot.props.numHaptics = aNumHaptics; + slot.props.numLights = aNumLights; + slot.props.numTouches = aNumTouches; + }); + } + + void RemoveGamepad(GamepadHandle aHandle) { + // If the handle was never added, this function will do nothing + ModifyGamepadSlot(aHandle, + [&](GamepadSlot& slot) { slot = GamepadSlot{}; }); + } + + void NewAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, double aValue) { + MOZ_RELEASE_ASSERT(aAxis < kMaxAxesPerGamepad); + + // If the handle was never added, this function will do nothing + ModifyGamepadSlot(aHandle, [&](GamepadSlot& slot) { + MOZ_ASSERT(aAxis < slot.props.numAxes); + slot.values.axes[aAxis] = aValue; + }); + } + + void NewButtonEvent(GamepadHandle aHandle, uint32_t aButton, bool aPressed, + bool aTouched, double aValue) { + MOZ_RELEASE_ASSERT(aButton < kMaxButtonsPerGamepad); + + // If the handle was never added, this function will do nothing + ModifyGamepadSlot(aHandle, [&](GamepadSlot& slot) { + MOZ_ASSERT(aButton < slot.props.numButtons); + slot.values.buttonValues[aButton] = aValue; + slot.values.buttonPressedBits[aButton] = aPressed; + slot.values.buttonTouchedBits[aButton] = aTouched; + }); + } + + void NewLightIndicatorTypeEvent(GamepadHandle aHandle, uint32_t aLight, + GamepadLightIndicatorType aType) { + MOZ_RELEASE_ASSERT(aLight < kMaxLightsPerGamepad); + + // If the handle was never added, this function will do nothing + ModifyGamepadSlot(aHandle, [&](GamepadSlot& slot) { + MOZ_ASSERT(aLight < slot.props.numLights); + slot.values.lights[aLight] = aType; + }); + } + + void NewPoseEvent(GamepadHandle aHandle, const GamepadPoseState& aState) { + // If the handle was never added, this function will do nothing + ModifyGamepadSlot(aHandle, + [&](GamepadSlot& slot) { slot.values.pose = aState; }); + } + + void NewMultiTouchEvent(GamepadHandle aHandle, uint32_t aTouchArrayIndex, + const GamepadTouchState& aState) { + MOZ_RELEASE_ASSERT(aTouchArrayIndex < kMaxNumMultiTouches); + + // If the handle was never added, this function will do nothing + ModifyGamepadSlot(aHandle, [&](GamepadSlot& slot) { + MOZ_ASSERT(aTouchArrayIndex < slot.props.numTouches); + slot.values.touches[aTouchArrayIndex] = aState; + }); + } + + void SendTestCommand(uint32_t aCommandId) { + mSharedState.RunWithLock([&](GamepadSystemState* p) { + // SECURITY BOUNDARY -- `GamepadSystemState* p` is a local copy of the + // shared memory (it is not a live copy), and we only write to it here. + // It is also only used for testing + p->testCommandId = aCommandId; + p->testCommandTrigger = !p->testCommandTrigger; + + p->changeId = mChangeId; + ++mChangeId; + }); + + TriggerEvents(); + } + // Disallow copy/move Impl(const Impl&) = delete; Impl& operator=(const Impl&) = delete; @@ -112,8 +215,44 @@ class GamepadStateBroadcaster::Impl { }; explicit Impl(SharedState aSharedState) - : mSharedState(std::move(aSharedState)) {} + : mChangeId(1), mSharedState(std::move(aSharedState)) {} + void ModifyGamepadSlot(GamepadHandle aHandle, + const std::function& aFn) { + mSharedState.RunWithLock([&](GamepadSystemState* p) { + // SECURITY BOUNDARY -- `GamepadSystemState* p` is a local copy of the + // shared memory (it is not a live copy), so after this validation we can + // trust the values inside of it + ValidateGamepadSystemState(p); + + GamepadSlot* foundSlot = nullptr; + for (auto& slot : p->gamepadSlots) { + if (slot.handle == aHandle) { + foundSlot = &slot; + break; + } + } + + if (!foundSlot) { + return; + } + + aFn(*foundSlot); + + p->changeId = mChangeId; + ++mChangeId; + }); + + TriggerEvents(); + } + + void TriggerEvents() { + for (auto& x : mBroadcastEventHandles) { + MOZ_ALWAYS_TRUE(::SetEvent(x.eventHandle.Get())); + } + } + + uint64_t mChangeId; SharedState mSharedState; Vector mBroadcastEventHandles; }; @@ -143,6 +282,49 @@ void GamepadStateBroadcaster::RemoveReceiver( mImpl->RemoveReceiver(aActor); } +void GamepadStateBroadcaster::AddGamepad( + GamepadHandle aHandle, const char* aID, GamepadMappingType aMapping, + GamepadHand aHand, uint32_t aNumButtons, uint32_t aNumAxes, + uint32_t aNumHaptics, uint32_t aNumLights, uint32_t aNumTouches) { + mImpl->AddGamepad(aHandle, aID, aMapping, aHand, aNumButtons, aNumAxes, + aNumHaptics, aNumLights, aNumTouches); +} + +void GamepadStateBroadcaster::RemoveGamepad(GamepadHandle aHandle) { + mImpl->RemoveGamepad(aHandle); +} + +void GamepadStateBroadcaster::NewAxisMoveEvent(GamepadHandle aHandle, + uint32_t aAxis, double aValue) { + mImpl->NewAxisMoveEvent(aHandle, aAxis, aValue); +} + +void GamepadStateBroadcaster::NewButtonEvent(GamepadHandle aHandle, + uint32_t aButton, bool aPressed, + bool aTouched, double aValue) { + mImpl->NewButtonEvent(aHandle, aButton, aPressed, aTouched, aValue); +} + +void GamepadStateBroadcaster::NewLightIndicatorTypeEvent( + GamepadHandle aHandle, uint32_t aLight, GamepadLightIndicatorType aType) { + mImpl->NewLightIndicatorTypeEvent(aHandle, aLight, aType); +} + +void GamepadStateBroadcaster::NewPoseEvent(GamepadHandle aHandle, + const GamepadPoseState& aState) { + mImpl->NewPoseEvent(aHandle, aState); +} + +void GamepadStateBroadcaster::NewMultiTouchEvent( + GamepadHandle aHandle, uint32_t aTouchArrayIndex, + const GamepadTouchState& aState) { + mImpl->NewMultiTouchEvent(aHandle, aTouchArrayIndex, aState); +} + +void GamepadStateBroadcaster::SendTestCommand(uint32_t aCommandId) { + mImpl->SendTestCommand(aCommandId); +} + GamepadStateBroadcaster::GamepadStateBroadcaster( GamepadStateBroadcaster&& aOther) = default; diff --git a/dom/gamepad/windows/GamepadStateLayout.h b/dom/gamepad/windows/GamepadStateLayout.h index 84e9a3c464a7..76b057f88507 100644 --- a/dom/gamepad/windows/GamepadStateLayout.h +++ b/dom/gamepad/windows/GamepadStateLayout.h @@ -7,6 +7,7 @@ #ifndef GAMEPAD_DOM_GAMEPADSTATELAYOUT_H_ #define GAMEPAD_DOM_GAMEPADSTATELAYOUT_H_ +#include "mozilla/dom/Gamepad.h" #include "mozilla/dom/GamepadBinding.h" #include "mozilla/dom/GamepadHandle.h" #include "mozilla/dom/GamepadLightIndicatorBinding.h" @@ -18,11 +19,82 @@ namespace mozilla::dom { -// Placeholder for the actual shared state in a later changelist -struct GamepadSystemState { - uint32_t placeholder; +// Define the shared memory and reasonable maximums for gamepads +constexpr size_t kMaxGamepadIdLength = 32; +constexpr size_t kMaxGamepads = 8; + +// These 2 values from from the w3 spec: +// https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html#remapping +constexpr size_t kMaxButtonsPerGamepad = kStandardGamepadButtons; +constexpr size_t kMaxAxesPerGamepad = kStandardGamepadAxes; + +constexpr size_t kMaxHapticsPerGamepad = 0; // We don't support haptics yet +constexpr size_t kMaxLightsPerGamepad = 2; +constexpr size_t kMaxNumMultiTouches = 2; + +// CAUTION: You must update ValidateGamepadSystemState() if you change +// any of these structures + +struct GamepadProperties { + Array id{}; + GamepadMappingType mapping{}; + GamepadHand hand{}; + uint32_t numAxes{}; + uint32_t numButtons{}; + uint32_t numHaptics{}; + uint32_t numLights{}; + uint32_t numTouches{}; }; +// It is important that the default values for these members matches the +// default values for the JS objects. When a new gamepad is added, its state +// will be compared to a default GamepadValues object to determine what events +// are needed. +struct GamepadValues { + Array axes{}; + Array buttonValues{}; + Array lights{}; + Array touches{}; + GamepadPoseState pose{}; + std::bitset buttonPressedBits; + std::bitset buttonTouchedBits; +}; + +struct GamepadSlot { + GamepadHandle handle{}; + GamepadProperties props{}; + GamepadValues values{}; +}; + +struct GamepadSystemState { + Array gamepadSlots{}; + uint64_t changeId{}; + uint32_t testCommandId{}; + bool testCommandTrigger{}; +}; + +static void ValidateGamepadSystemState(GamepadSystemState* p) { + for (auto& slot : p->gamepadSlots) { + // Check that id is a null-terminated string + bool hasNull = false; + for (char c : slot.props.id) { + if (c == 0) { + hasNull = true; + break; + } + } + MOZ_RELEASE_ASSERT(hasNull); + + MOZ_RELEASE_ASSERT(slot.props.mapping < GamepadMappingType::EndGuard_); + MOZ_RELEASE_ASSERT(slot.props.hand < GamepadHand::EndGuard_); + MOZ_RELEASE_ASSERT(slot.props.numAxes <= kMaxAxesPerGamepad); + MOZ_RELEASE_ASSERT(slot.props.numButtons <= kMaxButtonsPerGamepad); + MOZ_RELEASE_ASSERT(slot.props.numHaptics <= kMaxHapticsPerGamepad); + MOZ_RELEASE_ASSERT(slot.props.numLights <= kMaxLightsPerGamepad); + MOZ_RELEASE_ASSERT(slot.props.numTouches <= kMaxNumMultiTouches); + } +} + } // namespace mozilla::dom #endif // GAMEPAD_DOM_GAMEPADSTATELAYOUT_H_ diff --git a/dom/gamepad/windows/GamepadStateReceiver.cpp b/dom/gamepad/windows/GamepadStateReceiver.cpp index 47f186866134..05a0b9133178 100644 --- a/dom/gamepad/windows/GamepadStateReceiver.cpp +++ b/dom/gamepad/windows/GamepadStateReceiver.cpp @@ -12,6 +12,8 @@ #include "mozilla/dom/GamepadEventTypes.h" #include "mozilla/dom/SynchronizedSharedMemory.h" #include "mozilla/ipc/ProtocolUtils.h" +#include "prthread.h" +#include #include #include @@ -37,6 +39,42 @@ class GamepadStateReceiver::Impl { new Impl(std::move(*sharedState), std::move(eventHandle))); } + bool StartMonitoringThread( + const std::function& aMonitorFn, + const std::function& aTestCommandFn) { + MOZ_ASSERT(!mMonitorThread); + + mMonitorFn = aMonitorFn; + mTestCommandFn = aTestCommandFn; + + // Every time the thread wakes up from an event, it checks this before + // it does anything else. The thread exits when this is `true` + mStopMonitoring.store(false, std::memory_order_release); + + mMonitorThread = PR_CreateThread( + PR_USER_THREAD, + [](void* p) { static_cast(p)->MonitoringThread(); }, this, + PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0); + + return !!mMonitorThread; + } + + void StopMonitoringThread() { + MOZ_ASSERT(mMonitorThread); + + /// Every time the thread wakes up from an event, it checks this before + // it does anything else. The thread exits when this is `true` + mStopMonitoring.store(true, std::memory_order_release); + + // Wake the thread up with the event, causing it to exit + MOZ_ALWAYS_TRUE(::SetEvent(mEventHandle.Get())); + + MOZ_ALWAYS_TRUE(PR_JoinThread(mMonitorThread) == PR_SUCCESS); + mMonitorThread = nullptr; + mMonitorFn = nullptr; + mTestCommandFn = nullptr; + } + // Disallow copy/move Impl(const Impl&) = delete; Impl& operator=(const Impl&) = delete; @@ -44,14 +82,180 @@ class GamepadStateReceiver::Impl { Impl(Impl&&) = delete; Impl& operator=(Impl&&) = delete; + ~Impl() { + if (mMonitorThread) { + MOZ_ASSERT(false, + "GamepadStateReceiver::~Impl() was called without " + "calling StopMonitoringThread()."); + StopMonitoringThread(); + } + } + private: explicit Impl(SharedState aSharedState, UniqueHandle aEventHandle) : mSharedState(std::move(aSharedState)), - mEventHandle(std::move(aEventHandle)) {} + mEventHandle(std::move(aEventHandle)), + mMonitorThread(nullptr) {} + + // This compares two GamepadValues structures for a single gamepad and + // generates events for any detected differences. + // Generally we are either comparing a known gamepad to its last known state, + // or we are comparing a new gamepad against the default state + void DiffGamepadValues( + GamepadHandle handle, const GamepadProperties& props, + const GamepadValues& curValues, const GamepadValues& newValues, + const std::function& aFn) { + // Diff axes + for (uint32_t i = 0; i < props.numAxes; ++i) { + if (curValues.axes[i] != newValues.axes[i]) { + GamepadAxisInformation axisInfo(i, newValues.axes[i]); + GamepadChangeEvent e(handle, axisInfo); + aFn(e); + } + } + + // Diff buttons + for (uint32_t i = 0; i < props.numButtons; ++i) { + if ((curValues.buttonValues[i] != newValues.buttonValues[i]) || + (curValues.buttonPressedBits[i] != newValues.buttonPressedBits[i]) || + (curValues.buttonTouchedBits[i] != newValues.buttonTouchedBits[i])) { + GamepadButtonInformation buttonInfo(i, newValues.buttonValues[i], + newValues.buttonPressedBits[i], + newValues.buttonTouchedBits[i]); + GamepadChangeEvent e(handle, buttonInfo); + aFn(e); + } + } + + // Diff indictator lights + for (uint32_t i = 0; i < props.numLights; ++i) { + if (curValues.lights[i] != newValues.lights[i]) { + GamepadLightIndicatorTypeInformation lightInfo(i, newValues.lights[i]); + GamepadChangeEvent e(handle, lightInfo); + aFn(e); + } + } + + // Diff multi-touch states + for (uint32_t i = 0; i < props.numTouches; ++i) { + if (curValues.touches[i] != newValues.touches[i]) { + GamepadTouchInformation touchInfo(i, newValues.touches[i]); + GamepadChangeEvent e(handle, touchInfo); + aFn(e); + } + } + + // Diff poses + if (curValues.pose != newValues.pose) { + GamepadPoseInformation poseInfo(newValues.pose); + GamepadChangeEvent e(handle, poseInfo); + aFn(e); + } + } + + // Compare two gamepad slots and generate events based on the differences + // + // This is generally used to compare the old and new state of a single slot. + // If the same gamepad is still in the slot from last check, we just need + // to diff the gamepad's values. + // + // If a different gamepad is now in the slot, we need to unregister the old + // one, register the new one, and generate events for every value that is + // different than the default "new gamepad" state. + // + void DiffGamepadSlots( + uint32_t changeId, const GamepadSlot& curState, + const GamepadSlot& newState, + const std::function& aFn) { + if (curState.handle == newState.handle) { + if (newState.handle != GamepadHandle{}) { + // Same gamepad is plugged in as last time - Just diff values + DiffGamepadValues(newState.handle, newState.props, curState.values, + newState.values, aFn); + } + return; + } + + // The gamepad in this slot has changed. + + // If there was previously a gamepad in this slot, remove it + if (curState.handle != GamepadHandle{}) { + GamepadChangeEvent e(curState.handle, GamepadRemoved{}); + aFn(e); + } + + // If there is a gamepad in it now, register it + if (newState.handle != GamepadHandle{}) { + GamepadAdded gamepadInfo( + NS_ConvertUTF8toUTF16(nsDependentCString(&newState.props.id[0])), + newState.props.mapping, newState.props.hand, 0, + newState.props.numButtons, newState.props.numAxes, + newState.props.numHaptics, newState.props.numLights, + newState.props.numTouches); + + GamepadChangeEvent e(newState.handle, gamepadInfo); + aFn(e); + + // Since the gamepad is new, we diff its values against a + // default-constructed GamepadValues structure + DiffGamepadValues(newState.handle, newState.props, GamepadValues{}, + newState.values, aFn); + } + } + + void MonitoringThread() { + while (true) { + // If the other side crashes or something goes very wrong, we're probably + // about to be destroyed anyhow. Just quit the thread and await our + // inevitable doom + if (::WaitForSingleObject(mEventHandle.Get(), INFINITE) != + WAIT_OBJECT_0) { + break; + } + + // First check if the signal is from StopMonitoring telling us we're done + if (mStopMonitoring.load(std::memory_order_acquire)) { + break; + } + + // Read the shared memory + GamepadSystemState newSystemState; + mSharedState.RunWithLock( + [&](GamepadSystemState* p) { newSystemState = *p; }); + + // SECURITY BOUNDARY -- After this validation, we can trust newSystemState + ValidateGamepadSystemState(&newSystemState); + + if (mGamepadSystemState.changeId != newSystemState.changeId) { + // Diff the state of each gamepad slot from the previous read + for (size_t i = 0; i < kMaxGamepads; ++i) { + DiffGamepadSlots(newSystemState.changeId, + mGamepadSystemState.gamepadSlots[i], + newSystemState.gamepadSlots[i], mMonitorFn); + } + + if (mGamepadSystemState.testCommandTrigger != + newSystemState.testCommandTrigger) { + if (mTestCommandFn) { + mTestCommandFn(newSystemState.testCommandId); + } + } + + // Save the current read for the next diff + mGamepadSystemState = newSystemState; + } + } + } SharedState mSharedState; UniqueHandle mEventHandle; + GamepadSystemState mGamepadSystemState; + + std::atomic_bool mStopMonitoring; + std::function mMonitorFn; + std::function mTestCommandFn; + PRThread* mMonitorThread; }; //////////// Everything below this line is Pimpl boilerplate /////////////////// @@ -79,4 +283,18 @@ GamepadStateReceiver::GamepadStateReceiver() = default; GamepadStateReceiver::GamepadStateReceiver(UniquePtr aImpl) : mImpl(std::move(aImpl)) {} +bool GamepadStateReceiver::StartMonitoringThread( + const std::function& aFn) { + return mImpl->StartMonitoringThread(aFn, nullptr); +} +bool GamepadStateReceiver::StartMonitoringThreadForTesting( + const std::function& aMonitorFn, + const std::function& aTestCommandFn) { + return mImpl->StartMonitoringThread(aMonitorFn, aTestCommandFn); +} + +void GamepadStateReceiver::StopMonitoringThread() { + mImpl->StopMonitoringThread(); +} + } // namespace mozilla::dom