mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 23:02:20 +00:00
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:
parent
b6e93b9fd7
commit
71e2d8d3af
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
451
dom/gamepad/tests/gtest/TestGamepadStateBroadcastWindows.cpp
Normal file
451
dom/gamepad/tests/gtest/TestGamepadStateBroadcastWindows.cpp
Normal 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
|
@ -4,6 +4,7 @@
|
||||
|
||||
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
|
||||
SOURCES = [
|
||||
"TestGamepadStateBroadcastWindows.cpp",
|
||||
"TestSynchronizedSharedMemoryWindows.cpp",
|
||||
]
|
||||
else:
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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_
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user