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
This commit is contained in:
Chris Martin 2021-03-03 18:26:48 +00:00
parent b6e93b9fd7
commit 71e2d8d3af
13 changed files with 1143 additions and 29 deletions

View File

@ -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;

View File

@ -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<Promise> GamepadManager::SetLightIndicatorColor(
void GamepadManager::SetupRemoteInfo(
const GamepadStateBroadcastReceiverInfo& aReceiverInfo) {
mMaybeGamepadStateReceiver = GamepadStateReceiver::Create(aReceiverInfo);
if (mMaybeGamepadStateReceiver) {
RefPtr<GamepadManager> 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

View File

@ -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<GamepadAdded>(gamepadHandle, a);
}
mGamepadAdded.emplace(gamepadHandle, a);
NotifyGamepadChange<GamepadAdded>(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<GamepadRemoved>(aHandle, a);
mGamepadAdded.erase(aHandle);
if (mMaybeGamepadStateBroadcaster) {
mMaybeGamepadStateBroadcaster->RemoveGamepad(aHandle);
} else {
GamepadRemoved a;
NotifyGamepadChange<GamepadRemoved>(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<GamepadButtonInformation>(aHandle, a);
if (mMaybeGamepadStateBroadcaster) {
mMaybeGamepadStateBroadcaster->NewButtonEvent(aHandle, aButton, aPressed,
aTouched, aValue);
} else {
GamepadButtonInformation a(aButton, aValue, aPressed, aTouched);
NotifyGamepadChange<GamepadButtonInformation>(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<GamepadAxisInformation>(aHandle, a);
if (mMaybeGamepadStateBroadcaster) {
mMaybeGamepadStateBroadcaster->NewAxisMoveEvent(aHandle, aAxis, aValue);
} else {
GamepadAxisInformation a(aAxis, aValue);
NotifyGamepadChange<GamepadAxisInformation>(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<GamepadLightIndicatorTypeInformation>(aHandle, a);
if (mMaybeGamepadStateBroadcaster) {
mMaybeGamepadStateBroadcaster->NewLightIndicatorTypeEvent(aHandle, aLight,
aType);
} else {
GamepadLightIndicatorTypeInformation a(aLight, aType);
NotifyGamepadChange<GamepadLightIndicatorTypeInformation>(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<GamepadPoseInformation>(aHandle, a);
if (mMaybeGamepadStateBroadcaster) {
mMaybeGamepadStateBroadcaster->NewPoseEvent(aHandle, aState);
} else {
GamepadPoseInformation a(aState);
NotifyGamepadChange<GamepadPoseInformation>(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<GamepadTouchInformation>(aHandle, a);
if (mMaybeGamepadStateBroadcaster) {
mMaybeGamepadStateBroadcaster->NewMultiTouchEvent(aHandle, aTouchArrayIndex,
aState);
} else {
GamepadTouchInformation a(aTouchArrayIndex, aState);
NotifyGamepadChange<GamepadTouchInformation>(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);
}
}
}
}

View File

@ -146,6 +146,8 @@ class GamepadPlatformService final {
std::map<GamepadHandle, GamepadAdded> mGamepadAdded;
// This variable may contain the shared-memory "GamepadStateBroadcaster" if
// it is available. If not, everything will just fall back to using IPDL
Maybe<GamepadStateBroadcaster> mMaybeGamepadStateBroadcaster;
};

View File

@ -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<Impl> aImpl);
UniquePtr<Impl> mImpl;
friend class GamepadTestHelper;
void SendTestCommand(uint32_t aCommandId);
};
} // namespace mozilla::dom

View File

@ -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<GamepadStateReceiver> 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<GamepadStateReceiver> 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<void(const GamepadChangeEvent&)>& 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<Impl> aImpl);
UniquePtr<Impl> mImpl;
friend class GamepadTestHelper;
bool StartMonitoringThreadForTesting(
const std::function<void(const GamepadChangeEvent&)>& aMonitorFn,
const std::function<void(uint32_t)>& aTestCommandFn);
};
} // namespace mozilla::dom

View File

@ -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;

View File

@ -32,4 +32,18 @@ GamepadStateReceiver::GamepadStateReceiver() = default;
GamepadStateReceiver::GamepadStateReceiver(UniquePtr<Impl> aImpl)
: mImpl(std::move(aImpl)) {}
bool GamepadStateReceiver::StartMonitoringThread(
const std::function<void(const GamepadChangeEvent&)>& aFn) {
MOZ_CRASH("Should never be called");
}
bool GamepadStateReceiver::StartMonitoringThreadForTesting(
const std::function<void(const GamepadChangeEvent&)>& aMonitorFn,
const std::function<void(uint32_t)>& aTestCommandFn) {
MOZ_CRASH("Should never be called");
}
void GamepadStateReceiver::StopMonitoringThread() {
MOZ_CRASH("Should never be called");
}
} // namespace mozilla::dom

View File

@ -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 <array>
#include <condition_variable>
#include <list>
#include <map>
#include <mutex>
#include <random>
#include <string>
#include <thread>
#include <variant>
#include <vector>
#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<void(const GamepadChangeEvent&)> aMonitorFn,
std::function<void(uint32_t)> aTestCommandFn) {
return p->StartMonitoringThreadForTesting(aMonitorFn, aTestCommandFn);
}
static GamepadHandle GetTestHandle(uint32_t value) {
return GamepadHandle(value, GamepadHandleKind::GamepadPlatformManager);
}
};
namespace {
template <typename T>
class EventQueue {
public:
void Enqueue(T t) {
std::unique_lock<std::mutex> lock(mMutex);
mList.emplace_back(std::move(t));
mCond.notify_one();
}
T Dequeue() {
std::unique_lock<std::mutex> 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<T> mList{};
};
class Signal {
public:
void SetSignal() {
std::unique_lock<std::mutex> lock(mMutex);
mSignalled = true;
mCond.notify_one();
}
void WaitForSignal() {
std::unique_lock<std::mutex> 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<GamepadButton, kMaxButtons> buttons{};
std::array<double, kMaxAxes> 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<GamepadHandle, GamepadInfo>& a,
const std::map<GamepadHandle, GamepadInfo>& 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<GamepadHandle, GamepadInfo> expectedState) {
std::unique_lock<std::mutex> 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<GamepadHandle, GamepadInfo>& 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<GamepadHandle, GamepadInfo>& gamepads,
uint32_t commandId) {
if (commandId == kCommandQuit) {
return true;
}
MYASSERT(commandId == kCommandSelfCheck);
std::unique_lock<std::mutex> lock(mExpectedStateMutex);
ExpectEqual(mExpectedState, gamepads);
return false;
}
void ThreadFunc(GamepadStateBroadcastReceiverInfo remoteInfo) {
std::random_device rd;
std::mt19937 mersenne_twister(rd());
std::uniform_int_distribution<uint32_t> randomDelayGenerator(1, 1000);
Maybe<GamepadStateReceiver> maybeReceiver =
GamepadStateReceiver::Create(remoteInfo);
MYASSERT(maybeReceiver);
EventQueue<std::variant<GamepadChangeEvent, uint32_t>> 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<GamepadHandle, GamepadInfo> gamepads{};
bool quit = false;
while (!quit) {
auto event = eventQueue.Dequeue();
if (std::holds_alternative<GamepadChangeEvent>(event)) {
HandleGamepadEvent(gamepads, std::get<GamepadChangeEvent>(event));
} else {
quit = ProcessCommand(gamepads, std::get<uint32_t>(event));
mCommandProcessed.SetSignal();
}
}
maybeReceiver->StopMonitoringThread();
}
std::thread mThread{};
std::mutex mExpectedStateMutex{};
std::map<GamepadHandle, GamepadInfo> 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<GamepadHandle, GamepadInfo> expected{};
Maybe<GamepadStateBroadcaster> maybeBroadcaster =
GamepadStateBroadcaster::Create();
MYASSERT(maybeBroadcaster);
std::array<TestThread, kNumThreads> 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

View File

@ -4,6 +4,7 @@
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
SOURCES = [
"TestGamepadStateBroadcastWindows.cpp",
"TestSynchronizedSharedMemoryWindows.cpp",
]
else:

View File

@ -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<void(GamepadSlot&)>& 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<BroadcastEventHandle> 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;

View File

@ -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<char, kMaxGamepadIdLength + 1> 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<double, kMaxAxesPerGamepad> axes{};
Array<double, kMaxButtonsPerGamepad> buttonValues{};
Array<GamepadLightIndicatorType, kMaxLightsPerGamepad> lights{};
Array<GamepadTouchState, kMaxNumMultiTouches> touches{};
GamepadPoseState pose{};
std::bitset<kMaxButtonsPerGamepad> buttonPressedBits;
std::bitset<kMaxButtonsPerGamepad> buttonTouchedBits;
};
struct GamepadSlot {
GamepadHandle handle{};
GamepadProperties props{};
GamepadValues values{};
};
struct GamepadSystemState {
Array<GamepadSlot, kMaxGamepads> 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_

View File

@ -12,6 +12,8 @@
#include "mozilla/dom/GamepadEventTypes.h"
#include "mozilla/dom/SynchronizedSharedMemory.h"
#include "mozilla/ipc/ProtocolUtils.h"
#include "prthread.h"
#include <atomic>
#include <inttypes.h>
#include <windows.h>
@ -37,6 +39,42 @@ class GamepadStateReceiver::Impl {
new Impl(std::move(*sharedState), std::move(eventHandle)));
}
bool StartMonitoringThread(
const std::function<void(const GamepadChangeEvent&)>& aMonitorFn,
const std::function<void(uint32_t)>& 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<Impl*>(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<NTEventHandleTraits> 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<void(const GamepadChangeEvent&)>& 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<void(const GamepadChangeEvent&)>& 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<NTEventHandleTraits> mEventHandle;
GamepadSystemState mGamepadSystemState;
std::atomic_bool mStopMonitoring;
std::function<void(const GamepadChangeEvent&)> mMonitorFn;
std::function<void(uint32_t)> mTestCommandFn;
PRThread* mMonitorThread;
};
//////////// Everything below this line is Pimpl boilerplate ///////////////////
@ -79,4 +283,18 @@ GamepadStateReceiver::GamepadStateReceiver() = default;
GamepadStateReceiver::GamepadStateReceiver(UniquePtr<Impl> aImpl)
: mImpl(std::move(aImpl)) {}
bool GamepadStateReceiver::StartMonitoringThread(
const std::function<void(const GamepadChangeEvent&)>& aFn) {
return mImpl->StartMonitoringThread(aFn, nullptr);
}
bool GamepadStateReceiver::StartMonitoringThreadForTesting(
const std::function<void(const GamepadChangeEvent&)>& aMonitorFn,
const std::function<void(uint32_t)>& aTestCommandFn) {
return mImpl->StartMonitoringThread(aMonitorFn, aTestCommandFn);
}
void GamepadStateReceiver::StopMonitoringThread() {
mImpl->StopMonitoringThread();
}
} // namespace mozilla::dom