diff options
Diffstat (limited to 'dom/gamepad')
44 files changed, 8924 insertions, 0 deletions
diff --git a/dom/gamepad/Gamepad.cpp b/dom/gamepad/Gamepad.cpp new file mode 100644 index 0000000000..f64bb7f4eb --- /dev/null +++ b/dom/gamepad/Gamepad.cpp @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Gamepad.h" +#include "nsPIDOMWindow.h" +#include "nsTArray.h" +#include "nsVariant.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/GamepadBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Gamepad) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Gamepad) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Gamepad) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Gamepad, mParent, mButtons, mPose, + mHapticActuators, mLightIndicators, + mTouchEvents) + +void Gamepad::UpdateTimestamp() { + nsCOMPtr<nsPIDOMWindowInner> newWindow(do_QueryInterface(mParent)); + if (newWindow) { + Performance* perf = newWindow->GetPerformance(); + if (perf) { + mTimestamp = perf->Now(); + } + } +} + +Gamepad::Gamepad(nsISupports* aParent, const nsAString& aID, int32_t aIndex, + GamepadHandle aHandle, GamepadMappingType aMapping, + GamepadHand aHand, uint32_t aDisplayID, uint32_t aNumButtons, + uint32_t aNumAxes, uint32_t aNumHaptics, + uint32_t aNumLightIndicator, uint32_t aNumTouchEvents) + : mParent(aParent), + mID(aID), + mIndex(aIndex), + mHandle(aHandle), + mDisplayId(aDisplayID), + mTouchIdHashValue(0), + mMapping(aMapping), + mHand(aHand), + mConnected(true), + mButtons(aNumButtons), + mAxes(aNumAxes), + mTimestamp(0) { + for (unsigned i = 0; i < aNumButtons; i++) { + mButtons.InsertElementAt(i, new GamepadButton(mParent)); + } + mAxes.InsertElementsAt(0, aNumAxes, 0.0f); + mPose = new GamepadPose(aParent); + for (uint32_t i = 0; i < aNumHaptics; ++i) { + mHapticActuators.AppendElement( + new GamepadHapticActuator(mParent, mHandle, i)); + } + for (uint32_t i = 0; i < aNumLightIndicator; ++i) { + mLightIndicators.AppendElement( + new GamepadLightIndicator(mParent, mHandle, i)); + } + for (uint32_t i = 0; i < aNumTouchEvents; ++i) { + mTouchEvents.AppendElement(new GamepadTouch(mParent)); + } + + // Mapping touchId(0) to touchIdHash(0) by default. + mTouchIdHash.InsertOrUpdate(0, mTouchIdHashValue); + ++mTouchIdHashValue; + UpdateTimestamp(); +} + +void Gamepad::SetIndex(int32_t aIndex) { mIndex = aIndex; } + +void Gamepad::SetConnected(bool aConnected) { mConnected = aConnected; } + +void Gamepad::SetButton(uint32_t aButton, bool aPressed, bool aTouched, + double aValue) { + MOZ_ASSERT(aButton < mButtons.Length()); + mButtons[aButton]->SetPressed(aPressed); + mButtons[aButton]->SetTouched(aTouched); + mButtons[aButton]->SetValue(aValue); + UpdateTimestamp(); +} + +void Gamepad::SetAxis(uint32_t aAxis, double aValue) { + MOZ_ASSERT(aAxis < mAxes.Length()); + if (mAxes[aAxis] != aValue) { + mAxes[aAxis] = aValue; + Gamepad_Binding::ClearCachedAxesValue(this); + } + UpdateTimestamp(); +} + +void Gamepad::SetPose(const GamepadPoseState& aPose) { + mPose->SetPoseState(aPose); + UpdateTimestamp(); +} + +void Gamepad::SetLightIndicatorType(uint32_t aLightIndex, + GamepadLightIndicatorType aType) { + mLightIndicators[aLightIndex]->SetType(aType); + UpdateTimestamp(); +} + +void Gamepad::SetTouchEvent(uint32_t aTouchIndex, + const GamepadTouchState& aTouch) { + if (aTouchIndex >= mTouchEvents.Length()) { + MOZ_CRASH("Touch index exceeds the event array."); + return; + } + + // Handling cross-origin tracking. + GamepadTouchState touchState(aTouch); + touchState.touchId = mTouchIdHash.LookupOrInsertWith( + touchState.touchId, [&] { return mTouchIdHashValue++; }); + mTouchEvents[aTouchIndex]->SetTouchState(touchState); + UpdateTimestamp(); +} + +void Gamepad::SetHand(GamepadHand aHand) { mHand = aHand; } + +void Gamepad::SyncState(Gamepad* aOther) { + if (mButtons.Length() != aOther->mButtons.Length() || + mAxes.Length() != aOther->mAxes.Length()) { + return; + } + + mConnected = aOther->mConnected; + for (uint32_t i = 0; i < mButtons.Length(); ++i) { + mButtons[i]->SetPressed(aOther->mButtons[i]->Pressed()); + mButtons[i]->SetTouched(aOther->mButtons[i]->Touched()); + mButtons[i]->SetValue(aOther->mButtons[i]->Value()); + } + + bool changed = false; + for (uint32_t i = 0; i < mAxes.Length(); ++i) { + changed = changed || (mAxes[i] != aOther->mAxes[i]); + mAxes[i] = aOther->mAxes[i]; + } + if (changed) { + Gamepad_Binding::ClearCachedAxesValue(this); + } + + if (StaticPrefs::dom_gamepad_extensions_enabled()) { + MOZ_ASSERT(aOther->GetPose()); + mPose->SetPoseState(aOther->GetPose()->GetPoseState()); + mHand = aOther->Hand(); + for (uint32_t i = 0; i < mHapticActuators.Length(); ++i) { + mHapticActuators[i]->Set(aOther->mHapticActuators[i]); + } + + if (StaticPrefs::dom_gamepad_extensions_lightindicator()) { + for (uint32_t i = 0; i < mLightIndicators.Length(); ++i) { + mLightIndicators[i]->Set(aOther->mLightIndicators[i]); + } + } + if (StaticPrefs::dom_gamepad_extensions_multitouch()) { + for (uint32_t i = 0; i < mTouchEvents.Length(); ++i) { + mTouchEvents[i]->Set(aOther->mTouchEvents[i]); + } + } + } + + UpdateTimestamp(); +} + +already_AddRefed<Gamepad> Gamepad::Clone(nsISupports* aParent) { + RefPtr<Gamepad> out = + new Gamepad(aParent, mID, mIndex, mHandle, mMapping, mHand, mDisplayId, + mButtons.Length(), mAxes.Length(), mHapticActuators.Length(), + mLightIndicators.Length(), mTouchEvents.Length()); + out->SyncState(this); + return out.forget(); +} + +/* virtual */ +JSObject* Gamepad::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return Gamepad_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/Gamepad.h b/dom/gamepad/Gamepad.h new file mode 100644 index 0000000000..689ea7f6d2 --- /dev/null +++ b/dom/gamepad/Gamepad.h @@ -0,0 +1,145 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_Gamepad_h +#define mozilla_dom_gamepad_Gamepad_h + +#include "mozilla/dom/GamepadBinding.h" +#include "mozilla/dom/GamepadButton.h" +#include "mozilla/dom/GamepadHandle.h" +#include "mozilla/dom/GamepadPose.h" +#include "mozilla/dom/GamepadHapticActuator.h" +#include "mozilla/dom/GamepadLightIndicator.h" +#include "mozilla/dom/GamepadTouch.h" +#include "mozilla/dom/Performance.h" +#include <stdint.h> +#include "nsCOMPtr.h" +#include "nsTHashMap.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class GamepadHapticActuator; + +// Per spec: +// https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html#remapping +const int kStandardGamepadButtons = 17; +const int kStandardGamepadAxes = 4; + +const int kButtonLeftTrigger = 6; +const int kButtonRightTrigger = 7; + +const int kLeftStickXAxis = 0; +const int kLeftStickYAxis = 1; +const int kRightStickXAxis = 2; +const int kRightStickYAxis = 3; + +class Gamepad final : public nsISupports, public nsWrapperCache { + public: + Gamepad(nsISupports* aParent, const nsAString& aID, int32_t aIndex, + GamepadHandle aHandle, GamepadMappingType aMapping, GamepadHand aHand, + uint32_t aDisplayID, uint32_t aNumButtons, uint32_t aNumAxes, + uint32_t aNumHaptics, uint32_t aNumLightIndicator, + uint32_t aNumTouchEvents); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Gamepad) + + void SetConnected(bool aConnected); + void SetButton(uint32_t aButton, bool aPressed, bool aTouched, double aValue); + void SetAxis(uint32_t aAxis, double aValue); + void SetIndex(int32_t aIndex); + void SetPose(const GamepadPoseState& aPose); + void SetLightIndicatorType(uint32_t aLightIndex, + GamepadLightIndicatorType aType); + void SetTouchEvent(uint32_t aTouchIndex, const GamepadTouchState& aTouch); + void SetHand(GamepadHand aHand); + + // Make the state of this gamepad equivalent to other. + void SyncState(Gamepad* aOther); + + // Return a new Gamepad containing the same data as this object, + // parented to aParent. + already_AddRefed<Gamepad> Clone(nsISupports* aParent); + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetId(nsAString& aID) const { aID = mID; } + + DOMHighResTimeStamp Timestamp() const { return mTimestamp; } + + GamepadMappingType Mapping() { return mMapping; } + + uint32_t DisplayId() const { return mDisplayId; } + + GamepadHand Hand() { return mHand; } + + bool Connected() const { return mConnected; } + + int32_t Index() const { return mIndex; } + + void GetButtons(nsTArray<RefPtr<GamepadButton>>& aButtons) const { + aButtons = mButtons.Clone(); + } + + void GetAxes(nsTArray<double>& aAxes) const { aAxes = mAxes.Clone(); } + + GamepadPose* GetPose() const { return mPose; } + + void GetHapticActuators( + nsTArray<RefPtr<GamepadHapticActuator>>& aHapticActuators) const { + aHapticActuators = mHapticActuators.Clone(); + } + + void GetLightIndicators( + nsTArray<RefPtr<GamepadLightIndicator>>& aLightIndicators) const { + aLightIndicators = mLightIndicators.Clone(); + } + + void GetTouchEvents(nsTArray<RefPtr<GamepadTouch>>& aTouchEvents) const { + aTouchEvents = mTouchEvents.Clone(); + } + + GamepadHandle GetHandle() const { return mHandle; } + + private: + virtual ~Gamepad() = default; + void UpdateTimestamp(); + + protected: + nsCOMPtr<nsISupports> mParent; + nsString mID; + int32_t mIndex; + // the gamepad hash key in GamepadManager + GamepadHandle mHandle; + uint32_t mDisplayId; + uint32_t mTouchIdHashValue; + // The mapping in use. + GamepadMappingType mMapping; + GamepadHand mHand; + + // true if this gamepad is currently connected. + bool mConnected; + + // Current state of buttons, axes. + nsTArray<RefPtr<GamepadButton>> mButtons; + nsTArray<double> mAxes; + DOMHighResTimeStamp mTimestamp; + RefPtr<GamepadPose> mPose; + nsTArray<RefPtr<GamepadHapticActuator>> mHapticActuators; + nsTArray<RefPtr<GamepadLightIndicator>> mLightIndicators; + nsTArray<RefPtr<GamepadTouch>> mTouchEvents; + nsTHashMap<nsUint32HashKey, uint32_t> mTouchIdHash; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_Gamepad_h diff --git a/dom/gamepad/GamepadButton.cpp b/dom/gamepad/GamepadButton.cpp new file mode 100644 index 0000000000..3ee985af3c --- /dev/null +++ b/dom/gamepad/GamepadButton.cpp @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/GamepadButton.h" +#include "mozilla/dom/GamepadBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(GamepadButton) +NS_IMPL_CYCLE_COLLECTING_RELEASE(GamepadButton) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GamepadButton) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(GamepadButton, mParent) + +/* virtual */ +JSObject* GamepadButton::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return GamepadButton_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadButton.h b/dom/gamepad/GamepadButton.h new file mode 100644 index 0000000000..74bde07fcf --- /dev/null +++ b/dom/gamepad/GamepadButton.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_GamepadButton_h +#define mozilla_dom_gamepad_GamepadButton_h + +#include <stdint.h> +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class GamepadButton : public nsISupports, public nsWrapperCache { + public: + explicit GamepadButton(nsISupports* aParent) + : mParent(aParent), mValue(0), mPressed(false), mTouched(false) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(GamepadButton) + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void SetPressed(bool aPressed) { mPressed = aPressed; } + + void SetTouched(bool aTouched) { mTouched = aTouched; } + + void SetValue(double aValue) { mValue = aValue; } + + bool Pressed() const { return mPressed; } + + bool Touched() const { return mTouched; } + + double Value() const { return mValue; } + + private: + virtual ~GamepadButton() = default; + + protected: + nsCOMPtr<nsISupports> mParent; + double mValue; + bool mPressed; + bool mTouched; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_GamepadButton_h diff --git a/dom/gamepad/GamepadHandle.cpp b/dom/gamepad/GamepadHandle.cpp new file mode 100644 index 0000000000..0357975a71 --- /dev/null +++ b/dom/gamepad/GamepadHandle.cpp @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GamepadHandle.h" +#include "mozilla/Assertions.h" +#include "mozilla/HashFunctions.h" + +namespace mozilla::dom { + +GamepadHandle::GamepadHandle(uint32_t aValue, GamepadHandleKind aKind) + : mValue(aValue), mKind(aKind) { + MOZ_RELEASE_ASSERT(mValue); +} + +GamepadHandleKind GamepadHandle::GetKind() const { return mKind; } + +PLDHashNumber GamepadHandle::Hash() const { + return HashGeneric(mValue, uint8_t(mKind)); +} + +bool operator==(const GamepadHandle& a, const GamepadHandle& b) { + return (a.mValue == b.mValue) && (a.mKind == b.mKind); +} + +bool operator!=(const GamepadHandle& a, const GamepadHandle& b) { + return !(a == b); +} + +bool operator<(const GamepadHandle& a, const GamepadHandle& b) { + if (a.mKind == b.mKind) { + return a.mValue < b.mValue; + } + // Arbitrarily order them by kind + return uint8_t(a.mKind) < uint8_t(b.mKind); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadHandle.h b/dom/gamepad/GamepadHandle.h new file mode 100644 index 0000000000..c13e2b912e --- /dev/null +++ b/dom/gamepad/GamepadHandle.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file defines a strongly-typed opaque gamepad handle +// +// The handle is designed to be copied around and passed over IPC. It keeps +// track of which "provider" created the handle so it can ensure that +// providers are never mixed. It also allows each provider to have its own +// algorithm for generating gamepad IDs, since the VR and Platform services +// do it differently. + +#ifndef mozilla_dom_gamepad_GamepadHandle_h +#define mozilla_dom_gamepad_GamepadHandle_h + +#include "PLDHashTable.h" +#include <type_traits> +#include <cinttypes> + +namespace IPC { + +template <class> +struct ParamTraits; + +} // namespace IPC + +namespace mozilla::gfx { + +class VRDisplayClient; +class VRManager; + +} // namespace mozilla::gfx + +namespace mozilla::dom { + +class GamepadPlatformService; +class GamepadServiceTest; +class XRInputSource; + +// The "kind" of a gamepad handle is based on which provider created it +enum class GamepadHandleKind : uint8_t { + GamepadPlatformManager, + VR, +}; + +class GamepadHandle { + public: + // Allow handle to be passed around as a simple object + GamepadHandle() = default; + GamepadHandle(const GamepadHandle&) = default; + GamepadHandle& operator=(const GamepadHandle&) = default; + + // Helps code know which manager to send requests to + GamepadHandleKind GetKind() const; + + // Define operators so the handle can compared and stored in maps + friend bool operator==(const GamepadHandle& a, const GamepadHandle& b); + friend bool operator!=(const GamepadHandle& a, const GamepadHandle& b); + friend bool operator<(const GamepadHandle& a, const GamepadHandle& b); + + PLDHashNumber Hash() const; + + private: + explicit GamepadHandle(uint32_t aValue, GamepadHandleKind aKind); + uint32_t GetValue() const { return mValue; } + + uint32_t mValue{0}; + GamepadHandleKind mKind{GamepadHandleKind::GamepadPlatformManager}; + + // These are the classes that are "gamepad managers". They are allowed to + // create new handles and inspect their actual value + friend class mozilla::dom::GamepadPlatformService; + friend class mozilla::dom::GamepadServiceTest; + friend class mozilla::dom::XRInputSource; + friend class mozilla::gfx::VRDisplayClient; + friend class mozilla::gfx::VRManager; + + // Allow IPDL to serialize us + friend struct IPC::ParamTraits<mozilla::dom::GamepadHandle>; +}; + +static_assert(std::is_trivially_copyable<GamepadHandle>::value, + "GamepadHandle must be trivially copyable"); + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_GamepadHandle_h diff --git a/dom/gamepad/GamepadHapticActuator.cpp b/dom/gamepad/GamepadHapticActuator.cpp new file mode 100644 index 0000000000..462e386c8e --- /dev/null +++ b/dom/gamepad/GamepadHapticActuator.cpp @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/GamepadHapticActuator.h" +#include "mozilla/dom/GamepadManager.h" +#include "mozilla/dom/Promise.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(GamepadHapticActuator) +NS_IMPL_CYCLE_COLLECTING_RELEASE(GamepadHapticActuator) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GamepadHapticActuator) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(GamepadHapticActuator, mParent) + +GamepadHapticActuator::GamepadHapticActuator(nsISupports* aParent, + GamepadHandle aGamepadHandle, + uint32_t aIndex) + : mParent(aParent), + mGamepadHandle(aGamepadHandle), + mType(GamepadHapticActuatorType::Vibration), + mIndex(aIndex) {} + +/* virtual */ +JSObject* GamepadHapticActuator::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return GamepadHapticActuator_Binding::Wrap(aCx, this, aGivenProto); +} + +nsISupports* GamepadHapticActuator::GetParentObject() const { return mParent; } + +#define CLAMP(f, min, max) (((f) < min) ? min : (((f) > max) ? max : (f))) + +already_AddRefed<Promise> GamepadHapticActuator::Pulse(double aValue, + double aDuration, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + MOZ_ASSERT(global); + + RefPtr<GamepadManager> gamepadManager(GamepadManager::GetService()); + MOZ_ASSERT(gamepadManager); + + // Clamp intensity aValue to be 0~1. + double value = CLAMP(aValue, 0, 1); + // aDuration should be always positive. + double duration = CLAMP(aDuration, 0, aDuration); + + switch (mType) { + case GamepadHapticActuatorType::Vibration: { + RefPtr<Promise> promise = gamepadManager->VibrateHaptic( + mGamepadHandle, mIndex, value, duration, global, aRv); + if (!promise) { + return nullptr; + } + return promise.forget(); + } + default: { + // We need to implement other types of haptic + MOZ_ASSERT(false); + return nullptr; + } + } +} + +GamepadHapticActuatorType GamepadHapticActuator::Type() const { return mType; } + +void GamepadHapticActuator::Set(const GamepadHapticActuator* aOther) { + mGamepadHandle = aOther->mGamepadHandle; + mType = aOther->mType; + mIndex = aOther->mIndex; +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadHapticActuator.h b/dom/gamepad/GamepadHapticActuator.h new file mode 100644 index 0000000000..18b659137b --- /dev/null +++ b/dom/gamepad/GamepadHapticActuator.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_GamepadHapticActuator_h +#define mozilla_dom_gamepad_GamepadHapticActuator_h + +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/GamepadHapticActuatorBinding.h" +#include "mozilla/dom/Gamepad.h" +#include "mozilla/dom/GamepadHandle.h" + +namespace mozilla::dom { +class Promise; + +class GamepadHapticActuator : public nsISupports, public nsWrapperCache { + public: + GamepadHapticActuator(nsISupports* aParent, GamepadHandle aGamepadHandle, + uint32_t aIndex); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(GamepadHapticActuator) + + nsISupports* GetParentObject() const; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + already_AddRefed<Promise> Pulse(double aValue, double aDuration, + ErrorResult& aRv); + + GamepadHapticActuatorType Type() const; + + void Set(const GamepadHapticActuator* aOther); + + private: + virtual ~GamepadHapticActuator() = default; + + protected: + nsCOMPtr<nsISupports> mParent; + GamepadHandle mGamepadHandle; + GamepadHapticActuatorType mType; + uint32_t mIndex; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_GamepadHapticActuator_h diff --git a/dom/gamepad/GamepadLightIndicator.cpp b/dom/gamepad/GamepadLightIndicator.cpp new file mode 100644 index 0000000000..0ddb1bffe6 --- /dev/null +++ b/dom/gamepad/GamepadLightIndicator.cpp @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/GamepadLightIndicator.h" +#include "mozilla/dom/GamepadManager.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/HoldDropJSObjects.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(GamepadLightIndicator) +NS_IMPL_CYCLE_COLLECTING_RELEASE(GamepadLightIndicator) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GamepadLightIndicator) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(GamepadLightIndicator, mParent) + +GamepadLightIndicator::GamepadLightIndicator(nsISupports* aParent, + GamepadHandle aGamepadHandle, + uint32_t aIndex) + : mParent(aParent), + mType(DefaultType()), + mGamepadHandle(aGamepadHandle), + mIndex(aIndex) {} + +GamepadLightIndicator::~GamepadLightIndicator() { + mozilla::DropJSObjects(this); +} + +/* virtual */ JSObject* GamepadLightIndicator::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return GamepadLightIndicator_Binding::Wrap(aCx, this, aGivenProto); +} + +nsISupports* GamepadLightIndicator::GetParentObject() const { return mParent; } + +already_AddRefed<Promise> GamepadLightIndicator::SetColor( + const GamepadLightColor& color, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + MOZ_ASSERT(global); + + RefPtr<GamepadManager> gamepadManager(GamepadManager::GetService()); + MOZ_ASSERT(gamepadManager); + + RefPtr<Promise> promise = gamepadManager->SetLightIndicatorColor( + mGamepadHandle, mIndex, color.mRed, color.mGreen, color.mBlue, global, + aRv); + if (!promise) { + return nullptr; + } + return promise.forget(); +} + +GamepadLightIndicatorType GamepadLightIndicator::Type() const { return mType; } + +void GamepadLightIndicator::Set(const GamepadLightIndicator* aOther) { + MOZ_ASSERT(aOther); + mGamepadHandle = aOther->mGamepadHandle; + mType = aOther->mType; + mIndex = aOther->mIndex; +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadLightIndicator.h b/dom/gamepad/GamepadLightIndicator.h new file mode 100644 index 0000000000..80c6f9c13b --- /dev/null +++ b/dom/gamepad/GamepadLightIndicator.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_GamepadLightIndicator_h +#define mozilla_dom_gamepad_GamepadLightIndicator_h + +#include "mozilla/dom/GamepadLightIndicatorBinding.h" +#include "mozilla/dom/GamepadHandle.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class GamepadLightIndicator final : public nsISupports, public nsWrapperCache { + public: + GamepadLightIndicator(nsISupports* aParent, GamepadHandle aGamepadHandle, + uint32_t aIndex); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(GamepadLightIndicator) + + static GamepadLightIndicatorType DefaultType() { + return GamepadLightIndicatorType::Rgb; + } + + nsISupports* GetParentObject() const; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + already_AddRefed<Promise> SetColor(const GamepadLightColor& color, + ErrorResult& aRv); + + void SetType(GamepadLightIndicatorType aType) { mType = aType; } + + GamepadLightIndicatorType Type() const; + + void Set(const GamepadLightIndicator* aOther); + + private: + virtual ~GamepadLightIndicator(); + + nsCOMPtr<nsISupports> mParent; + GamepadLightIndicatorType mType; + GamepadHandle mGamepadHandle; + uint32_t mIndex; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_GamepadLightIndicator_h diff --git a/dom/gamepad/GamepadManager.cpp b/dom/gamepad/GamepadManager.cpp new file mode 100644 index 0000000000..8d67670746 --- /dev/null +++ b/dom/gamepad/GamepadManager.cpp @@ -0,0 +1,662 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/GamepadManager.h" + +#include "mozilla/dom/Gamepad.h" +#include "mozilla/dom/GamepadAxisMoveEvent.h" +#include "mozilla/dom/GamepadButtonEvent.h" +#include "mozilla/dom/GamepadEvent.h" +#include "mozilla/dom/GamepadEventChannelChild.h" +#include "mozilla/dom/GamepadMonitoring.h" +#include "mozilla/dom/Promise.h" + +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" + +#include "nsContentUtils.h" +#include "nsGlobalWindowInner.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsThreadUtils.h" +#include "VRManagerChild.h" +#include "mozilla/Services.h" +#include "mozilla/Unused.h" + +#include <cstddef> + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +namespace { + +const nsTArray<RefPtr<nsGlobalWindowInner>>::index_type NoIndex = + nsTArray<RefPtr<nsGlobalWindowInner>>::NoIndex; + +bool sShutdown = false; + +StaticRefPtr<GamepadManager> gGamepadManagerSingleton; + +// A threshold value of axis move to determine the first +// intent. +const float AXIS_FIRST_INTENT_THRESHOLD_VALUE = 0.1f; + +} // namespace + +NS_IMPL_ISUPPORTS(GamepadManager, nsIObserver) + +GamepadManager::GamepadManager() + : mEnabled(false), + mNonstandardEventsEnabled(false), + mShuttingDown(false), + mPromiseID(0) {} + +nsresult GamepadManager::Init() { + mEnabled = StaticPrefs::dom_gamepad_enabled(); + mNonstandardEventsEnabled = + StaticPrefs::dom_gamepad_non_standard_events_enabled(); + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + if (NS_WARN_IF(!observerService)) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + rv = observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, + false); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +GamepadManager::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID); + } + BeginShutdown(); + return NS_OK; +} + +void GamepadManager::StopMonitoring() { + if (mChannelChild) { + PGamepadEventChannelChild::Send__delete__(mChannelChild); + mChannelChild = nullptr; + } + if (gfx::VRManagerChild::IsCreated()) { + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->SendControllerListenerRemoved(); + } + mGamepads.Clear(); +} + +void GamepadManager::BeginShutdown() { + mShuttingDown = true; + StopMonitoring(); + // Don't let windows call back to unregister during shutdown + for (uint32_t i = 0; i < mListeners.Length(); i++) { + mListeners[i]->SetHasGamepadEventListener(false); + } + mListeners.Clear(); + sShutdown = true; +} + +void GamepadManager::AddListener(nsGlobalWindowInner* aWindow) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(NS_IsMainThread()); + + // IPDL child has not been created + if (!mChannelChild) { + PBackgroundChild* actor = BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actor)) { + // We are probably shutting down. + return; + } + + RefPtr<GamepadEventChannelChild> child(GamepadEventChannelChild::Create()); + if (!actor->SendPGamepadEventChannelConstructor(child.get())) { + // We are probably shutting down. + return; + } + + mChannelChild = child; + + if (gfx::VRManagerChild::IsCreated()) { + // Construct VRManagerChannel and ask adding the connected + // VR controllers to GamepadManager + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->SendControllerListenerAdded(); + } + } + + if (!mEnabled || mShuttingDown || + aWindow->ShouldResistFingerprinting(RFPTarget::Gamepad)) { + return; + } + + if (mListeners.IndexOf(aWindow) != NoIndex) { + return; // already exists + } + + mListeners.AppendElement(aWindow); +} + +void GamepadManager::RemoveListener(nsGlobalWindowInner* aWindow) { + MOZ_ASSERT(aWindow); + + if (mShuttingDown) { + // Doesn't matter at this point. It's possible we're being called + // as a result of our own destructor here, so just bail out. + return; + } + + if (mListeners.IndexOf(aWindow) == NoIndex) { + return; // doesn't exist + } + + for (const auto& key : mGamepads.Keys()) { + aWindow->RemoveGamepad(key); + } + + mListeners.RemoveElement(aWindow); + + if (mListeners.IsEmpty()) { + StopMonitoring(); + } +} + +already_AddRefed<Gamepad> GamepadManager::GetGamepad( + GamepadHandle aHandle) const { + RefPtr<Gamepad> gamepad; + if (mGamepads.Get(aHandle, getter_AddRefs(gamepad))) { + return gamepad.forget(); + } + + return nullptr; +} + +void GamepadManager::AddGamepad(GamepadHandle aHandle, const nsAString& aId, + GamepadMappingType aMapping, GamepadHand aHand, + uint32_t aDisplayID, uint32_t aNumButtons, + uint32_t aNumAxes, uint32_t aNumHaptics, + uint32_t aNumLightIndicator, + uint32_t aNumTouchEvents) { + // TODO: bug 852258: get initial button/axis state + RefPtr<Gamepad> newGamepad = + new Gamepad(nullptr, aId, + 0, // index is set by global window + aHandle, aMapping, aHand, aDisplayID, aNumButtons, aNumAxes, + aNumHaptics, aNumLightIndicator, aNumTouchEvents); + + // We store the gamepad related to its index given by the parent process, + // and no duplicate index is allowed. + MOZ_ASSERT(!mGamepads.Contains(aHandle)); + mGamepads.InsertOrUpdate(aHandle, std::move(newGamepad)); + NewConnectionEvent(aHandle, true); +} + +void GamepadManager::RemoveGamepad(GamepadHandle aHandle) { + RefPtr<Gamepad> gamepad = GetGamepad(aHandle); + if (!gamepad) { + NS_WARNING("Trying to delete gamepad with invalid index"); + return; + } + gamepad->SetConnected(false); + NewConnectionEvent(aHandle, false); + mGamepads.Remove(aHandle); +} + +void GamepadManager::FireButtonEvent(EventTarget* aTarget, Gamepad* aGamepad, + uint32_t aButton, double aValue) { + nsString name = + aValue == 1.0L ? u"gamepadbuttondown"_ns : u"gamepadbuttonup"_ns; + GamepadButtonEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mGamepad = aGamepad; + init.mButton = aButton; + RefPtr<GamepadButtonEvent> event = + GamepadButtonEvent::Constructor(aTarget, name, init); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void GamepadManager::FireAxisMoveEvent(EventTarget* aTarget, Gamepad* aGamepad, + uint32_t aAxis, double aValue) { + GamepadAxisMoveEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mGamepad = aGamepad; + init.mAxis = aAxis; + init.mValue = aValue; + RefPtr<GamepadAxisMoveEvent> event = + GamepadAxisMoveEvent::Constructor(aTarget, u"gamepadaxismove"_ns, init); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void GamepadManager::NewConnectionEvent(GamepadHandle aHandle, + bool aConnected) { + if (mShuttingDown) { + return; + } + + RefPtr<Gamepad> gamepad = GetGamepad(aHandle); + if (!gamepad) { + return; + } + + // Hold on to listeners in a separate array because firing events + // can mutate the mListeners array. + nsTArray<RefPtr<nsGlobalWindowInner>> listeners(mListeners.Clone()); + + if (aConnected) { + for (uint32_t i = 0; i < listeners.Length(); i++) { +#ifdef NIGHTLY_BUILD + // Don't fire a gamepadconnected event unless it's a secure context + if (!listeners[i]->IsSecureContext()) { + continue; + } +#endif + + // Do not fire gamepadconnected and gamepaddisconnected events when + // privacy.resistFingerprinting is true. + if (listeners[i]->ShouldResistFingerprinting(RFPTarget::Gamepad)) { + continue; + } + + // Only send events to non-background windows + if (!listeners[i]->IsCurrentInnerWindow() || + listeners[i]->GetOuterWindow()->IsBackground()) { + continue; + } + + // We don't fire a connected event here unless the window + // has seen input from at least one device. + if (!listeners[i]->HasSeenGamepadInput()) { + continue; + } + + SetWindowHasSeenGamepad(listeners[i], aHandle); + + RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aHandle); + if (listenerGamepad) { + // Fire event + FireConnectionEvent(listeners[i], listenerGamepad, aConnected); + } + } + } else { + // For disconnection events, fire one at every window that has received + // data from this gamepad. + for (uint32_t i = 0; i < listeners.Length(); i++) { + // Even background windows get these events, so we don't have to + // deal with the hassle of syncing the state of removed gamepads. + + // Do not fire gamepadconnected and gamepaddisconnected events when + // privacy.resistFingerprinting is true. + if (listeners[i]->ShouldResistFingerprinting(RFPTarget::Gamepad)) { + continue; + } + + if (WindowHasSeenGamepad(listeners[i], aHandle)) { + RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aHandle); + if (listenerGamepad) { + listenerGamepad->SetConnected(false); + // Fire event + FireConnectionEvent(listeners[i], listenerGamepad, false); + listeners[i]->RemoveGamepad(aHandle); + } + } + } + } +} + +void GamepadManager::FireConnectionEvent(EventTarget* aTarget, + Gamepad* aGamepad, bool aConnected) { + nsString name = + aConnected ? u"gamepadconnected"_ns : u"gamepaddisconnected"_ns; + GamepadEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mGamepad = aGamepad; + RefPtr<GamepadEvent> event = GamepadEvent::Constructor(aTarget, name, init); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void GamepadManager::SyncGamepadState(GamepadHandle aHandle, + nsGlobalWindowInner* aWindow, + Gamepad* aGamepad) { + if (mShuttingDown || !mEnabled || + aWindow->ShouldResistFingerprinting(RFPTarget::Gamepad)) { + return; + } + + RefPtr<Gamepad> gamepad = GetGamepad(aHandle); + if (!gamepad) { + return; + } + + aGamepad->SyncState(gamepad); +} + +// static +bool GamepadManager::IsServiceRunning() { return !!gGamepadManagerSingleton; } + +// static +already_AddRefed<GamepadManager> GamepadManager::GetService() { + if (sShutdown) { + return nullptr; + } + + if (!gGamepadManagerSingleton) { + RefPtr<GamepadManager> manager = new GamepadManager(); + nsresult rv = manager->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + gGamepadManagerSingleton = manager; + ClearOnShutdown(&gGamepadManagerSingleton); + } + + RefPtr<GamepadManager> service(gGamepadManagerSingleton); + return service.forget(); +} + +bool GamepadManager::AxisMoveIsFirstIntent(nsGlobalWindowInner* aWindow, + GamepadHandle aHandle, + const GamepadChangeEvent& aEvent) { + const GamepadChangeEventBody& body = aEvent.body(); + if (!WindowHasSeenGamepad(aWindow, aHandle) && + body.type() == GamepadChangeEventBody::TGamepadAxisInformation) { + // Some controllers would send small axis values even they are just idle. + // To avoid controllers be activated without its first intent. + const GamepadAxisInformation& a = body.get_GamepadAxisInformation(); + if (abs(a.value()) < AXIS_FIRST_INTENT_THRESHOLD_VALUE) { + return false; + } + } + return true; +} + +bool GamepadManager::MaybeWindowHasSeenGamepad(nsGlobalWindowInner* aWindow, + GamepadHandle aHandle) { + if (!WindowHasSeenGamepad(aWindow, aHandle)) { + // This window hasn't seen this gamepad before, so + // send a connection event first. + SetWindowHasSeenGamepad(aWindow, aHandle); + return false; + } + return true; +} + +bool GamepadManager::WindowHasSeenGamepad(nsGlobalWindowInner* aWindow, + GamepadHandle aHandle) const { + RefPtr<Gamepad> gamepad = aWindow->GetGamepad(aHandle); + return gamepad != nullptr; +} + +void GamepadManager::SetWindowHasSeenGamepad(nsGlobalWindowInner* aWindow, + GamepadHandle aHandle, + bool aHasSeen) { + MOZ_ASSERT(aWindow); + + if (mListeners.IndexOf(aWindow) == NoIndex) { + // This window isn't even listening for gamepad events. + return; + } + + if (aHasSeen) { + aWindow->SetHasSeenGamepadInput(true); + nsCOMPtr<nsISupports> window = ToSupports(aWindow); + RefPtr<Gamepad> gamepad = GetGamepad(aHandle); + if (!gamepad) { + return; + } + RefPtr<Gamepad> clonedGamepad = gamepad->Clone(window); + aWindow->AddGamepad(aHandle, clonedGamepad); + } else { + aWindow->RemoveGamepad(aHandle); + } +} + +void GamepadManager::Update(const GamepadChangeEvent& aEvent) { + if (!mEnabled || mShuttingDown) { + return; + } + + const GamepadHandle handle = aEvent.handle(); + + GamepadChangeEventBody body = aEvent.body(); + + if (body.type() == GamepadChangeEventBody::TGamepadAdded) { + const GamepadAdded& a = body.get_GamepadAdded(); + AddGamepad(handle, a.id(), static_cast<GamepadMappingType>(a.mapping()), + static_cast<GamepadHand>(a.hand()), a.display_id(), + a.num_buttons(), a.num_axes(), a.num_haptics(), a.num_lights(), + a.num_touches()); + return; + } + if (body.type() == GamepadChangeEventBody::TGamepadRemoved) { + RemoveGamepad(handle); + return; + } + + if (!SetGamepadByEvent(aEvent)) { + return; + } + + // Hold on to listeners in a separate array because firing events + // can mutate the mListeners array. + nsTArray<RefPtr<nsGlobalWindowInner>> listeners(mListeners.Clone()); + + for (uint32_t i = 0; i < listeners.Length(); i++) { + // Only send events to non-background windows + if (!listeners[i]->IsCurrentInnerWindow() || + listeners[i]->GetOuterWindow()->IsBackground() || + listeners[i]->ShouldResistFingerprinting(RFPTarget::Gamepad)) { + continue; + } + + SetGamepadByEvent(aEvent, listeners[i]); + MaybeConvertToNonstandardGamepadEvent(aEvent, listeners[i]); + } +} + +void GamepadManager::MaybeConvertToNonstandardGamepadEvent( + const GamepadChangeEvent& aEvent, nsGlobalWindowInner* aWindow) { + MOZ_ASSERT(aWindow); + + if (!mNonstandardEventsEnabled) { + return; + } + + GamepadHandle handle = aEvent.handle(); + + RefPtr<Gamepad> gamepad = aWindow->GetGamepad(handle); + const GamepadChangeEventBody& body = aEvent.body(); + + if (gamepad) { + switch (body.type()) { + case GamepadChangeEventBody::TGamepadButtonInformation: { + const GamepadButtonInformation& a = body.get_GamepadButtonInformation(); + FireButtonEvent(aWindow, gamepad, a.button(), a.value()); + break; + } + case GamepadChangeEventBody::TGamepadAxisInformation: { + const GamepadAxisInformation& a = body.get_GamepadAxisInformation(); + FireAxisMoveEvent(aWindow, gamepad, a.axis(), a.value()); + break; + } + default: + break; + } + } +} + +bool GamepadManager::SetGamepadByEvent(const GamepadChangeEvent& aEvent, + nsGlobalWindowInner* aWindow) { + bool ret = false; + bool firstTime = false; + + GamepadHandle handle = aEvent.handle(); + + if (aWindow) { + if (!AxisMoveIsFirstIntent(aWindow, handle, aEvent)) { + return false; + } + firstTime = !MaybeWindowHasSeenGamepad(aWindow, handle); + } + + RefPtr<Gamepad> gamepad = + aWindow ? aWindow->GetGamepad(handle) : GetGamepad(handle); + const GamepadChangeEventBody& body = aEvent.body(); + + if (gamepad) { + switch (body.type()) { + case GamepadChangeEventBody::TGamepadButtonInformation: { + const GamepadButtonInformation& a = body.get_GamepadButtonInformation(); + gamepad->SetButton(a.button(), a.pressed(), a.touched(), a.value()); + break; + } + case GamepadChangeEventBody::TGamepadAxisInformation: { + const GamepadAxisInformation& a = body.get_GamepadAxisInformation(); + gamepad->SetAxis(a.axis(), a.value()); + break; + } + case GamepadChangeEventBody::TGamepadPoseInformation: { + const GamepadPoseInformation& a = body.get_GamepadPoseInformation(); + gamepad->SetPose(a.pose_state()); + break; + } + case GamepadChangeEventBody::TGamepadLightIndicatorTypeInformation: { + const GamepadLightIndicatorTypeInformation& a = + body.get_GamepadLightIndicatorTypeInformation(); + gamepad->SetLightIndicatorType(a.light(), a.type()); + break; + } + case GamepadChangeEventBody::TGamepadTouchInformation: { + // Avoid GamepadTouch's touchId be accessed in cross-origin tracking. + for (uint32_t i = 0; i < mListeners.Length(); i++) { + RefPtr<Gamepad> listenerGamepad = mListeners[i]->GetGamepad(handle); + if (listenerGamepad && mListeners[i]->IsCurrentInnerWindow() && + !mListeners[i]->GetOuterWindow()->IsBackground()) { + const GamepadTouchInformation& a = + body.get_GamepadTouchInformation(); + listenerGamepad->SetTouchEvent(a.index(), a.touch_state()); + } + } + break; + } + case GamepadChangeEventBody::TGamepadHandInformation: { + const GamepadHandInformation& a = body.get_GamepadHandInformation(); + gamepad->SetHand(a.hand()); + break; + } + default: + MOZ_ASSERT(false); + break; + } + ret = true; + } + + if (aWindow && firstTime) { + FireConnectionEvent(aWindow, gamepad, true); + } + + return ret; +} + +already_AddRefed<Promise> GamepadManager::VibrateHaptic( + GamepadHandle aHandle, uint32_t aHapticIndex, double aIntensity, + double aDuration, nsIGlobalObject* aGlobal, ErrorResult& aRv) { + RefPtr<Promise> promise = Promise::Create(aGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + if (StaticPrefs::dom_gamepad_haptic_feedback_enabled()) { + if (aHandle.GetKind() == GamepadHandleKind::VR) { + if (gfx::VRManagerChild::IsCreated()) { + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->AddPromise(mPromiseID, promise); + vm->SendVibrateHaptic(aHandle, aHapticIndex, aIntensity, aDuration, + mPromiseID); + } + } else { + if (mChannelChild) { + mChannelChild->AddPromise(mPromiseID, promise); + mChannelChild->SendVibrateHaptic(aHandle, aHapticIndex, aIntensity, + aDuration, mPromiseID); + } + } + } + + ++mPromiseID; + return promise.forget(); +} + +void GamepadManager::StopHaptics() { + if (!StaticPrefs::dom_gamepad_haptic_feedback_enabled()) { + return; + } + + for (const auto& entry : mGamepads) { + const GamepadHandle handle = entry.GetWeak()->GetHandle(); + if (handle.GetKind() == GamepadHandleKind::VR) { + if (gfx::VRManagerChild::IsCreated()) { + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->SendStopVibrateHaptic(handle); + } + } else { + if (mChannelChild) { + mChannelChild->SendStopVibrateHaptic(handle); + } + } + } +} + +already_AddRefed<Promise> GamepadManager::SetLightIndicatorColor( + GamepadHandle aHandle, uint32_t aLightColorIndex, uint8_t aRed, + uint8_t aGreen, uint8_t aBlue, nsIGlobalObject* aGlobal, ErrorResult& aRv) { + RefPtr<Promise> promise = Promise::Create(aGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + if (StaticPrefs::dom_gamepad_extensions_lightindicator()) { + MOZ_RELEASE_ASSERT(aHandle.GetKind() != GamepadHandleKind::VR, + "We don't support light indicator in VR."); + + if (mChannelChild) { + mChannelChild->AddPromise(mPromiseID, promise); + mChannelChild->SendLightIndicatorColor(aHandle, aLightColorIndex, aRed, + aGreen, aBlue, mPromiseID); + } + } + + ++mPromiseID; + return promise.forget(); +} +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadManager.h b/dom/gamepad/GamepadManager.h new file mode 100644 index 0000000000..794c0d1ca4 --- /dev/null +++ b/dom/gamepad/GamepadManager.h @@ -0,0 +1,156 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GamepadManager_h_ +#define mozilla_dom_GamepadManager_h_ + +#include "nsIObserver.h" +#include "nsRefPtrHashtable.h" +// Needed for GamepadMappingType +#include "mozilla/dom/GamepadBinding.h" +#include "mozilla/dom/GamepadHandle.h" +#include <utility> + +class nsGlobalWindowInner; +class nsIGlobalObject; + +namespace mozilla { +namespace gfx { +class VRManagerChild; +} // namespace gfx +namespace dom { + +class EventTarget; +class Gamepad; +class GamepadChangeEvent; +class GamepadEventChannelChild; + +class GamepadManager final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + // Returns true if we actually have a service up and running + static bool IsServiceRunning(); + // Get the singleton service + static already_AddRefed<GamepadManager> GetService(); + + void BeginShutdown(); + void StopMonitoring(); + + // Indicate that |aWindow| wants to receive gamepad events. + void AddListener(nsGlobalWindowInner* aWindow); + // Indicate that |aWindow| should no longer receive gamepad events. + void RemoveListener(nsGlobalWindowInner* aWindow); + + // Add a gamepad to the list of known gamepads. + void AddGamepad(GamepadHandle aHandle, const nsAString& aID, + GamepadMappingType aMapping, GamepadHand aHand, + uint32_t aDisplayID, uint32_t aNumButtons, uint32_t aNumAxes, + uint32_t aNumHaptics, uint32_t aNumLightIndicator, + uint32_t aNumTouchEvents); + + // Remove the gamepad at |aIndex| from the list of known gamepads. + void RemoveGamepad(GamepadHandle aHandle); + + // Synchronize the state of |aGamepad| to match the gamepad stored at |aIndex| + void SyncGamepadState(GamepadHandle aHandle, nsGlobalWindowInner* aWindow, + Gamepad* aGamepad); + + // Returns gamepad object if index exists, null otherwise + already_AddRefed<Gamepad> GetGamepad(GamepadHandle aHandle) const; + + // Receive GamepadChangeEvent messages from parent process to fire DOM events + void Update(const GamepadChangeEvent& aGamepadEvent); + + // Trigger vibrate haptic event to gamepad channels. + already_AddRefed<Promise> VibrateHaptic(GamepadHandle aHandle, + uint32_t aHapticIndex, + double aIntensity, double aDuration, + nsIGlobalObject* aGlobal, + ErrorResult& aRv); + // Send stop haptic events to gamepad channels. + void StopHaptics(); + + // Set light indicator color event to gamepad channels. + already_AddRefed<Promise> SetLightIndicatorColor(GamepadHandle aHandle, + uint32_t aLightColorIndex, + uint8_t aRed, uint8_t aGreen, + uint8_t aBlue, + nsIGlobalObject* aGlobal, + ErrorResult& aRv); + + protected: + GamepadManager(); + ~GamepadManager() = default; + + // Fire a gamepadconnected or gamepaddisconnected event for the gamepad + // at |aIndex| to all windows that are listening and have received + // gamepad input. + void NewConnectionEvent(GamepadHandle aHandle, bool aConnected); + + // Fire a gamepadaxismove event to the window at |aTarget| for |aGamepad|. + void FireAxisMoveEvent(EventTarget* aTarget, Gamepad* aGamepad, uint32_t axis, + double value); + + // Fire one of gamepadbutton{up,down} event at the window at |aTarget| for + // |aGamepad|. + void FireButtonEvent(EventTarget* aTarget, Gamepad* aGamepad, + uint32_t aButton, double aValue); + + // Fire one of gamepad{connected,disconnected} event at the window at + // |aTarget| for |aGamepad|. + void FireConnectionEvent(EventTarget* aTarget, Gamepad* aGamepad, + bool aConnected); + + // true if this feature is enabled in preferences + bool mEnabled; + // true if non-standard events are enabled in preferences + bool mNonstandardEventsEnabled; + // true when shutdown has begun + bool mShuttingDown; + + RefPtr<GamepadEventChannelChild> mChannelChild; + + private: + nsresult Init(); + + void MaybeConvertToNonstandardGamepadEvent(const GamepadChangeEvent& aEvent, + nsGlobalWindowInner* aWindow); + + bool SetGamepadByEvent(const GamepadChangeEvent& aEvent, + nsGlobalWindowInner* aWindow = nullptr); + // To avoid unintentionally causing the gamepad be activated. + // Returns false if this gamepad hasn't been seen by this window + // and the axis move data is less than AXIS_FIRST_INTENT_THRESHOLD_VALUE. + bool AxisMoveIsFirstIntent(nsGlobalWindowInner* aWindow, + GamepadHandle aHandle, + const GamepadChangeEvent& aEvent); + bool MaybeWindowHasSeenGamepad(nsGlobalWindowInner* aWindow, + GamepadHandle aHandle); + // Returns true if we have already sent data from this gamepad + // to this window. This should only return true if the user + // explicitly interacted with a gamepad while this window + // was focused, by pressing buttons or similar actions. + bool WindowHasSeenGamepad(nsGlobalWindowInner* aWindow, + GamepadHandle aHandle) const; + // Indicate that a window has received data from a gamepad. + void SetWindowHasSeenGamepad(nsGlobalWindowInner* aWindow, + GamepadHandle aHandle, bool aHasSeen = true); + + // Gamepads connected to the system. Copies of these are handed out + // to each window. + nsRefPtrHashtable<nsGenericHashKey<GamepadHandle>, Gamepad> mGamepads; + // Inner windows that are listening for gamepad events. + // has been sent to that window. + nsTArray<RefPtr<nsGlobalWindowInner>> mListeners; + uint32_t mPromiseID; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_GamepadManager_h_ diff --git a/dom/gamepad/GamepadMonitoring.h b/dom/gamepad/GamepadMonitoring.h new file mode 100644 index 0000000000..67b4737470 --- /dev/null +++ b/dom/gamepad/GamepadMonitoring.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GamepadMonitoring_h_ +#define mozilla_dom_GamepadMonitoring_h_ +#include "mozilla/dom/GamepadHandle.h" + +namespace mozilla::dom { + +// These two functions are implemented in the platform specific service files +// (linux/LinuxGamepad.cpp, cocoa/CocoaGamepad.cpp, etc) +void StartGamepadMonitoring(); +void StopGamepadMonitoring(); +void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, + const uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue); + +} // namespace mozilla::dom + +#endif diff --git a/dom/gamepad/GamepadPlatformService.cpp b/dom/gamepad/GamepadPlatformService.cpp new file mode 100644 index 0000000000..7d730e3d0a --- /dev/null +++ b/dom/gamepad/GamepadPlatformService.cpp @@ -0,0 +1,336 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/GamepadPlatformService.h" + +#include "mozilla/dom/GamepadEventChannelParent.h" +#include "mozilla/dom/GamepadMonitoring.h" +#include "mozilla/dom/GamepadTestChannelParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/Mutex.h" +#include "mozilla/Unused.h" + +#include "nsCOMPtr.h" +#include "nsHashKeys.h" + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +namespace { + +// This is the singleton instance of GamepadPlatformService, can be called +// by both background and monitor thread. +StaticRefPtr<GamepadPlatformService> gGamepadPlatformServiceSingleton; + +} // namespace + +// static +GamepadMonitoringState& GamepadMonitoringState::GetSingleton() { + static GamepadMonitoringState sInstance{}; + return sInstance; +} + +void GamepadMonitoringState::AddObserver(GamepadTestChannelParent* aParent) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + MOZ_ALWAYS_TRUE(mObservers.append(aParent)); +} + +void GamepadMonitoringState::RemoveObserver(GamepadTestChannelParent* aParent) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + + WeakPtr<GamepadTestChannelParent>* observer = nullptr; + + for (auto& item : mObservers) { + if (item == aParent) { + observer = &item; + } + } + + MOZ_ASSERT( + observer, + "Attempted to remove a GamepadTestChannelParent that was never added"); + + std::swap(*observer, mObservers.back()); + mObservers.popBack(); +} + +bool GamepadMonitoringState::IsMonitoring() const { + AssertIsOnBackgroundThread(); + return mIsMonitoring; +} + +void GamepadMonitoringState::Set(bool aIsMonitoring) { + AssertIsOnBackgroundThread(); + + if (mIsMonitoring != aIsMonitoring) { + mIsMonitoring = aIsMonitoring; + for (auto& observer : mObservers) { + // Since each GamepadTestChannelParent removes itself in its dtor, this + // should never be nullptr + MOZ_RELEASE_ASSERT(observer); + observer->OnMonitoringStateChanged(aIsMonitoring); + } + } +} + +GamepadPlatformService::GamepadPlatformService() + : mNextGamepadHandleValue(1), + mMutex("mozilla::dom::GamepadPlatformService") {} + +GamepadPlatformService::~GamepadPlatformService() { Cleanup(); } + +// static +already_AddRefed<GamepadPlatformService> +GamepadPlatformService::GetParentService() { + // GamepadPlatformService can only be accessed in parent process + MOZ_ASSERT(XRE_IsParentProcess()); + if (!gGamepadPlatformServiceSingleton) { + // Only Background Thread can create new GamepadPlatformService instance. + if (IsOnBackgroundThread()) { + gGamepadPlatformServiceSingleton = new GamepadPlatformService(); + } else { + return nullptr; + } + } + RefPtr<GamepadPlatformService> service(gGamepadPlatformServiceSingleton); + return service.forget(); +} + +template <class T> +void GamepadPlatformService::NotifyGamepadChange(GamepadHandle aHandle, + const T& aInfo) { + // This method is called by monitor populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + + GamepadChangeEventBody body(aInfo); + GamepadChangeEvent e(aHandle, body); + + // mChannelParents may be accessed by background thread in the + // same time, we use mutex to prevent possible race condtion + MutexAutoLock autoLock(mMutex); + + for (uint32_t i = 0; i < mChannelParents.Length(); ++i) { + mChannelParents[i]->DispatchUpdateEvent(e); + } +} + +GamepadHandle GamepadPlatformService::AddGamepad( + const char* aID, GamepadMappingType aMapping, GamepadHand aHand, + uint32_t aNumButtons, uint32_t aNumAxes, uint32_t aHaptics, + uint32_t aNumLightIndicator, uint32_t aNumTouchEvents) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + + 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); + + mGamepadAdded.emplace(gamepadHandle, a); + NotifyGamepadChange<GamepadAdded>(gamepadHandle, a); + return gamepadHandle; +} + +void GamepadPlatformService::RemoveGamepad(GamepadHandle aHandle) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + GamepadRemoved a; + NotifyGamepadChange<GamepadRemoved>(aHandle, a); + mGamepadAdded.erase(aHandle); +} + +void GamepadPlatformService::NewButtonEvent(GamepadHandle aHandle, + uint32_t aButton, bool aPressed, + bool aTouched, double aValue) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + GamepadButtonInformation a(aButton, aValue, aPressed, aTouched); + NotifyGamepadChange<GamepadButtonInformation>(aHandle, a); +} + +void GamepadPlatformService::NewButtonEvent(GamepadHandle aHandle, + uint32_t aButton, bool aPressed, + double aValue) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + // When only a digital button is available the value will be synthesized. + NewButtonEvent(aHandle, aButton, aPressed, aPressed, aValue); +} + +void GamepadPlatformService::NewButtonEvent(GamepadHandle aHandle, + uint32_t aButton, bool aPressed, + bool aTouched) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + // When only a digital button is available the value will be synthesized. + NewButtonEvent(aHandle, aButton, aPressed, aTouched, aPressed ? 1.0L : 0.0L); +} + +void GamepadPlatformService::NewButtonEvent(GamepadHandle aHandle, + uint32_t aButton, bool aPressed) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + // When only a digital button is available the value will be synthesized. + NewButtonEvent(aHandle, aButton, aPressed, aPressed, aPressed ? 1.0L : 0.0L); +} + +void GamepadPlatformService::NewAxisMoveEvent(GamepadHandle aHandle, + uint32_t aAxis, double aValue) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + GamepadAxisInformation a(aAxis, aValue); + NotifyGamepadChange<GamepadAxisInformation>(aHandle, a); +} + +void GamepadPlatformService::NewLightIndicatorTypeEvent( + GamepadHandle aHandle, uint32_t aLight, GamepadLightIndicatorType aType) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + GamepadLightIndicatorTypeInformation a(aLight, aType); + NotifyGamepadChange<GamepadLightIndicatorTypeInformation>(aHandle, a); +} + +void GamepadPlatformService::NewPoseEvent(GamepadHandle aHandle, + const GamepadPoseState& aState) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + GamepadPoseInformation a(aState); + NotifyGamepadChange<GamepadPoseInformation>(aHandle, a); +} + +void GamepadPlatformService::NewMultiTouchEvent( + GamepadHandle aHandle, uint32_t aTouchArrayIndex, + const GamepadTouchState& aState) { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + + GamepadTouchInformation a(aTouchArrayIndex, aState); + NotifyGamepadChange<GamepadTouchInformation>(aHandle, a); +} + +void GamepadPlatformService::ResetGamepadIndexes() { + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + mNextGamepadHandleValue = 1; +} + +void GamepadPlatformService::AddChannelParent( + GamepadEventChannelParent* aParent) { + // mChannelParents can only be modified once GamepadEventChannelParent + // is created or removed in Background thread + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + MOZ_ASSERT(!mChannelParents.Contains(aParent)); + + // We use mutex here to prevent race condition with monitor thread + { + MutexAutoLock autoLock(mMutex); + mChannelParents.AppendElement(aParent); + + // 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); + } + } + } + + StartGamepadMonitoring(); + + GamepadMonitoringState::GetSingleton().Set(true); +} + +void GamepadPlatformService::RemoveChannelParent( + GamepadEventChannelParent* aParent) { + // mChannelParents can only be modified once GamepadEventChannelParent + // is created or removed in Background thread + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + MOZ_ASSERT(mChannelParents.Contains(aParent)); + + // We use mutex here to prevent race condition with monitor thread + { + MutexAutoLock autoLock(mMutex); + mChannelParents.RemoveElement(aParent); + if (!mChannelParents.IsEmpty()) { + return; + } + } + + GamepadMonitoringState::GetSingleton().Set(false); + + StopGamepadMonitoring(); + ResetGamepadIndexes(); + MaybeShutdown(); +} + +void GamepadPlatformService::MaybeShutdown() { + // This method is invoked in MaybeStopGamepadMonitoring when + // an IPDL channel is going to be destroyed + AssertIsOnBackgroundThread(); + + // We have to release gGamepadPlatformServiceSingleton ouside + // the mutex as well as making upcoming GetParentService() call + // recreate new singleton, so we use this RefPtr to temporarily + // hold the reference, postponing the release process until this + // method ends. + RefPtr<GamepadPlatformService> kungFuDeathGrip; + + bool isChannelParentEmpty; + { + MutexAutoLock autoLock(mMutex); + isChannelParentEmpty = mChannelParents.IsEmpty(); + if (isChannelParentEmpty) { + kungFuDeathGrip = gGamepadPlatformServiceSingleton; + gGamepadPlatformServiceSingleton = nullptr; + mGamepadAdded.clear(); + } + } +} + +void GamepadPlatformService::Cleanup() { + // This method is called when GamepadPlatformService is + // successfully distructed in background thread + AssertIsOnBackgroundThread(); + + MutexAutoLock autoLock(mMutex); + mChannelParents.Clear(); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadPlatformService.h b/dom/gamepad/GamepadPlatformService.h new file mode 100644 index 0000000000..7353f1f449 --- /dev/null +++ b/dom/gamepad/GamepadPlatformService.h @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GamepadPlatformService_h_ +#define mozilla_dom_GamepadPlatformService_h_ + +#include "mozilla/dom/GamepadBinding.h" +#include "mozilla/dom/GamepadHandle.h" + +#include <map> +#include "mozilla/Mutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Vector.h" +#include "mozilla/WeakPtr.h" + +namespace mozilla::dom { + +class GamepadAdded; +class GamepadEventChannelParent; +enum class GamepadLightIndicatorType : uint8_t; +struct GamepadPoseState; +class GamepadTestChannelParent; +struct GamepadTouchState; +class GamepadPlatformService; + +class GamepadMonitoringState { + public: + static GamepadMonitoringState& GetSingleton(); + + void AddObserver(GamepadTestChannelParent* aParent); + void RemoveObserver(GamepadTestChannelParent* aParent); + + bool IsMonitoring() const; + + GamepadMonitoringState(const GamepadMonitoringState&) = delete; + GamepadMonitoringState(GamepadMonitoringState&&) = delete; + GamepadMonitoringState& operator=(const GamepadMonitoringState) = delete; + GamepadMonitoringState& operator=(GamepadMonitoringState&&) = delete; + + private: + GamepadMonitoringState() = default; + ~GamepadMonitoringState() = default; + + void Set(bool aIsMonitoring); + + bool mIsMonitoring{false}; + Vector<WeakPtr<GamepadTestChannelParent>> mObservers; + + friend class mozilla::dom::GamepadPlatformService; +}; + +// Platform Service for building and transmitting IPDL messages +// through the HAL sandbox. Used by platform specific +// Gamepad implementations +// +// This class can be accessed by the following 2 threads : +// 1. Background thread: +// This thread takes charge of IPDL communications +// between here and DOM side +// +// 2. Monitor Thread: +// This thread is populated in platform-dependent backends, which +// is in charge of processing gamepad hardware events from OS +class GamepadPlatformService final { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadPlatformService) + public: + // Get the singleton service + static already_AddRefed<GamepadPlatformService> GetParentService(); + + // Add a gamepad to the list of known gamepads, and return its handle. + GamepadHandle AddGamepad(const char* aID, GamepadMappingType aMapping, + GamepadHand aHand, uint32_t aNumButtons, + uint32_t aNumAxes, uint32_t aNumHaptics, + uint32_t aNumLightIndicator, + uint32_t aNumTouchEvents); + // Remove the gamepad at |aHandle| from the list of known gamepads. + void RemoveGamepad(GamepadHandle aHandle); + + // Update the state of |aButton| for the gamepad at |aHandle| for all + // windows that are listening and visible, and fire one of + // a gamepadbutton{up,down} event at them as well. + // aPressed is used for digital buttons, aTouched is for detecting touched + // events, aValue is for analog buttons. + void NewButtonEvent(GamepadHandle aHandle, uint32_t aButton, bool aPressed, + bool aTouched, double aValue); + // When only a digital button is available the value will be synthesized. + void NewButtonEvent(GamepadHandle aHandle, uint32_t aButton, bool aPressed); + // When only a digital button are available the value will be synthesized. + void NewButtonEvent(GamepadHandle aHandle, uint32_t aButton, bool aPressed, + bool aTouched); + // When only a digital button are available the value will be synthesized. + void NewButtonEvent(GamepadHandle aHandle, uint32_t aButton, bool aPressed, + double aValue); + // Update the state of |aAxis| for the gamepad at |aHandle| for all + // windows that are listening and visible, and fire a gamepadaxismove + // event at them as well. + void NewAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, double aValue); + // Update the state of |aState| for the gamepad at |aHandle| for all + // windows that are listening and visible. + void NewPoseEvent(GamepadHandle aHandle, const GamepadPoseState& aState); + // Update the type of |aType| for the gamepad at |aHandle| for all + // windows that are listening and visible. + void NewLightIndicatorTypeEvent(GamepadHandle aHandle, uint32_t aLight, + GamepadLightIndicatorType aType); + // Update the state of |aState| for the gamepad at |aHandle| with + // |aTouchArrayIndex| for all windows that are listening and visible. + void NewMultiTouchEvent(GamepadHandle aHandle, uint32_t aTouchArrayIndex, + const GamepadTouchState& aState); + + // When shutting down the platform communications for gamepad, also reset the + // indexes. + void ResetGamepadIndexes(); + + // Add IPDL parent instance + void AddChannelParent(GamepadEventChannelParent* aParent); + + // Remove IPDL parent instance + void RemoveChannelParent(GamepadEventChannelParent* aParent); + + void MaybeShutdown(); + + private: + GamepadPlatformService(); + ~GamepadPlatformService(); + template <class T> + void NotifyGamepadChange(GamepadHandle aHandle, const T& aInfo); + + void Cleanup(); + + // mNextGamepadHandleValue can only be accessed by monitor thread + uint32_t mNextGamepadHandleValue; + + // mChannelParents stores all the GamepadEventChannelParent instances + // which may be accessed by both background thread and monitor thread + // simultaneously, so we have a mutex to prevent race condition + nsTArray<RefPtr<GamepadEventChannelParent>> mChannelParents; + + // This mutex protects mChannelParents from race condition + // between background and monitor thread + Mutex mMutex MOZ_UNANNOTATED; + + std::map<GamepadHandle, GamepadAdded> mGamepadAdded; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/gamepad/GamepadPose.cpp b/dom/gamepad/GamepadPose.cpp new file mode 100644 index 0000000000..e2a5d09efc --- /dev/null +++ b/dom/gamepad/GamepadPose.cpp @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWrapperCache.h" + +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/GamepadPoseBinding.h" +#include "mozilla/dom/GamepadPose.h" + +namespace mozilla::dom { + +GamepadPose::GamepadPose(nsISupports* aParent, const GamepadPoseState& aState) + : Pose(aParent), mPoseState(aState) { + mozilla::HoldJSObjects(this); +} + +GamepadPose::GamepadPose(nsISupports* aParent) : Pose(aParent) { + mozilla::HoldJSObjects(this); + mPoseState.Clear(); +} + +GamepadPose::~GamepadPose() { mozilla::DropJSObjects(this); } + +/* virtual */ +JSObject* GamepadPose::WrapObject(JSContext* aJSContext, + JS::Handle<JSObject*> aGivenProto) { + return GamepadPose_Binding::Wrap(aJSContext, this, aGivenProto); +} + +bool GamepadPose::HasOrientation() const { + return bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation); +} + +bool GamepadPose::HasPosition() const { + return bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position) || + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_PositionEmulated); +} + +void GamepadPose::GetPosition(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + mPoseState.isPositionValid && + (bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position) || + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_PositionEmulated)); + SetFloat32Array(aJSContext, this, aRetval, mPosition, + valid ? mPoseState.position : nullptr, 3, aRv); +} + +void GamepadPose::GetLinearVelocity(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + mPoseState.isPositionValid && + (bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position) || + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_PositionEmulated)); + SetFloat32Array(aJSContext, this, aRetval, mLinearVelocity, + valid ? mPoseState.linearVelocity : nullptr, 3, aRv); +} + +void GamepadPose::GetLinearAcceleration(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + mPoseState.isPositionValid && + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_LinearAcceleration); + SetFloat32Array(aJSContext, this, aRetval, mLinearAcceleration, + valid ? mPoseState.linearAcceleration : nullptr, 3, aRv); +} + +void GamepadPose::GetOrientation(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + mPoseState.isOrientationValid && + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation); + SetFloat32Array(aJSContext, this, aRetval, mOrientation, + valid ? mPoseState.orientation : nullptr, 4, aRv); +} + +void GamepadPose::GetAngularVelocity(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + mPoseState.isOrientationValid && + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation); + SetFloat32Array(aJSContext, this, aRetval, mAngularVelocity, + valid ? mPoseState.angularVelocity : nullptr, 3, aRv); +} + +void GamepadPose::GetAngularAcceleration(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + mPoseState.isOrientationValid && + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_AngularAcceleration); + SetFloat32Array(aJSContext, this, aRetval, mAngularAcceleration, + valid ? mPoseState.angularAcceleration : nullptr, 3, aRv); +} + +void GamepadPose::SetPoseState(const GamepadPoseState& aPose) { + mPoseState = aPose; +} + +const GamepadPoseState& GamepadPose::GetPoseState() { return mPoseState; } + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadPose.h b/dom/gamepad/GamepadPose.h new file mode 100644 index 0000000000..2dabfa1117 --- /dev/null +++ b/dom/gamepad/GamepadPose.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_GamepadPose_h +#define mozilla_dom_gamepad_GamepadPose_h + +#include "mozilla/TypedEnumBits.h" +#include "mozilla/dom/Pose.h" +#include "mozilla/dom/GamepadPoseState.h" +#include "nsCOMPtr.h" + +namespace mozilla::dom { + +class GamepadPose final : public Pose { + public: + GamepadPose(nsISupports* aParent, const GamepadPoseState& aState); + explicit GamepadPose(nsISupports* aParent); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + bool HasOrientation() const; + bool HasPosition() const; + virtual void GetPosition(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetLinearVelocity(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetLinearAcceleration(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetOrientation(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetAngularVelocity(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetAngularAcceleration(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + void SetPoseState(const GamepadPoseState& aPose); + const GamepadPoseState& GetPoseState(); + + private: + virtual ~GamepadPose(); + + nsCOMPtr<nsISupports> mParent; + GamepadPoseState mPoseState; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_GamepadPose_h diff --git a/dom/gamepad/GamepadPoseState.h b/dom/gamepad/GamepadPoseState.h new file mode 100644 index 0000000000..2272b84c76 --- /dev/null +++ b/dom/gamepad/GamepadPoseState.h @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_GamepadPoseState_h_ +#define mozilla_dom_gamepad_GamepadPoseState_h_ + +namespace mozilla::dom { + +enum class GamepadCapabilityFlags : uint16_t { + Cap_None = 0, + /** + * Cap_Position is set if the Gamepad is capable of tracking its position. + */ + Cap_Position = 1 << 1, + /** + * Cap_Orientation is set if the Gamepad is capable of tracking its + * orientation. + */ + Cap_Orientation = 1 << 2, + /** + * Cap_AngularAcceleration is set if the Gamepad is capable of tracking its + * angular acceleration. + */ + Cap_AngularAcceleration = 1 << 3, + /** + * Cap_LinearAcceleration is set if the Gamepad is capable of tracking its + * linear acceleration. + */ + Cap_LinearAcceleration = 1 << 4, + /** + * Cap_GripSpacePosition is set if the Gamepad has a grip space position. + */ + Cap_GripSpacePosition = 1 << 5, + /** + * Cap_PositionEmulated is set if the VRDisplay is capable of setting a + * emulated position (e.g. neck model) even if still doesn't support 6DOF + * tracking. + */ + Cap_PositionEmulated = 1 << 6, + /** + * Cap_All used for validity checking during IPC serialization + */ + Cap_All = (1 << 7) - 1 +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(GamepadCapabilityFlags) + +struct GamepadPoseState { + GamepadCapabilityFlags flags; + float orientation[4]; + float position[3]; + float angularVelocity[3]; + float angularAcceleration[3]; + float linearVelocity[3]; + float linearAcceleration[3]; + bool isPositionValid; + bool isOrientationValid; + + GamepadPoseState() + : flags(GamepadCapabilityFlags::Cap_None), + orientation{0, 0, 0, 0}, + position{0, 0, 0}, + angularVelocity{0, 0, 0}, + angularAcceleration{0, 0, 0}, + linearVelocity{0, 0, 0}, + linearAcceleration{0, 0, 0}, + isPositionValid(false), + isOrientationValid(false) {} + + bool operator==(const GamepadPoseState& aPose) const { + return flags == aPose.flags && orientation[0] == aPose.orientation[0] && + orientation[1] == aPose.orientation[1] && + orientation[2] == aPose.orientation[2] && + orientation[3] == aPose.orientation[3] && + position[0] == aPose.position[0] && + position[1] == aPose.position[1] && + position[2] == aPose.position[2] && + angularVelocity[0] == aPose.angularVelocity[0] && + angularVelocity[1] == aPose.angularVelocity[1] && + angularVelocity[2] == aPose.angularVelocity[2] && + angularAcceleration[0] == aPose.angularAcceleration[0] && + angularAcceleration[1] == aPose.angularAcceleration[1] && + angularAcceleration[2] == aPose.angularAcceleration[2] && + linearVelocity[0] == aPose.linearVelocity[0] && + linearVelocity[1] == aPose.linearVelocity[1] && + linearVelocity[2] == aPose.linearVelocity[2] && + linearAcceleration[0] == aPose.linearAcceleration[0] && + linearAcceleration[1] == aPose.linearAcceleration[1] && + linearAcceleration[2] == aPose.linearAcceleration[2] && + isPositionValid == aPose.isPositionValid && + isOrientationValid == aPose.isOrientationValid; + } + + bool operator!=(const GamepadPoseState& aPose) const { + return !(*this == aPose); + } + + void Clear() { + memset(&flags, 0, + reinterpret_cast<char*>(&isOrientationValid) + + sizeof(isOrientationValid) - reinterpret_cast<char*>(&flags)); + } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_GamepadPoseState_h_ diff --git a/dom/gamepad/GamepadRemapping.cpp b/dom/gamepad/GamepadRemapping.cpp new file mode 100644 index 0000000000..2c3ec252f5 --- /dev/null +++ b/dom/gamepad/GamepadRemapping.cpp @@ -0,0 +1,2178 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Based on +// https://cs.chromium.org/chromium/src/device/gamepad/gamepad_standard_mappings.h + +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "mozilla/dom/GamepadRemapping.h" +#include "mozilla/dom/GamepadPlatformService.h" + +#include <vector> +#include <unordered_map> + +namespace mozilla::dom { + +const float BUTTON_THRESHOLD_VALUE = 0.1f; + +float NormalizeTouch(long aValue, long aMin, long aMax) { + return (2.f * (aValue - aMin) / static_cast<float>(aMax - aMin)) - 1.f; +} + +double AxisToButtonValue(double aValue) { + // Mapping axis value range from (-1, +1) to (0, +1). + return (aValue + 1.0f) * 0.5f; +} + +void FetchDpadFromAxis(GamepadHandle aHandle, double dir) { + bool up = false; + bool right = false; + bool down = false; + bool left = false; + + // Dpad is mapped as a direction on one axis, where -1 is up and it + // increases clockwise to 1, which is up + left. It's set to a large (> 1.f) + // number when nothing is depressed, except on start up, sometimes it's 0.0 + // for no data, rather than the large number. + if (dir != 0.0f) { + up = (dir >= -1.f && dir < -0.7f) || (dir >= .95f && dir <= 1.f); + right = dir >= -.75f && dir < -.1f; + down = dir >= -.2f && dir < .45f; + left = dir >= .4f && dir <= 1.f; + } + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_UP, up); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_RIGHT, right); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_DOWN, down); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_LEFT, left); +} + +class DefaultRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return numAxes; } + + virtual uint32_t GetButtonCount() const override { return numButtons; } + + virtual void SetAxisCount(uint32_t aAxisCount) override { + numAxes = aAxisCount; + } + + virtual void SetButtonCount(uint32_t aButtonCount) override { + numButtons = aButtonCount; + } + + virtual GamepadMappingType GetMappingType() const override { + return GamepadMappingType::_empty; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + if (GetAxisCount() <= aAxis) { + NS_WARNING( + nsPrintfCString("Axis idx '%d' doesn't support in DefaultRemapper().", + aAxis) + .get()); + return; + } + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + service->NewAxisMoveEvent(aHandle, aAxis, aValue); + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in DefaultRemapper().", aButton) + .get()); + return; + } + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + service->NewButtonEvent(aHandle, aButton, aPressed); + } + + private: + uint32_t numAxes; + uint32_t numButtons; +}; + +class ADT1Remapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString("Axis idx '%d' doesn't support in ADT1Remapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString("Button idx '%d' doesn't support in ADT1Remapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {3, BUTTON_INDEX_TERTIARY}, + {4, BUTTON_INDEX_QUATERNARY}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, + {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {12, BUTTON_INDEX_META}, + {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class TwoAxesEightKeysRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return 0; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT - 1; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_LEFT, + AxisNegativeAsButton(aValue)); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_RIGHT, + AxisPositiveAsButton(aValue)); + break; + case 1: + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_UP, + AxisNegativeAsButton(aValue)); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_DOWN, + AxisPositiveAsButton(aValue)); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in TwoAxesEightKeysRemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in TwoAxesEightKeysRemapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {0, BUTTON_INDEX_QUATERNARY}, + {2, BUTTON_INDEX_PRIMARY}, + {3, BUTTON_INDEX_TERTIARY}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class StadiaControllerRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return STADIA_BUTTON_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in StadiaControllerRemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (STADIA_BUTTON_COUNT <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in StadiaControllerRemapper().", + aButton) + .get()); + return; + } + + service->NewButtonEvent(aHandle, aButton, aPressed); + } + + private: + enum STADIAButtons { + STADIA_BUTTON_EXTRA1 = BUTTON_INDEX_COUNT, + STADIA_BUTTON_EXTRA2, + STADIA_BUTTON_COUNT + }; +}; + +class Playstation3Remapper final : public GamepadRemapper { + public: + Playstation3Remapper() = default; + + uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + uint32_t GetButtonCount() const override { return BUTTON_INDEX_COUNT; } + + void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in Playstation3Remapper().", + aAxis) + .get()); + break; + } + } + + void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + constexpr std::array buttonMapping = {BUTTON_INDEX_BACK_SELECT, + BUTTON_INDEX_LEFT_THUMBSTICK, + BUTTON_INDEX_RIGHT_THUMBSTICK, + BUTTON_INDEX_START, + BUTTON_INDEX_DPAD_UP, + BUTTON_INDEX_DPAD_RIGHT, + BUTTON_INDEX_DPAD_DOWN, + BUTTON_INDEX_DPAD_LEFT, + BUTTON_INDEX_LEFT_TRIGGER, + BUTTON_INDEX_RIGHT_TRIGGER, + BUTTON_INDEX_LEFT_SHOULDER, + BUTTON_INDEX_RIGHT_SHOULDER, + BUTTON_INDEX_QUATERNARY, + BUTTON_INDEX_SECONDARY, + BUTTON_INDEX_PRIMARY, + BUTTON_INDEX_TERTIARY, + BUTTON_INDEX_META}; + + if (buttonMapping.size() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in Playstation3Remapper().", + aButton) + .get()); + return; + } + service->NewButtonEvent(aHandle, buttonMapping[aButton], aPressed); + } +}; + +class Dualshock4Remapper final : public GamepadRemapper { + public: + Dualshock4Remapper() { + mLastTouches.SetLength(TOUCH_EVENT_COUNT); + mLastTouchId.SetLength(TOUCH_EVENT_COUNT); + } + + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return DUALSHOCK_BUTTON_COUNT; + } + + virtual uint32_t GetLightIndicatorCount() const override { + return LIGHT_INDICATOR_COUNT; + } + + virtual void GetLightIndicators( + nsTArray<GamepadLightIndicatorType>& aTypes) const override { + const uint32_t len = GetLightIndicatorCount(); + aTypes.SetLength(len); + for (uint32_t i = 0; i < len; ++i) { + aTypes[i] = GamepadLightIndicatorType::Rgb; + } + } + + virtual uint32_t GetTouchEventCount() const override { + return TOUCH_EVENT_COUNT; + } + + virtual void GetLightColorReport( + uint8_t aRed, uint8_t aGreen, uint8_t aBlue, + std::vector<uint8_t>& aReport) const override { + const size_t report_length = 32; + aReport.resize(report_length); + aReport.assign(report_length, 0); + + aReport[0] = 0x05; // report ID USB only + aReport[1] = 0x02; // LED only + aReport[6] = aRed; + aReport[7] = aGreen; + aReport[8] = aBlue; + } + + virtual uint32_t GetMaxInputReportLength() const override { + return MAX_INPUT_LEN; + } + + virtual void ProcessTouchData(GamepadHandle aHandle, void* aInput) override { + nsTArray<GamepadTouchState> touches(TOUCH_EVENT_COUNT); + touches.SetLength(TOUCH_EVENT_COUNT); + uint8_t* rawData = (uint8_t*)aInput; + + const uint32_t kTouchDimensionX = 1920; + const uint32_t kTouchDimensionY = 942; + bool touch0Pressed = (((rawData[35] & 0xff) >> 7) == 0); + bool touch1Pressed = (((rawData[39] & 0xff) >> 7) == 0); + + if ((touch0Pressed && (rawData[35] & 0xff) < mLastTouchId[0]) || + (touch1Pressed && (rawData[39] & 0xff) < mLastTouchId[1])) { + mTouchIdBase += 128; + } + + if (touch0Pressed) { + touches[0].touchId = mTouchIdBase + (rawData[35] & 0x7f); + touches[0].surfaceId = 0; + touches[0].position[0] = NormalizeTouch( + ((rawData[37] & 0xf) << 8) | rawData[36], 0, (kTouchDimensionX - 1)); + touches[0].position[1] = + NormalizeTouch(rawData[38] << 4 | ((rawData[37] & 0xf0) >> 4), 0, + (kTouchDimensionY - 1)); + touches[0].surfaceDimensions[0] = kTouchDimensionX; + touches[0].surfaceDimensions[1] = kTouchDimensionY; + touches[0].isSurfaceDimensionsValid = true; + mLastTouchId[0] = rawData[35] & 0x7f; + } + if (touch1Pressed) { + touches[1].touchId = mTouchIdBase + (rawData[39] & 0x7f); + touches[1].surfaceId = 0; + touches[1].position[0] = + NormalizeTouch((((rawData[41] & 0xf) << 8) | rawData[40]) + 1, 0, + (kTouchDimensionX - 1)); + touches[1].position[1] = + NormalizeTouch(rawData[42] << 4 | ((rawData[41] & 0xf0) >> 4), 0, + (kTouchDimensionY - 1)); + touches[1].surfaceDimensions[0] = kTouchDimensionX; + touches[1].surfaceDimensions[1] = kTouchDimensionY; + touches[1].isSurfaceDimensionsValid = true; + mLastTouchId[1] = rawData[39] & 0x7f; + } + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + // Avoid to send duplicate untouched events to the gamepad service. + if ((mLastTouches[0] != touch0Pressed) || touch0Pressed) { + service->NewMultiTouchEvent(aHandle, 0, touches[0]); + } + if ((mLastTouches[1] != touch1Pressed) || touch1Pressed) { + service->NewMultiTouchEvent(aHandle, 1, touches[1]); + } + mLastTouches[0] = touch0Pressed; + mLastTouches[1] = touch1Pressed; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in Dualshock4Remapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + constexpr std::array<uint32_t, 14> buttonMapping = { + BUTTON_INDEX_TERTIARY, + BUTTON_INDEX_PRIMARY, + BUTTON_INDEX_SECONDARY, + BUTTON_INDEX_QUATERNARY, + BUTTON_INDEX_LEFT_SHOULDER, + BUTTON_INDEX_RIGHT_SHOULDER, + BUTTON_INDEX_LEFT_TRIGGER, + BUTTON_INDEX_RIGHT_TRIGGER, + BUTTON_INDEX_BACK_SELECT, + BUTTON_INDEX_START, + BUTTON_INDEX_LEFT_THUMBSTICK, + BUTTON_INDEX_RIGHT_THUMBSTICK, + BUTTON_INDEX_META, + DUALSHOCK_BUTTON_TOUCHPAD}; + + if (buttonMapping.size() <= aButton) { + NS_WARNING(nsPrintfCString( + "Button idx '%d' doesn't support in Dualshock4Remapper().", + aButton) + .get()); + return; + } + + service->NewButtonEvent(aHandle, buttonMapping[aButton], aPressed); + } + + private: + enum Dualshock4Buttons { + DUALSHOCK_BUTTON_TOUCHPAD = BUTTON_INDEX_COUNT, + DUALSHOCK_BUTTON_COUNT + }; + + static const uint32_t LIGHT_INDICATOR_COUNT = 1; + static const uint32_t TOUCH_EVENT_COUNT = 2; + static const uint32_t MAX_INPUT_LEN = 68; + + nsTArray<unsigned long> mLastTouchId; + nsTArray<bool> mLastTouches; + unsigned long mTouchIdBase = 0; +}; + +class Xbox360Remapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 3: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 4: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 5: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in Xbox360Remapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in Xbox360Remapper().", aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {6, BUTTON_INDEX_LEFT_THUMBSTICK}, {7, BUTTON_INDEX_RIGHT_THUMBSTICK}, + {8, BUTTON_INDEX_START}, {9, BUTTON_INDEX_BACK_SELECT}, + {10, BUTTON_INDEX_META}, {11, BUTTON_INDEX_DPAD_UP}, + {12, BUTTON_INDEX_DPAD_DOWN}, {13, BUTTON_INDEX_DPAD_LEFT}, + {14, BUTTON_INDEX_DPAD_RIGHT}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class XboxOneSRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 3: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 4: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 5: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in XboxOneSRemapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in XboxOneSRemapper().", aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {6, BUTTON_INDEX_BACK_SELECT}, + {7, BUTTON_INDEX_START}, + {8, BUTTON_INDEX_LEFT_THUMBSTICK}, + {9, BUTTON_INDEX_RIGHT_THUMBSTICK}, + {10, BUTTON_INDEX_META}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class XboxOneS2016FirmwareRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING(nsPrintfCString("Axis idx '%d' doesn't support in " + "XboxOneS2016FirmwareRemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING(nsPrintfCString("Button idx '%d' doesn't support in " + "XboxOneS2016FirmwareRemapper().", + aButton) + .get()); + return; + } + + // kMicrosoftProductXboxOneSWireless2016 controller received a firmware + // update in 2019 that changed which field is populated with the meta button + // state. In order to cover the old and new cases, we have to check both + // fields of {12, 15} buttons. + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {0, BUTTON_INDEX_PRIMARY}, + {1, BUTTON_INDEX_SECONDARY}, + {3, BUTTON_INDEX_TERTIARY}, + {4, BUTTON_INDEX_QUATERNARY}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, + {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {11, BUTTON_INDEX_START}, + {12, BUTTON_INDEX_META}, + {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}, + {15, BUTTON_INDEX_META}, + {16, BUTTON_INDEX_BACK_SELECT}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class XboxOneRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + case 10: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 11: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in XboxOneRemapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in XboxOneRemapper().", aButton) + .get()); + return; + } + + // Accessing {30, 31} buttons looks strange to me + // and without an avilable device to help verify it. + // It is according to `MapperXboxOneBluetooth()` in + // https://cs.chromium.org/chromium/src/device/gamepad/gamepad_standard_mappings_mac.mm + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {0, BUTTON_INDEX_PRIMARY}, + {1, BUTTON_INDEX_SECONDARY}, + {3, BUTTON_INDEX_TERTIARY}, + {4, BUTTON_INDEX_QUATERNARY}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, + {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {11, BUTTON_INDEX_START}, + {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}, + {30, BUTTON_INDEX_META}, + {31, BUTTON_INDEX_BACK_SELECT}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class XboxSeriesXRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + case 148: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 149: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in XboxSeriesXRemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in XboxSeriesXRemapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {0, BUTTON_INDEX_PRIMARY}, + {1, BUTTON_INDEX_SECONDARY}, + {3, BUTTON_INDEX_TERTIARY}, + {4, BUTTON_INDEX_QUATERNARY}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, + {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {10, BUTTON_INDEX_BACK_SELECT}, + {11, BUTTON_INDEX_START}, + {12, BUTTON_INDEX_META}, + {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class LogitechDInputRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + // The Logitech button (BUTTON_INDEX_META) is not accessible through the + // device's D-mode. + return BUTTON_INDEX_COUNT - 1; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in LogitechDInputRemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in LogitechDInputRemapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {0, BUTTON_INDEX_TERTIARY}, + {1, BUTTON_INDEX_PRIMARY}, + {2, BUTTON_INDEX_SECONDARY}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class SwitchJoyConRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return 2; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + if (GetAxisCount() <= aAxis) { + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in SwitchJoyConRemapper().", aAxis) + .get()); + return; + } + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + service->NewButtonEvent(aHandle, aButton, aPressed); + } +}; + +class SwitchProRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + // The Switch Pro controller has a Capture button that has no equivalent in + // the Standard Gamepad. + return SWITCHPRO_BUTTON_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + if (GetAxisCount() <= aAxis) { + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in SwitchProRemapper().", aAxis) + .get()); + return; + } + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + service->NewButtonEvent(aHandle, aButton, aPressed); + } + + private: + enum SwitchProButtons { + SWITCHPRO_BUTTON_EXTRA = BUTTON_INDEX_COUNT, + SWITCHPRO_BUTTON_COUNT + }; +}; + +class NvShieldRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return SHIELD_BUTTON_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in NvShieldRemapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in NvShieldRemapper().", aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {2, BUTTON_INDEX_META}, + {3, BUTTON_INDEX_TERTIARY}, + {4, BUTTON_INDEX_QUATERNARY}, + {5, SHIELD_BUTTON_CIRCLE}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, + {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {9, BUTTON_INDEX_BACK_SELECT}, + {11, BUTTON_INDEX_START}, + {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } + + private: + enum ShieldButtons { + SHIELD_BUTTON_CIRCLE = BUTTON_INDEX_COUNT, + SHIELD_BUTTON_COUNT + }; +}; + +class NvShield2017Remapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return SHIELD2017_BUTTON_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in NvShield2017Remapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in NvShield2017Remapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {2, BUTTON_INDEX_META}, + {3, BUTTON_INDEX_TERTIARY}, + {4, BUTTON_INDEX_QUATERNARY}, + {5, BUTTON_INDEX_START}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, + {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {8, BUTTON_INDEX_BACK_SELECT}, + {11, SHIELD2017_BUTTON_PLAYPAUSE}, + {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } + + private: + enum Shield2017Buttons { + SHIELD2017_BUTTON_PLAYPAUSE = BUTTON_INDEX_COUNT, + SHIELD2017_BUTTON_COUNT + }; +}; + +class IBuffaloRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return 2; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT - 1; /* no meta */ + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_LEFT, + AxisNegativeAsButton(aValue)); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_RIGHT, + AxisPositiveAsButton(aValue)); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_UP, + AxisNegativeAsButton(aValue)); + service->NewButtonEvent(aHandle, BUTTON_INDEX_DPAD_DOWN, + AxisPositiveAsButton(aValue)); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in IBuffaloRemapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in IBuffaloRemapper().", aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {0, BUTTON_INDEX_SECONDARY}, {1, BUTTON_INDEX_PRIMARY}, + {2, BUTTON_INDEX_QUATERNARY}, {3, BUTTON_INDEX_TERTIARY}, + {5, BUTTON_INDEX_RIGHT_TRIGGER}, {6, BUTTON_INDEX_BACK_SELECT}, + {7, BUTTON_INDEX_START}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class XSkillsRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return GAMECUBE_BUTTON_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in XSkillsRemapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in XSkillsRemapper().", aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {0, BUTTON_INDEX_PRIMARY}, // A + {1, BUTTON_INDEX_TERTIARY}, // B + {2, BUTTON_INDEX_SECONDARY}, // X + {3, BUTTON_INDEX_QUATERNARY}, // Y + {4, GAMECUBE_BUTTON_LEFT_TRIGGER_CLICK}, + {5, GAMECUBE_BUTTON_RIGHT_TRIGGER_CLICK}, + {6, BUTTON_INDEX_RIGHT_SHOULDER}, + {7, BUTTON_INDEX_START}, + {8, BUTTON_INDEX_DPAD_LEFT}, + {9, BUTTON_INDEX_DPAD_RIGHT}, + {10, BUTTON_INDEX_DPAD_DOWN}, + {11, BUTTON_INDEX_DPAD_UP}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } + + private: + enum GamecubeButtons { + GAMECUBE_BUTTON_LEFT_TRIGGER_CLICK = BUTTON_INDEX_COUNT, + GAMECUBE_BUTTON_RIGHT_TRIGGER_CLICK, + GAMECUBE_BUTTON_COUNT + }; +}; + +class BoomN64PsxRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT - 1; // no meta + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in BoomN64PsxRemapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + static constexpr std::array buttonMapping = { + BUTTON_INDEX_QUATERNARY, BUTTON_INDEX_SECONDARY, + BUTTON_INDEX_PRIMARY, BUTTON_INDEX_TERTIARY, + BUTTON_INDEX_LEFT_TRIGGER, BUTTON_INDEX_RIGHT_TRIGGER, + BUTTON_INDEX_LEFT_SHOULDER, BUTTON_INDEX_RIGHT_SHOULDER, + BUTTON_INDEX_BACK_SELECT, BUTTON_INDEX_LEFT_THUMBSTICK, + BUTTON_INDEX_RIGHT_THUMBSTICK, BUTTON_INDEX_START, + BUTTON_INDEX_DPAD_UP, BUTTON_INDEX_DPAD_RIGHT, + BUTTON_INDEX_DPAD_DOWN, BUTTON_INDEX_DPAD_LEFT}; + + if (buttonMapping.size() <= aButton) { + NS_WARNING(nsPrintfCString( + "Button idx '%d' doesn't support in BoomN64PsxRemapper().", + aButton) + .get()); + return; + } + + service->NewButtonEvent(aHandle, buttonMapping[aButton], aPressed); + } + + private: + enum GamecubeButtons { + GAMECUBE_BUTTON_LEFT_TRIGGER_CLICK = BUTTON_INDEX_COUNT, + GAMECUBE_BUTTON_RIGHT_TRIGGER_CLICK, + GAMECUBE_BUTTON_COUNT + }; +}; + +class StadiaControllerOldFirmwareRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return ANALOG_GAMEPAD_BUTTON_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in AnalogGamepadRemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in AnalogGamepadRemapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {3, BUTTON_INDEX_TERTIARY}, + {4, BUTTON_INDEX_QUATERNARY}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, + {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {10, BUTTON_INDEX_BACK_SELECT}, + {11, BUTTON_INDEX_META}, + {12, BUTTON_INDEX_START}, + {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}, + {16, ANALOG_GAMEPAD_BUTTON_EXTRA}, + {17, ANALOG_GAMEPAD_BUTTON_EXTRA2}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } + + private: + enum AnalogGamepadButtons { + ANALOG_GAMEPAD_BUTTON_EXTRA = BUTTON_INDEX_COUNT, + ANALOG_GAMEPAD_BUTTON_EXTRA2, + ANALOG_GAMEPAD_BUTTON_COUNT + }; +}; + +class RazerServalRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT - 1; /* no meta */ + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in RazerServalRemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in RazerServalRemapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {3, BUTTON_INDEX_TERTIARY}, {4, BUTTON_INDEX_QUATERNARY}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {10, BUTTON_INDEX_BACK_SELECT}, {11, BUTTON_INDEX_START}, + {12, BUTTON_INDEX_START}, {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class MogaProRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT - 1; /* no meta */ + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 3: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 4: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 5: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in MogaProRemapper().", aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in MogaProRemapper().", aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {3, BUTTON_INDEX_TERTIARY}, {4, BUTTON_INDEX_QUATERNARY}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {11, BUTTON_INDEX_START}, {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class OnLiveWirelessRemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 3: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 4: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 5: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 9: + FetchDpadFromAxis(aHandle, aValue); + break; + default: + NS_WARNING( + nsPrintfCString( + "Axis idx '%d' doesn't support in OnLiveWirelessRemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString( + "Button idx '%d' doesn't support in OnLiveWirelessRemapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {3, BUTTON_INDEX_TERTIARY}, + {4, BUTTON_INDEX_QUATERNARY}, + {6, BUTTON_INDEX_LEFT_SHOULDER}, + {7, BUTTON_INDEX_RIGHT_SHOULDER}, + {12, BUTTON_INDEX_META}, + {13, BUTTON_INDEX_LEFT_THUMBSTICK}, + {14, BUTTON_INDEX_RIGHT_THUMBSTICK}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +class OUYARemapper final : public GamepadRemapper { + public: + virtual uint32_t GetAxisCount() const override { return AXIS_INDEX_COUNT; } + + virtual uint32_t GetButtonCount() const override { + return BUTTON_INDEX_COUNT; + } + + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + switch (aAxis) { + case 0: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_X, aValue); + break; + case 1: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_LEFT_STICK_Y, aValue); + break; + case 2: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_LEFT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + case 3: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_X, aValue); + break; + case 4: + service->NewAxisMoveEvent(aHandle, AXIS_INDEX_RIGHT_STICK_Y, aValue); + break; + case 5: { + const double value = AxisToButtonValue(aValue); + service->NewButtonEvent(aHandle, BUTTON_INDEX_RIGHT_TRIGGER, + value > BUTTON_THRESHOLD_VALUE, value); + break; + } + default: + NS_WARNING( + nsPrintfCString("Axis idx '%d' doesn't support in OUYARemapper().", + aAxis) + .get()); + break; + } + } + + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const override { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (GetButtonCount() <= aButton) { + NS_WARNING( + nsPrintfCString("Button idx '%d' doesn't support in OUYARemapper().", + aButton) + .get()); + return; + } + + const std::unordered_map<uint32_t, uint32_t> buttonMapping = { + {1, BUTTON_INDEX_TERTIARY}, {2, BUTTON_INDEX_QUATERNARY}, + {3, BUTTON_INDEX_SECONDARY}, {6, BUTTON_INDEX_LEFT_THUMBSTICK}, + {7, BUTTON_INDEX_RIGHT_THUMBSTICK}, {8, BUTTON_INDEX_DPAD_UP}, + {9, BUTTON_INDEX_DPAD_DOWN}, {10, BUTTON_INDEX_DPAD_LEFT}, + {11, BUTTON_INDEX_DPAD_RIGHT}, {15, BUTTON_INDEX_META}}; + + auto find = buttonMapping.find(aButton); + if (find != buttonMapping.end()) { + service->NewButtonEvent(aHandle, find->second, aPressed); + } else { + service->NewButtonEvent(aHandle, aButton, aPressed); + } + } +}; + +already_AddRefed<GamepadRemapper> GetGamepadRemapper(const uint16_t aVendorId, + const uint16_t aProductId, + bool& aUsingDefault) { + const std::vector<GamepadRemappingData> remappingRules = { + {GamepadId::kAsusTekProduct4500, new ADT1Remapper()}, + {GamepadId::kDragonRiseProduct0011, new TwoAxesEightKeysRemapper()}, + {GamepadId::kGoogleProduct2c40, new ADT1Remapper()}, + {GamepadId::kGoogleProduct9400, new StadiaControllerRemapper()}, + {GamepadId::kLogitechProductc216, new LogitechDInputRemapper()}, + {GamepadId::kLogitechProductc218, new LogitechDInputRemapper()}, + {GamepadId::kLogitechProductc219, new LogitechDInputRemapper()}, + {GamepadId::kMicrosoftProductXbox360Wireless, new Xbox360Remapper()}, + {GamepadId::kMicrosoftProductXbox360Wireless2, new Xbox360Remapper()}, + {GamepadId::kMicrosoftProductXboxOneElite2Wireless, + new XboxOneRemapper()}, + {GamepadId::kMicrosoftProductXboxOneSWireless, new XboxOneSRemapper()}, + {GamepadId::kMicrosoftProductXboxOneSWireless2016, + new XboxOneS2016FirmwareRemapper()}, + {GamepadId::kMicrosoftProductXboxAdaptiveWireless, new XboxOneRemapper()}, + {GamepadId::kMicrosoftProductXboxSeriesXWireless, + new XboxSeriesXRemapper()}, + {GamepadId::kNintendoProduct2006, new SwitchJoyConRemapper()}, + {GamepadId::kNintendoProduct2007, new SwitchJoyConRemapper()}, + {GamepadId::kNintendoProduct2009, new SwitchProRemapper()}, + {GamepadId::kNintendoProduct200e, new SwitchProRemapper()}, + {GamepadId::kNvidiaProduct7210, new NvShieldRemapper()}, + {GamepadId::kNvidiaProduct7214, new NvShield2017Remapper()}, + {GamepadId::kPadixProduct2060, new IBuffaloRemapper()}, + {GamepadId::kPlayComProduct0005, new XSkillsRemapper()}, + {GamepadId::kPrototypeVendorProduct0667, new BoomN64PsxRemapper()}, + {GamepadId::kPrototypeVendorProduct9401, + new StadiaControllerOldFirmwareRemapper()}, + {GamepadId::kRazer1532Product0900, new RazerServalRemapper()}, + {GamepadId::kSonyProduct0268, new Playstation3Remapper()}, + {GamepadId::kSonyProduct05c4, new Dualshock4Remapper()}, + {GamepadId::kSonyProduct09cc, new Dualshock4Remapper()}, + {GamepadId::kSonyProduct0ba0, new Dualshock4Remapper()}, + {GamepadId::kVendor20d6Product6271, new MogaProRemapper()}, + {GamepadId::kVendor2378Product1008, new OnLiveWirelessRemapper()}, + {GamepadId::kVendor2378Product100a, new OnLiveWirelessRemapper()}, + {GamepadId::kVendor2836Product0001, new OUYARemapper()}}; + const GamepadId id = static_cast<GamepadId>((aVendorId << 16) | aProductId); + + for (uint32_t i = 0; i < remappingRules.size(); ++i) { + if (id == remappingRules[i].id) { + aUsingDefault = false; + return do_AddRef(remappingRules[i].remapping.get()); + } + } + + RefPtr<GamepadRemapper> defaultRemapper = new DefaultRemapper(); + aUsingDefault = true; + return do_AddRef(defaultRemapper.get()); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadRemapping.h b/dom/gamepad/GamepadRemapping.h new file mode 100644 index 0000000000..0a508fe134 --- /dev/null +++ b/dom/gamepad/GamepadRemapping.h @@ -0,0 +1,177 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GamepadRemapping_h_ +#define mozilla_dom_GamepadRemapping_h_ + +#include "mozilla/dom/GamepadBinding.h" +#include "mozilla/dom/GamepadLightIndicator.h" +#include "mozilla/dom/GamepadPoseState.h" +#include "mozilla/dom/GamepadTouchState.h" + +namespace mozilla::dom { + +// GamepadId is (vendorId << 16) | productId) +enum class GamepadId : uint32_t { + // Nexus Player Controller + kAsusTekProduct4500 = 0x0b054500, + // 2Axes 8Keys Game Pad + kDragonRiseProduct0011 = 0x00790011, + // ADT-1 Controller + kGoogleProduct2c40 = 0x18d12c40, + // Stadia Controller + kGoogleProduct9400 = 0x18d19400, + // Logitech F310, D-mode + kLogitechProductc216 = 0x046dc216, + // Logitech F510, D-mode + kLogitechProductc218 = 0x046dc218, + // Logitech F710, D-mode + kLogitechProductc219 = 0x046dc219, + // Microsoft Xbox 360 + kMicrosoftProductXbox360 = 0x045e028e, + // Microsoft Xbox 360 Wireless + kMicrosoftProductXbox360Wireless = 0x045e028f, + // Microsoft Xbox 360 Wireless + kMicrosoftProductXbox360Wireless2 = 0x045e0719, + // Microsoft Xbox One 2013 + kMicrosoftProductXbox2013 = 0x045e02d1, + // Microsoft Xbox One (2015 FW) + kMicrosoftProductXbox2015 = 0x045e02dd, + // Microsoft Xbox One S + kMicrosoftProductXboxOneS = 0x045e02ea, + // Microsoft Xbox One S Wireless + kMicrosoftProductXboxOneSWireless = 0x045e02e0, + // Microsoft Xbox One Elite + kMicrosoftProductXboxOneElite = 0x045e02e3, + // Microsoft Xbox One Elite 2 + kMicrosoftProductXboxOneElite2 = 0x045e0b00, + // Microsoft Xbox One Elite 2 Wireless + kMicrosoftProductXboxOneElite2Wireless = 0x045e0b05, + // Xbox One S Wireless (2016 FW) + kMicrosoftProductXboxOneSWireless2016 = 0x045e02fd, + // Microsoft Xbox Adaptive + kMicrosoftProductXboxAdaptive = 0x045e0b0a, + // Microsoft Xbox Adaptive Wireless + kMicrosoftProductXboxAdaptiveWireless = 0x045e0b0c, + // Microsoft Xbox Series X Wireless + kMicrosoftProductXboxSeriesXWireless = 0x045e0b13, + // Switch Joy-Con L + kNintendoProduct2006 = 0x057e2006, + // Switch Joy-Con R + kNintendoProduct2007 = 0x057e2007, + // Switch Pro Controller + kNintendoProduct2009 = 0x057e2009, + // Switch Charging Grip + kNintendoProduct200e = 0x057e200e, + // Nvidia Shield gamepad (2015) + kNvidiaProduct7210 = 0x09557210, + // Nvidia Shield gamepad (2017) + kNvidiaProduct7214 = 0x09557214, + // iBuffalo Classic + kPadixProduct2060 = 0x05832060, + // XSkills Gamecube USB adapter + kPlayComProduct0005 = 0x0b430005, + // boom PSX+N64 USB Converter + kPrototypeVendorProduct0667 = 0x66660667, + // Analog game controller + kPrototypeVendorProduct9401 = 0x66669401, + // Razer Serval Controller + kRazer1532Product0900 = 0x15320900, + // Playstation 3 Controller + kSonyProduct0268 = 0x054c0268, + // Playstation Dualshock 4 + kSonyProduct05c4 = 0x054c05c4, + // Dualshock 4 (PS4 Slim) + kSonyProduct09cc = 0x054c09cc, + // Dualshock 4 USB receiver + kSonyProduct0ba0 = 0x054c0ba0, + // Moga Pro Controller (HID mode) + kVendor20d6Product6271 = 0x20d66271, + // OnLive Controller (Bluetooth) + kVendor2378Product1008 = 0x23781008, + // OnLive Controller (Wired) + kVendor2378Product100a = 0x2378100a, + // OUYA Controller + kVendor2836Product0001 = 0x28360001, +}; + +// Follow the canonical ordering recommendation for the "Standard Gamepad" +// from https://www.w3.org/TR/gamepad/#remapping. +enum CanonicalButtonIndex { + BUTTON_INDEX_PRIMARY, + BUTTON_INDEX_SECONDARY, + BUTTON_INDEX_TERTIARY, + BUTTON_INDEX_QUATERNARY, + BUTTON_INDEX_LEFT_SHOULDER, + BUTTON_INDEX_RIGHT_SHOULDER, + BUTTON_INDEX_LEFT_TRIGGER, + BUTTON_INDEX_RIGHT_TRIGGER, + BUTTON_INDEX_BACK_SELECT, + BUTTON_INDEX_START, + BUTTON_INDEX_LEFT_THUMBSTICK, + BUTTON_INDEX_RIGHT_THUMBSTICK, + BUTTON_INDEX_DPAD_UP, + BUTTON_INDEX_DPAD_DOWN, + BUTTON_INDEX_DPAD_LEFT, + BUTTON_INDEX_DPAD_RIGHT, + BUTTON_INDEX_META, + BUTTON_INDEX_COUNT +}; + +enum CanonicalAxisIndex { + AXIS_INDEX_LEFT_STICK_X, + AXIS_INDEX_LEFT_STICK_Y, + AXIS_INDEX_RIGHT_STICK_X, + AXIS_INDEX_RIGHT_STICK_Y, + AXIS_INDEX_COUNT +}; + +static inline bool AxisNegativeAsButton(double input) { return input < -0.5; } + +static inline bool AxisPositiveAsButton(double input) { return input > 0.5; } + +class GamepadRemapper { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadRemapper) + + public: + virtual uint32_t GetAxisCount() const = 0; + virtual uint32_t GetButtonCount() const = 0; + virtual uint32_t GetLightIndicatorCount() const { return 0; } + virtual void GetLightIndicators( + nsTArray<GamepadLightIndicatorType>& aTypes) const {} + virtual uint32_t GetTouchEventCount() const { return 0; } + virtual void GetLightColorReport(uint8_t aRed, uint8_t aGreen, uint8_t aBlue, + std::vector<uint8_t>& aReport) const {} + virtual uint32_t GetMaxInputReportLength() const { return 0; } + + virtual void SetAxisCount(uint32_t aButtonCount) {} + virtual void SetButtonCount(uint32_t aButtonCount) {} + virtual GamepadMappingType GetMappingType() const { + return GamepadMappingType::Standard; + } + virtual void ProcessTouchData(GamepadHandle aHandle, void* aInput) {} + virtual void RemapAxisMoveEvent(GamepadHandle aHandle, uint32_t aAxis, + double aValue) const = 0; + virtual void RemapButtonEvent(GamepadHandle aHandle, uint32_t aButton, + bool aPressed) const = 0; + + protected: + GamepadRemapper() = default; + virtual ~GamepadRemapper() = default; +}; + +struct GamepadRemappingData { + GamepadId id; + RefPtr<GamepadRemapper> remapping; +}; + +already_AddRefed<GamepadRemapper> GetGamepadRemapper(const uint16_t aVendorId, + const uint16_t aProductId, + bool& aUsingDefault); + +} // namespace mozilla::dom + +#endif // mozilla_dom_GamepadRemapping_h_ diff --git a/dom/gamepad/GamepadServiceTest.cpp b/dom/gamepad/GamepadServiceTest.cpp new file mode 100644 index 0000000000..18998dcc4b --- /dev/null +++ b/dom/gamepad/GamepadServiceTest.cpp @@ -0,0 +1,388 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GamepadServiceTest.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/Unused.h" + +#include "mozilla/dom/GamepadManager.h" +#include "mozilla/dom/GamepadPlatformService.h" +#include "mozilla/dom/GamepadServiceTestBinding.h" +#include "mozilla/dom/GamepadTestChannelChild.h" + +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" + +namespace mozilla::dom { + +/* + * Implementation of the test service. This is just to provide a simple binding + * of the GamepadService to JavaScript via WebIDL so that we can write + * Mochitests that add and remove fake gamepads, avoiding the platform-specific + * backends. + */ + +constexpr uint32_t kMaxButtons = 20; +constexpr uint32_t kMaxAxes = 10; +constexpr uint32_t kMaxHaptics = 2; +constexpr uint32_t kMaxLightIndicator = 2; +constexpr uint32_t kMaxTouchEvents = 4; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(GamepadServiceTest, DOMEventTargetHelper, + mWindow) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GamepadServiceTest) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(GamepadServiceTest, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(GamepadServiceTest, DOMEventTargetHelper) + +// static +already_AddRefed<GamepadServiceTest> GamepadServiceTest::CreateTestService( + nsPIDOMWindowInner* aWindow) { + MOZ_ASSERT(aWindow); + RefPtr<GamepadServiceTest> service = new GamepadServiceTest(aWindow); + service->InitPBackgroundActor(); + return service.forget(); +} + +void GamepadServiceTest::Shutdown() { + MOZ_ASSERT(!mShuttingDown); + mShuttingDown = true; + DestroyPBackgroundActor(); + mWindow = nullptr; +} + +GamepadServiceTest::GamepadServiceTest(nsPIDOMWindowInner* aWindow) + : mService(GamepadManager::GetService()), + mWindow(aWindow), + mEventNumber(0), + mShuttingDown(false), + mChild(nullptr) {} + +GamepadServiceTest::~GamepadServiceTest() { + MOZ_ASSERT(mPromiseList.IsEmpty()); +} + +void GamepadServiceTest::InitPBackgroundActor() { + MOZ_ASSERT(!mChild); + + ::mozilla::ipc::PBackgroundChild* actor = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actor)) { + MOZ_CRASH("Failed to create a PBackgroundChild actor!"); + } + + mChild = GamepadTestChannelChild::Create(this); + PGamepadTestChannelChild* initedChild = + actor->SendPGamepadTestChannelConstructor(mChild.get()); + if (NS_WARN_IF(!initedChild)) { + MOZ_CRASH("Failed to create a PBackgroundChild actor!"); + } +} + +void GamepadServiceTest::ReplyGamepadHandle(uint32_t aPromiseId, + const GamepadHandle& aHandle) { + uint32_t handleSlot = AddGamepadHandle(aHandle); + + RefPtr<Promise> p; + if (!mPromiseList.Get(aPromiseId, getter_AddRefs(p))) { + MOZ_CRASH("We should always have a promise."); + } + + p->MaybeResolve(handleSlot); + mPromiseList.Remove(aPromiseId); +} + +void GamepadServiceTest::DestroyPBackgroundActor() { + MOZ_ASSERT(mChild); + PGamepadTestChannelChild::Send__delete__(mChild); + mChild = nullptr; +} + +already_AddRefed<Promise> GamepadServiceTest::AddGamepad( + const nsAString& aID, GamepadMappingType aMapping, GamepadHand aHand, + uint32_t aNumButtons, uint32_t aNumAxes, uint32_t aNumHaptics, + uint32_t aNumLightIndicator, uint32_t aNumTouchEvents, ErrorResult& aRv) { + if (aNumButtons > kMaxButtons || aNumAxes > kMaxAxes || + aNumHaptics > kMaxHaptics || aNumLightIndicator > kMaxLightIndicator || + aNumTouchEvents > kMaxTouchEvents) { + aRv.ThrowNotSupportedError("exceeded maximum hardware dimensions"); + return nullptr; + } + + if (mShuttingDown) { + aRv.ThrowInvalidStateError("Shutting down"); + return nullptr; + } + + // The values here are ignored, the value just can't be zero to avoid an + // assertion + GamepadHandle gamepadHandle{1, GamepadHandleKind::GamepadPlatformManager}; + + // Only VR controllers has displayID, we give 0 to the general gamepads. + GamepadAdded a(nsString(aID), aMapping, aHand, 0, aNumButtons, aNumAxes, + aNumHaptics, aNumLightIndicator, aNumTouchEvents); + GamepadChangeEventBody body(a); + GamepadChangeEvent e(gamepadHandle, body); + + RefPtr<Promise> p = Promise::Create(mWindow->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + uint32_t id = ++mEventNumber; + + MOZ_ASSERT(!mPromiseList.Contains(id)); + mPromiseList.InsertOrUpdate(id, RefPtr{p}); + + mChild->SendGamepadTestEvent(id, e); + + return p.forget(); +} + +already_AddRefed<Promise> GamepadServiceTest::RemoveGamepad( + uint32_t aHandleSlot, ErrorResult& aRv) { + if (mShuttingDown) { + aRv.ThrowInvalidStateError("Shutting down"); + return nullptr; + } + + GamepadHandle gamepadHandle = GetHandleInSlot(aHandleSlot); + + GamepadRemoved a; + GamepadChangeEventBody body(a); + GamepadChangeEvent e(gamepadHandle, body); + + uint32_t id = ++mEventNumber; + + RefPtr<Promise> p = Promise::Create(mWindow->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + MOZ_ASSERT(!mPromiseList.Contains(id)); + mPromiseList.InsertOrUpdate(id, RefPtr{p}); + + mChild->SendGamepadTestEvent(id, e); + return p.forget(); +} + +already_AddRefed<Promise> GamepadServiceTest::NewButtonEvent( + uint32_t aHandleSlot, uint32_t aButton, bool aPressed, bool aTouched, + ErrorResult& aRv) { + if (mShuttingDown) { + aRv.ThrowInvalidStateError("Shutting down"); + return nullptr; + } + + GamepadHandle gamepadHandle = GetHandleInSlot(aHandleSlot); + + GamepadButtonInformation a(aButton, aPressed ? 1.0 : 0, aPressed, aTouched); + GamepadChangeEventBody body(a); + GamepadChangeEvent e(gamepadHandle, body); + + uint32_t id = ++mEventNumber; + RefPtr<Promise> p = Promise::Create(mWindow->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + MOZ_ASSERT(!mPromiseList.Contains(id)); + mPromiseList.InsertOrUpdate(id, RefPtr{p}); + mChild->SendGamepadTestEvent(id, e); + return p.forget(); +} + +already_AddRefed<Promise> GamepadServiceTest::NewButtonValueEvent( + uint32_t aHandleSlot, uint32_t aButton, bool aPressed, bool aTouched, + double aValue, ErrorResult& aRv) { + if (mShuttingDown) { + aRv.ThrowInvalidStateError("Shutting down"); + return nullptr; + } + + GamepadHandle gamepadHandle = GetHandleInSlot(aHandleSlot); + + GamepadButtonInformation a(aButton, aValue, aPressed, aTouched); + GamepadChangeEventBody body(a); + GamepadChangeEvent e(gamepadHandle, body); + + uint32_t id = ++mEventNumber; + RefPtr<Promise> p = Promise::Create(mWindow->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + MOZ_ASSERT(!mPromiseList.Contains(id)); + mPromiseList.InsertOrUpdate(id, RefPtr{p}); + mChild->SendGamepadTestEvent(id, e); + return p.forget(); +} + +already_AddRefed<Promise> GamepadServiceTest::NewAxisMoveEvent( + uint32_t aHandleSlot, uint32_t aAxis, double aValue, ErrorResult& aRv) { + if (mShuttingDown) { + aRv.ThrowInvalidStateError("Shutting down"); + return nullptr; + } + + GamepadHandle gamepadHandle = GetHandleInSlot(aHandleSlot); + + GamepadAxisInformation a(aAxis, aValue); + GamepadChangeEventBody body(a); + GamepadChangeEvent e(gamepadHandle, body); + + uint32_t id = ++mEventNumber; + RefPtr<Promise> p = Promise::Create(mWindow->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + MOZ_ASSERT(!mPromiseList.Contains(id)); + mPromiseList.InsertOrUpdate(id, RefPtr{p}); + mChild->SendGamepadTestEvent(id, e); + return p.forget(); +} + +already_AddRefed<Promise> GamepadServiceTest::NewPoseMove( + uint32_t aHandleSlot, const Nullable<Float32Array>& aOrient, + const Nullable<Float32Array>& aPos, + const Nullable<Float32Array>& aAngVelocity, + const Nullable<Float32Array>& aAngAcceleration, + const Nullable<Float32Array>& aLinVelocity, + const Nullable<Float32Array>& aLinAcceleration, ErrorResult& aRv) { + if (mShuttingDown) { + aRv.ThrowInvalidStateError("Shutting down"); + return nullptr; + } + + GamepadHandle gamepadHandle = GetHandleInSlot(aHandleSlot); + + GamepadPoseState poseState; + poseState.flags = GamepadCapabilityFlags::Cap_Orientation | + GamepadCapabilityFlags::Cap_Position | + GamepadCapabilityFlags::Cap_AngularAcceleration | + GamepadCapabilityFlags::Cap_LinearAcceleration; + if (!aOrient.IsNull()) { + DebugOnly<bool> ok = aOrient.Value().CopyDataTo(poseState.orientation); + MOZ_ASSERT( + ok, "aOrient.Value().Length() != ArrayLength(poseState.orientation)"); + poseState.isOrientationValid = true; + } + if (!aPos.IsNull()) { + DebugOnly<bool> ok = aPos.Value().CopyDataTo(poseState.position); + MOZ_ASSERT(ok, "aPos.Value().Length() != ArrayLength(poseState.position)"); + poseState.isPositionValid = true; + } + if (!aAngVelocity.IsNull()) { + DebugOnly<bool> ok = + aAngVelocity.Value().CopyDataTo(poseState.angularVelocity); + MOZ_ASSERT(ok, + "aAngVelocity.Value().Length() != " + "ArrayLength(poseState.angularVelocity)"); + } + if (!aAngAcceleration.IsNull()) { + DebugOnly<bool> ok = + aAngAcceleration.Value().CopyDataTo(poseState.angularAcceleration); + MOZ_ASSERT(ok, + "aAngAcceleration.Value().Length() != " + "ArrayLength(poseState.angularAcceleration)"); + } + if (!aLinVelocity.IsNull()) { + DebugOnly<bool> ok = + aLinVelocity.Value().CopyDataTo(poseState.linearVelocity); + MOZ_ASSERT(ok, + "aLinVelocity.Value().Length() != " + "ArrayLength(poseState.linearVelocity)"); + } + if (!aLinAcceleration.IsNull()) { + DebugOnly<bool> ok = + aLinAcceleration.Value().CopyDataTo(poseState.linearAcceleration); + MOZ_ASSERT(ok, + "aLinAcceleration.Value().Length() != " + "ArrayLength(poseState.linearAcceleration)"); + } + + GamepadPoseInformation a(poseState); + GamepadChangeEventBody body(a); + GamepadChangeEvent e(gamepadHandle, body); + + uint32_t id = ++mEventNumber; + RefPtr<Promise> p = Promise::Create(mWindow->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + MOZ_ASSERT(!mPromiseList.Contains(id)); + mPromiseList.InsertOrUpdate(id, RefPtr{p}); + mChild->SendGamepadTestEvent(id, e); + return p.forget(); +} + +already_AddRefed<Promise> GamepadServiceTest::NewTouch( + uint32_t aHandleSlot, uint32_t aTouchArrayIndex, uint32_t aTouchId, + uint8_t aSurfaceId, const Float32Array& aPos, + const Nullable<Float32Array>& aSurfDim, ErrorResult& aRv) { + if (mShuttingDown) { + aRv.ThrowInvalidStateError("Shutting down"); + return nullptr; + } + + GamepadHandle gamepadHandle = GetHandleInSlot(aHandleSlot); + + GamepadTouchState touchState; + touchState.touchId = aTouchId; + touchState.surfaceId = aSurfaceId; + DebugOnly<bool> ok = aPos.CopyDataTo(touchState.position); + MOZ_ASSERT(ok, "aPos.Length() != ArrayLength(touchState.position)"); + + if (!aSurfDim.IsNull()) { + ok = aSurfDim.Value().CopyDataTo(touchState.surfaceDimensions); + MOZ_ASSERT( + ok, "aSurfDim.Length() != ArrayLength(touchState.surfaceDimensions)"); + touchState.isSurfaceDimensionsValid = true; + } + + GamepadTouchInformation a(aTouchArrayIndex, touchState); + GamepadChangeEventBody body(a); + GamepadChangeEvent e(gamepadHandle, body); + + uint32_t id = ++mEventNumber; + RefPtr<Promise> p = Promise::Create(mWindow->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + MOZ_ASSERT(!mPromiseList.Contains(id)); + mPromiseList.InsertOrUpdate(id, RefPtr{p}); + mChild->SendGamepadTestEvent(id, e); + return p.forget(); +} + +JSObject* GamepadServiceTest::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return GamepadServiceTest_Binding::Wrap(aCx, this, aGivenProto); +} + +uint32_t GamepadServiceTest::AddGamepadHandle(GamepadHandle aHandle) { + uint32_t handleSlot = mGamepadHandles.Length(); + mGamepadHandles.AppendElement(aHandle); + return handleSlot; +} + +void GamepadServiceTest::RemoveGamepadHandle(uint32_t aHandleSlot) { + MOZ_ASSERT(aHandleSlot < mGamepadHandles.Length()); + return mGamepadHandles.RemoveElementAt(aHandleSlot); +} + +GamepadHandle GamepadServiceTest::GetHandleInSlot(uint32_t aHandleSlot) const { + MOZ_ASSERT(aHandleSlot < mGamepadHandles.Length()); + return mGamepadHandles.ElementAt(aHandleSlot); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadServiceTest.h b/dom/gamepad/GamepadServiceTest.h new file mode 100644 index 0000000000..3b88ff178a --- /dev/null +++ b/dom/gamepad/GamepadServiceTest.h @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GamepadServiceTest_h_ +#define mozilla_dom_GamepadServiceTest_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/GamepadBinding.h" +#include "mozilla/dom/GamepadHandle.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/WeakPtr.h" + +namespace mozilla::dom { + +class GamepadChangeEvent; +class GamepadManager; +class GamepadTestChannelChild; +class Promise; + +// Service for testing purposes +class GamepadServiceTest final : public DOMEventTargetHelper, + public SupportsWeakPtr { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(GamepadServiceTest, + DOMEventTargetHelper) + + GamepadMappingType NoMapping() const { return GamepadMappingType::_empty; } + GamepadMappingType StandardMapping() const { + return GamepadMappingType::Standard; + } + GamepadHand NoHand() const { return GamepadHand::_empty; } + GamepadHand LeftHand() const { return GamepadHand::Left; } + GamepadHand RightHand() const { return GamepadHand::Right; } + + // IPC receiver + void ReplyGamepadHandle(uint32_t aPromiseId, const GamepadHandle& aHandle); + + // Methods from GamepadServiceTest.webidl + already_AddRefed<Promise> AddGamepad( + const nsAString& aID, GamepadMappingType aMapping, GamepadHand aHand, + uint32_t aNumButtons, uint32_t aNumAxes, uint32_t aNumHaptics, + uint32_t aNumLightIndicator, uint32_t aNumTouchEvents, ErrorResult& aRv); + + already_AddRefed<Promise> RemoveGamepad(uint32_t aHandleSlot, + ErrorResult& aRv); + + already_AddRefed<Promise> NewButtonEvent(uint32_t aHandleSlot, + uint32_t aButton, bool aPressed, + bool aTouched, ErrorResult& aRv); + + already_AddRefed<Promise> NewButtonValueEvent(uint32_t aHandleSlot, + uint32_t aButton, bool aPressed, + bool aTouched, double aValue, + ErrorResult& aRv); + + already_AddRefed<Promise> NewAxisMoveEvent(uint32_t aHandleSlot, + uint32_t aAxis, double aValue, + ErrorResult& aRv); + + already_AddRefed<Promise> NewPoseMove( + uint32_t aHandleSlot, const Nullable<Float32Array>& aOrient, + const Nullable<Float32Array>& aPos, + const Nullable<Float32Array>& aAngVelocity, + const Nullable<Float32Array>& aAngAcceleration, + const Nullable<Float32Array>& aLinVelocity, + const Nullable<Float32Array>& aLinAcceleration, ErrorResult& aRv); + + already_AddRefed<Promise> NewTouch(uint32_t aHandleSlot, + uint32_t aTouchArrayIndex, + uint32_t aTouchId, uint8_t aSurfaceId, + const Float32Array& aPos, + const Nullable<Float32Array>& aSurfDim, + ErrorResult& aRv); + + void Shutdown(); + + static already_AddRefed<GamepadServiceTest> CreateTestService( + nsPIDOMWindowInner* aWindow); + nsPIDOMWindowInner* GetParentObject() const { return mWindow; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + // Hold a reference to the gamepad service so we don't have to worry about + // execution order in tests. + RefPtr<GamepadManager> mService; + nsCOMPtr<nsPIDOMWindowInner> mWindow; + uint32_t mEventNumber; + bool mShuttingDown; + + // IPDL Channel for us to send test events to GamepadPlatformService, it + // will only be used in this singleton class and deleted during the IPDL + // shutdown chain + RefPtr<GamepadTestChannelChild> mChild; + nsTArray<GamepadHandle> mGamepadHandles; + + nsRefPtrHashtable<nsUint32HashKey, dom::Promise> mPromiseList; + + explicit GamepadServiceTest(nsPIDOMWindowInner* aWindow); + ~GamepadServiceTest(); + void InitPBackgroundActor(); + void DestroyPBackgroundActor(); + + uint32_t AddGamepadHandle(GamepadHandle aHandle); + void RemoveGamepadHandle(uint32_t aHandleSlot); + GamepadHandle GetHandleInSlot(uint32_t aHandleSlot) const; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/gamepad/GamepadTouch.cpp b/dom/gamepad/GamepadTouch.cpp new file mode 100644 index 0000000000..87be8e6701 --- /dev/null +++ b/dom/gamepad/GamepadTouch.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/GamepadTouch.h" + +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/GamepadManager.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/TypedArray.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS(GamepadTouch, (mParent), + (mPosition, + mSurfaceDimensions)) + +GamepadTouch::GamepadTouch(nsISupports* aParent) + : mParent(aParent), mPosition(nullptr), mSurfaceDimensions(nullptr) { + mozilla::HoldJSObjects(this); + mTouchState = GamepadTouchState(); +} + +GamepadTouch::~GamepadTouch() { mozilla::DropJSObjects(this); } + +/* virtual */ JSObject* GamepadTouch::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return GamepadTouch_Binding::Wrap(aCx, this, aGivenProto); +} + +void GamepadTouch::GetPosition(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + mPosition = Float32Array::Create(aCx, this, mTouchState.position, aRv); + if (aRv.Failed()) { + return; + } + + aRetval.set(mPosition); +} + +void GamepadTouch::GetSurfaceDimensions(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + if (mTouchState.isSurfaceDimensionsValid) { + mSurfaceDimensions = + Uint32Array::Create(aCx, this, mTouchState.surfaceDimensions, aRv); + } else { + mSurfaceDimensions = Uint32Array::Create( + aCx, this, std::size(mTouchState.surfaceDimensions), aRv); + } + + if (!mSurfaceDimensions) { + aRv.NoteJSContextException(aCx); + return; + } + + aRetval.set(mSurfaceDimensions); +} + +void GamepadTouch::SetTouchState(const GamepadTouchState& aTouch) { + mTouchState = aTouch; +} + +void GamepadTouch::Set(const GamepadTouch* aOther) { + MOZ_ASSERT(aOther); + mTouchState = aOther->mTouchState; +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/GamepadTouch.h b/dom/gamepad/GamepadTouch.h new file mode 100644 index 0000000000..591905ed73 --- /dev/null +++ b/dom/gamepad/GamepadTouch.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_GamepadTouch_h +#define mozilla_dom_gamepad_GamepadTouch_h + +#include "mozilla/dom/GamepadTouchBinding.h" +#include "mozilla/dom/GamepadTouchState.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class GamepadTouch final : public nsWrapperCache { + public: + explicit GamepadTouch(nsISupports* aParent); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(GamepadTouch) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(GamepadTouch) + + nsISupports* GetParentObject() const { return mParent; } + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + uint32_t TouchId() const { return mTouchState.touchId; } + uint32_t SurfaceId() const { return mTouchState.surfaceId; } + void GetPosition(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + void GetSurfaceDimensions(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + void SetTouchState(const GamepadTouchState& aTouch); + void Set(const GamepadTouch* aOther); + + private: + virtual ~GamepadTouch(); + + nsCOMPtr<nsISupports> mParent; + JS::Heap<JSObject*> mPosition; + JS::Heap<JSObject*> mSurfaceDimensions; + GamepadTouchState mTouchState; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_GamepadTouch_h diff --git a/dom/gamepad/GamepadTouchState.h b/dom/gamepad/GamepadTouchState.h new file mode 100644 index 0000000000..0af81a6451 --- /dev/null +++ b/dom/gamepad/GamepadTouchState.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_GamepadTouchState_h_ +#define mozilla_dom_gamepad_GamepadTouchState_h_ + +namespace mozilla::dom { + +struct GamepadTouchState { + uint32_t touchId; + uint32_t surfaceId; + float position[2]; + uint32_t surfaceDimensions[2]; + bool isSurfaceDimensionsValid; + + GamepadTouchState() + : touchId(0), + surfaceId(0), + position{0, 0}, + surfaceDimensions{0, 0}, + isSurfaceDimensionsValid(false) {} + + bool operator==(const GamepadTouchState& aTouch) const { + return touchId == aTouch.touchId && surfaceId == aTouch.surfaceId && + position[0] == aTouch.position[0] && + position[1] == aTouch.position[1] && + surfaceDimensions[0] == aTouch.surfaceDimensions[0] && + surfaceDimensions[1] == aTouch.surfaceDimensions[1] && + isSurfaceDimensionsValid == aTouch.isSurfaceDimensionsValid; + } + + bool operator!=(const GamepadTouchState& aTouch) const { + return !(*this == aTouch); + } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_gamepad_GamepadTouchState_h_ diff --git a/dom/gamepad/android/AndroidGamepad.cpp b/dom/gamepad/android/AndroidGamepad.cpp new file mode 100644 index 0000000000..8887d4ac43 --- /dev/null +++ b/dom/gamepad/android/AndroidGamepad.cpp @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// TODO: Bug 680289, implement gamepad haptics for Android. +// TODO: Bug 1523355, implement gamepad lighindicator and touch for +// Android. + +#include "mozilla/java/AndroidGamepadManagerNatives.h" +#include "mozilla/java/GeckoAppShellWrappers.h" +#include "mozilla/dom/Gamepad.h" +#include "mozilla/dom/GamepadHandle.h" +#include "mozilla/dom/GamepadPlatformService.h" +#include "mozilla/Tainting.h" +#include "nsThreadUtils.h" + +using mozilla::dom::GamepadHandle; + +namespace mozilla { +namespace dom { + +class AndroidGamepadManager final + : public java::AndroidGamepadManager::Natives<AndroidGamepadManager> { + AndroidGamepadManager() = delete; + + public: + static jni::ByteArray::LocalRef NativeAddGamepad() { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_RELEASE_ASSERT(service); + + const GamepadHandle gamepadHandle = service->AddGamepad( + "android", GamepadMappingType::Standard, GamepadHand::_empty, + kStandardGamepadButtons, kStandardGamepadAxes, 0, 0, 0); + + return mozilla::jni::ByteArray::New( + reinterpret_cast<const int8_t*>(&gamepadHandle), sizeof(gamepadHandle)); + } + + static void NativeRemoveGamepad(jni::ByteArray::Param aGamepadHandleBytes) { + GamepadHandle handle = JNIByteArrayToGamepadHandle(aGamepadHandleBytes); + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + service->RemoveGamepad(handle); + } + + static void OnButtonChange(jni::ByteArray::Param aGamepadHandleBytes, + int32_t aButton, bool aPressed, float aValue) { + GamepadHandle handle = JNIByteArrayToGamepadHandle(aGamepadHandleBytes); + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + service->NewButtonEvent(handle, aButton, aPressed, aValue); + } + + static void OnAxisChange(jni::ByteArray::Param aGamepadHandleBytes, + jni::BooleanArray::Param aValid, + jni::FloatArray::Param aValues) { + GamepadHandle handle = JNIByteArrayToGamepadHandle(aGamepadHandleBytes); + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + const auto& valid = aValid->GetElements(); + const auto& values = aValues->GetElements(); + MOZ_ASSERT(valid.Length() == values.Length()); + + for (size_t i = 0; i < values.Length(); i++) { + if (valid[i]) { + service->NewAxisMoveEvent(handle, i, values[i]); + } + } + } + + private: + static GamepadHandle JNIByteArrayToGamepadHandle( + jni::ByteArray::Param aGamepadHandleBytes) { + MOZ_ASSERT(aGamepadHandleBytes->Length() == sizeof(GamepadHandle)); + + GamepadHandle gamepadHandle; + aGamepadHandleBytes->CopyTo(reinterpret_cast<int8_t*>(&gamepadHandle), + sizeof(gamepadHandle)); + + return gamepadHandle; + } +}; + +void StartGamepadMonitoring() { + AndroidGamepadManager::Init(); + java::AndroidGamepadManager::Start( + java::GeckoAppShell::GetApplicationContext()); +} + +void StopGamepadMonitoring() { + java::AndroidGamepadManager::Stop( + java::GeckoAppShell::GetApplicationContext()); +} + +void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, + const uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue) { + NS_WARNING("Android doesn't support gamepad light indicator."); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/cocoa/CocoaGamepad.cpp b/dom/gamepad/cocoa/CocoaGamepad.cpp new file mode 100644 index 0000000000..544c2e42b7 --- /dev/null +++ b/dom/gamepad/cocoa/CocoaGamepad.cpp @@ -0,0 +1,665 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// mostly derived from the Allegro source code at: +// http://alleg.svn.sourceforge.net/viewvc/alleg/allegro/branches/4.9/src/macosx/hidjoy.m?revision=13760&view=markup + +#include "mozilla/dom/GamepadHandle.h" +#include "mozilla/dom/GamepadPlatformService.h" +#include "mozilla/dom/GamepadRemapping.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Sprintf.h" +#include "mozilla/Tainting.h" +#include "nsComponentManagerUtils.h" +#include "nsITimer.h" +#include "nsThreadUtils.h" +#include <CoreFoundation/CoreFoundation.h> +#include <IOKit/hid/IOHIDBase.h> +#include <IOKit/hid/IOHIDKeys.h> +#include <IOKit/hid/IOHIDManager.h> + +#include <stdio.h> +#include <vector> + +namespace { + +using namespace mozilla; +using namespace mozilla::dom; +class DarwinGamepadService; + +DarwinGamepadService* gService = nullptr; + +struct Button { + int id; + bool analog; + IOHIDElementRef element; + CFIndex min; + CFIndex max; + bool pressed; + + Button(int aId, IOHIDElementRef aElement, CFIndex aMin, CFIndex aMax) + : id(aId), + analog((aMax - aMin) > 1), + element(aElement), + min(aMin), + max(aMax), + pressed(false) {} +}; + +struct Axis { + int id; + IOHIDElementRef element; + uint32_t usagePage; + uint32_t usage; + CFIndex min; + CFIndex max; + double value; +}; + +// These values can be found in the USB HID Usage Tables: +// http://www.usb.org/developers/hidpage +const unsigned kDesktopUsagePage = 0x01; +const unsigned kSimUsagePage = 0x02; +const unsigned kAcceleratorUsage = 0xC4; +const unsigned kBrakeUsage = 0xC5; +const unsigned kJoystickUsage = 0x04; +const unsigned kGamepadUsage = 0x05; +const unsigned kAxisUsageMin = 0x30; +const unsigned kAxisUsageMax = 0x35; +const unsigned kDpadUsage = 0x39; +const unsigned kButtonUsagePage = 0x09; +const unsigned kConsumerPage = 0x0C; +const unsigned kHomeUsage = 0x223; +const unsigned kBackUsage = 0x224; + +// We poll it periodically, +// 50ms is arbitrarily chosen. +const uint32_t kDarwinGamepadPollInterval = 50; + +struct GamepadInputReportContext { + DarwinGamepadService* service; + size_t gamepadSlot; +}; + +class Gamepad { + private: + IOHIDDeviceRef mDevice; + nsTArray<Button> buttons; + nsTArray<Axis> axes; + + public: + Gamepad() : mDevice(nullptr) {} + + bool operator==(IOHIDDeviceRef device) const { return mDevice == device; } + bool empty() const { return mDevice == nullptr; } + void clear() { + mDevice = nullptr; + buttons.Clear(); + axes.Clear(); + mHandle = GamepadHandle{}; + } + void init(IOHIDDeviceRef device, bool defaultRemapper); + void ReportChanged(uint8_t* report, CFIndex report_length); + size_t WriteOutputReport(const std::vector<uint8_t>& aReport) const; + + size_t numButtons() { return buttons.Length(); } + size_t numAxes() { return axes.Length(); } + + Button* lookupButton(IOHIDElementRef element) { + for (unsigned i = 0; i < buttons.Length(); i++) { + if (buttons[i].element == element) return &buttons[i]; + } + return nullptr; + } + + Axis* lookupAxis(IOHIDElementRef element) { + for (unsigned i = 0; i < axes.Length(); i++) { + if (axes[i].element == element) return &axes[i]; + } + return nullptr; + } + + GamepadHandle mHandle; + RefPtr<GamepadRemapper> mRemapper; + nsTArray<uint8_t> mInputReport; + UniquePtr<GamepadInputReportContext> mInputReportContext; +}; + +void Gamepad::init(IOHIDDeviceRef aDevice, bool aDefaultRemapper) { + clear(); + mDevice = aDevice; + + CFArrayRef elements = + IOHIDDeviceCopyMatchingElements(aDevice, nullptr, kIOHIDOptionsTypeNone); + CFIndex n = CFArrayGetCount(elements); + for (CFIndex i = 0; i < n; i++) { + IOHIDElementRef element = + (IOHIDElementRef)CFArrayGetValueAtIndex(elements, i); + uint32_t usagePage = IOHIDElementGetUsagePage(element); + uint32_t usage = IOHIDElementGetUsage(element); + + if (usagePage == kDesktopUsagePage && usage >= kAxisUsageMin && + usage <= kAxisUsageMax) { + Axis axis = {aDefaultRemapper ? int(axes.Length()) + : static_cast<int>(usage - kAxisUsageMin), + element, + usagePage, + usage, + IOHIDElementGetLogicalMin(element), + IOHIDElementGetLogicalMax(element)}; + axes.AppendElement(axis); + } else if (usagePage == kDesktopUsagePage && usage == kDpadUsage && + // Don't know how to handle d-pads that return weird values. + IOHIDElementGetLogicalMax(element) - + IOHIDElementGetLogicalMin(element) == + 7) { + Axis axis = {aDefaultRemapper ? int(axes.Length()) + : static_cast<int>(usage - kAxisUsageMin), + element, + usagePage, + usage, + IOHIDElementGetLogicalMin(element), + IOHIDElementGetLogicalMax(element)}; + axes.AppendElement(axis); + } else if (usagePage == kSimUsagePage && + (usage == kAcceleratorUsage || usage == kBrakeUsage)) { + if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Button) { + Button button(int(buttons.Length()), element, + IOHIDElementGetLogicalMin(element), + IOHIDElementGetLogicalMax(element)); + buttons.AppendElement(button); + } else { + Axis axis = {aDefaultRemapper ? int(axes.Length()) + : static_cast<int>(usage - kAxisUsageMin), + element, + usagePage, + usage, + IOHIDElementGetLogicalMin(element), + IOHIDElementGetLogicalMax(element)}; + axes.AppendElement(axis); + } + } else if ((usagePage == kButtonUsagePage) || + (usagePage == kConsumerPage && + (usage == kHomeUsage || usage == kBackUsage))) { + Button button(int(buttons.Length()), element, + IOHIDElementGetLogicalMin(element), + IOHIDElementGetLogicalMax(element)); + buttons.AppendElement(button); + } else { + // TODO: handle other usage pages + } + } +} + +// This service is created and destroyed in Background thread while +// operates in a seperate thread(We call it Monitor thread here). +class DarwinGamepadService { + private: + IOHIDManagerRef mManager; + nsTArray<Gamepad> mGamepads; + + nsCOMPtr<nsIThread> mMonitorThread; + nsCOMPtr<nsIThread> mBackgroundThread; + nsCOMPtr<nsITimer> mPollingTimer; + bool mIsRunning; + + static void DeviceAddedCallback(void* data, IOReturn result, void* sender, + IOHIDDeviceRef device); + static void DeviceRemovedCallback(void* data, IOReturn result, void* sender, + IOHIDDeviceRef device); + static void InputValueChangedCallback(void* data, IOReturn result, + void* sender, IOHIDValueRef newValue); + static void EventLoopOnceCallback(nsITimer* aTimer, void* aClosure); + + void DeviceAdded(IOHIDDeviceRef device); + void DeviceRemoved(IOHIDDeviceRef device); + void InputValueChanged(IOHIDValueRef value); + void StartupInternal(); + void RunEventLoopOnce(); + + public: + DarwinGamepadService(); + ~DarwinGamepadService(); + + static void ReportChangedCallback(void* context, IOReturn result, + void* sender, IOHIDReportType report_type, + uint32_t report_id, uint8_t* report, + CFIndex report_length); + + void Startup(); + void Shutdown(); + void SetLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, + const uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue); + friend class DarwinGamepadServiceStartupRunnable; + friend class DarwinGamepadServiceShutdownRunnable; +}; + +class DarwinGamepadServiceStartupRunnable final : public Runnable { + private: + ~DarwinGamepadServiceStartupRunnable() {} + // This Runnable schedules startup of DarwinGamepadService + // in a new thread, pointer to DarwinGamepadService is only + // used by this Runnable within its thread. + DarwinGamepadService MOZ_NON_OWNING_REF* mService; + + public: + explicit DarwinGamepadServiceStartupRunnable(DarwinGamepadService* service) + : Runnable("DarwinGamepadServiceStartupRunnable"), mService(service) {} + NS_IMETHOD Run() override { + MOZ_ASSERT(mService); + mService->StartupInternal(); + return NS_OK; + } +}; + +class DarwinGamepadServiceShutdownRunnable final : public Runnable { + private: + ~DarwinGamepadServiceShutdownRunnable() {} + + public: + // This Runnable schedules shutdown of DarwinGamepadService + // in background thread. + explicit DarwinGamepadServiceShutdownRunnable() + : Runnable("DarwinGamepadServiceStartupRunnable") {} + NS_IMETHOD Run() override { + MOZ_ASSERT(gService); + MOZ_ASSERT(NS_GetCurrentThread() == gService->mBackgroundThread); + + IOHIDManagerRef manager = (IOHIDManagerRef)gService->mManager; + + if (manager) { + IOHIDManagerClose(manager, 0); + CFRelease(manager); + gService->mManager = nullptr; + } + gService->mMonitorThread->Shutdown(); + delete gService; + gService = nullptr; + return NS_OK; + } +}; + +void DarwinGamepadService::DeviceAdded(IOHIDDeviceRef device) { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + size_t slot = size_t(-1); + for (size_t i = 0; i < mGamepads.Length(); i++) { + if (mGamepads[i] == device) return; + if (slot == size_t(-1) && mGamepads[i].empty()) slot = i; + } + + if (slot == size_t(-1)) { + slot = mGamepads.Length(); + mGamepads.AppendElement(Gamepad()); + } + + // Gather some identifying information + CFNumberRef vendorIdRef = + (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey)); + CFNumberRef productIdRef = + (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)); + CFStringRef productRef = + (CFStringRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey)); + int vendorId, productId; + CFNumberGetValue(vendorIdRef, kCFNumberIntType, &vendorId); + CFNumberGetValue(productIdRef, kCFNumberIntType, &productId); + char product_name[128]; + CFStringGetCString(productRef, product_name, sizeof(product_name), + kCFStringEncodingASCII); + char buffer[256]; + SprintfLiteral(buffer, "%x-%x-%s", vendorId, productId, product_name); + + bool defaultRemapper = false; + RefPtr<GamepadRemapper> remapper = + GetGamepadRemapper(vendorId, productId, defaultRemapper); + MOZ_ASSERT(remapper); + mGamepads[slot].init(device, defaultRemapper); + + remapper->SetAxisCount(mGamepads[slot].numAxes()); + remapper->SetButtonCount(mGamepads[slot].numButtons()); + + GamepadHandle handle = service->AddGamepad( + buffer, remapper->GetMappingType(), mozilla::dom::GamepadHand::_empty, + remapper->GetButtonCount(), remapper->GetAxisCount(), + 0, // TODO: Bug 680289, implement gamepad haptics for cocoa. + remapper->GetLightIndicatorCount(), remapper->GetTouchEventCount()); + + nsTArray<GamepadLightIndicatorType> lightTypes; + remapper->GetLightIndicators(lightTypes); + for (uint32_t i = 0; i < lightTypes.Length(); ++i) { + if (lightTypes[i] != GamepadLightIndicator::DefaultType()) { + service->NewLightIndicatorTypeEvent(handle, i, lightTypes[i]); + } + } + + mGamepads[slot].mHandle = handle; + mGamepads[slot].mInputReport.SetLength(remapper->GetMaxInputReportLength()); + mGamepads[slot].mInputReportContext = UniquePtr<GamepadInputReportContext>( + new GamepadInputReportContext{this, slot}); + mGamepads[slot].mRemapper = remapper.forget(); + + IOHIDDeviceRegisterInputReportCallback( + device, mGamepads[slot].mInputReport.Elements(), + mGamepads[slot].mInputReport.Length(), ReportChangedCallback, + mGamepads[slot].mInputReportContext.get()); +} + +void DarwinGamepadService::DeviceRemoved(IOHIDDeviceRef device) { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + for (Gamepad& gamepad : mGamepads) { + if (gamepad == device) { + IOHIDDeviceRegisterInputReportCallback( + device, gamepad.mInputReport.Elements(), 0, NULL, + gamepad.mInputReportContext.get()); + + gamepad.mInputReportContext.reset(); + + service->RemoveGamepad(gamepad.mHandle); + gamepad.clear(); + return; + } + } +} + +// Replace context to be Gamepad. +void DarwinGamepadService::ReportChangedCallback( + void* context, IOReturn result, void* sender, IOHIDReportType report_type, + uint32_t report_id, uint8_t* report, CFIndex report_length) { + if (context && report_type == kIOHIDReportTypeInput && report_length) { + auto reportContext = static_cast<GamepadInputReportContext*>(context); + DarwinGamepadService* service = reportContext->service; + service->mGamepads[reportContext->gamepadSlot].ReportChanged(report, + report_length); + } +} + +void Gamepad::ReportChanged(uint8_t* report, CFIndex report_len) { + MOZ_RELEASE_ASSERT(report_len <= mRemapper->GetMaxInputReportLength()); + mRemapper->ProcessTouchData(mHandle, report); +} + +size_t Gamepad::WriteOutputReport(const std::vector<uint8_t>& aReport) const { + IOReturn success = + IOHIDDeviceSetReport(mDevice, kIOHIDReportTypeOutput, aReport[0], + aReport.data(), aReport.size()); + + MOZ_ASSERT(success == kIOReturnSuccess); + return (success == kIOReturnSuccess) ? aReport.size() : 0; +} + +void DarwinGamepadService::InputValueChanged(IOHIDValueRef value) { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + uint32_t value_length = IOHIDValueGetLength(value); + if (value_length > 4) { + // Workaround for bizarre issue with PS3 controllers that try to return + // massive (30+ byte) values and crash IOHIDValueGetIntegerValue + return; + } + IOHIDElementRef element = IOHIDValueGetElement(value); + IOHIDDeviceRef device = IOHIDElementGetDevice(element); + + for (Gamepad& gamepad : mGamepads) { + if (gamepad == device) { + // Axis elements represent axes and d-pads. + if (Axis* axis = gamepad.lookupAxis(element)) { + const double d = IOHIDValueGetIntegerValue(value); + const double v = + 2.0f * (d - axis->min) / (double)(axis->max - axis->min) - 1.0f; + if (axis->value != v) { + gamepad.mRemapper->RemapAxisMoveEvent(gamepad.mHandle, axis->id, v); + axis->value = v; + } + } else if (Button* button = gamepad.lookupButton(element)) { + const int iv = IOHIDValueGetIntegerValue(value); + const bool pressed = iv != 0; + if (button->pressed != pressed) { + gamepad.mRemapper->RemapButtonEvent(gamepad.mHandle, button->id, + pressed); + button->pressed = pressed; + } + } + return; + } + } +} + +void DarwinGamepadService::DeviceAddedCallback(void* data, IOReturn result, + void* sender, + IOHIDDeviceRef device) { + DarwinGamepadService* service = (DarwinGamepadService*)data; + service->DeviceAdded(device); +} + +void DarwinGamepadService::DeviceRemovedCallback(void* data, IOReturn result, + void* sender, + IOHIDDeviceRef device) { + DarwinGamepadService* service = (DarwinGamepadService*)data; + service->DeviceRemoved(device); +} + +void DarwinGamepadService::InputValueChangedCallback(void* data, + IOReturn result, + void* sender, + IOHIDValueRef newValue) { + DarwinGamepadService* service = (DarwinGamepadService*)data; + service->InputValueChanged(newValue); +} + +void DarwinGamepadService::EventLoopOnceCallback(nsITimer* aTimer, + void* aClosure) { + DarwinGamepadService* service = static_cast<DarwinGamepadService*>(aClosure); + service->RunEventLoopOnce(); +} + +static CFMutableDictionaryRef MatchingDictionary(UInt32 inUsagePage, + UInt32 inUsage) { + CFMutableDictionaryRef dict = CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + if (!dict) return nullptr; + CFNumberRef number = + CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &inUsagePage); + if (!number) { + CFRelease(dict); + return nullptr; + } + CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsagePageKey), number); + CFRelease(number); + + number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &inUsage); + if (!number) { + CFRelease(dict); + return nullptr; + } + CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsageKey), number); + CFRelease(number); + + return dict; +} + +DarwinGamepadService::DarwinGamepadService() + : mManager(nullptr), mIsRunning(false) {} + +DarwinGamepadService::~DarwinGamepadService() { + if (mManager != nullptr) CFRelease(mManager); + mMonitorThread = nullptr; + mBackgroundThread = nullptr; + if (mPollingTimer) { + mPollingTimer->Cancel(); + mPollingTimer = nullptr; + } +} + +void DarwinGamepadService::RunEventLoopOnce() { + MOZ_ASSERT(NS_GetCurrentThread() == mMonitorThread); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true); + + // This timer must be created in monitor thread + if (!mPollingTimer) { + mPollingTimer = NS_NewTimer(); + } + mPollingTimer->Cancel(); + if (mIsRunning) { + mPollingTimer->InitWithNamedFuncCallback( + EventLoopOnceCallback, this, kDarwinGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT, "EventLoopOnceCallback"); + } else { + // We schedule a task shutdown and cleaning up resources to Background + // thread here to make sure no runloop is running to prevent potential race + // condition. + RefPtr<Runnable> shutdownTask = new DarwinGamepadServiceShutdownRunnable(); + mBackgroundThread->Dispatch(shutdownTask.forget(), NS_DISPATCH_NORMAL); + } +} + +void DarwinGamepadService::StartupInternal() { + if (mManager != nullptr) return; + + IOHIDManagerRef manager = + IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + + CFMutableDictionaryRef criteria_arr[2]; + criteria_arr[0] = MatchingDictionary(kDesktopUsagePage, kJoystickUsage); + if (!criteria_arr[0]) { + CFRelease(manager); + return; + } + + criteria_arr[1] = MatchingDictionary(kDesktopUsagePage, kGamepadUsage); + if (!criteria_arr[1]) { + CFRelease(criteria_arr[0]); + CFRelease(manager); + return; + } + + CFArrayRef criteria = CFArrayCreate(kCFAllocatorDefault, + (const void**)criteria_arr, 2, nullptr); + if (!criteria) { + CFRelease(criteria_arr[1]); + CFRelease(criteria_arr[0]); + CFRelease(manager); + return; + } + + IOHIDManagerSetDeviceMatchingMultiple(manager, criteria); + CFRelease(criteria); + CFRelease(criteria_arr[1]); + CFRelease(criteria_arr[0]); + + IOHIDManagerRegisterDeviceMatchingCallback(manager, DeviceAddedCallback, + this); + IOHIDManagerRegisterDeviceRemovalCallback(manager, DeviceRemovedCallback, + this); + IOHIDManagerRegisterInputValueCallback(manager, InputValueChangedCallback, + this); + IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode); + IOReturn rv = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone); + if (rv != kIOReturnSuccess) { + CFRelease(manager); + return; + } + + mManager = manager; + + mIsRunning = true; + RunEventLoopOnce(); +} + +void DarwinGamepadService::Startup() { + mBackgroundThread = NS_GetCurrentThread(); + Unused << NS_NewNamedThread("Gamepad", getter_AddRefs(mMonitorThread), + new DarwinGamepadServiceStartupRunnable(this)); +} + +void DarwinGamepadService::Shutdown() { + // Flipping this flag will stop the eventloop in Monitor thread + // and dispatch a task destroying and cleaning up resources in + // Background thread + mIsRunning = false; +} + +void DarwinGamepadService::SetLightIndicatorColor( + const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, const uint8_t& aRed, + const uint8_t& aGreen, const uint8_t& aBlue) { + // We get aControllerIdx from GamepadPlatformService::AddGamepad(), + // It begins from 1 and is stored at Gamepad.id. + const Gamepad* gamepad = MOZ_FIND_AND_VALIDATE( + aGamepadHandle, list_item.mHandle == aGamepadHandle, mGamepads); + if (!gamepad) { + MOZ_ASSERT(false); + return; + } + + RefPtr<GamepadRemapper> remapper = gamepad->mRemapper; + if (!remapper || + MOZ_IS_VALID(aLightColorIndex, + remapper->GetLightIndicatorCount() <= aLightColorIndex)) { + MOZ_ASSERT(false); + return; + } + + std::vector<uint8_t> report; + remapper->GetLightColorReport(aRed, aGreen, aBlue, report); + gamepad->WriteOutputReport(report); +} + +} // namespace + +namespace mozilla { +namespace dom { + +void StartGamepadMonitoring() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + if (gService) { + return; + } + + gService = new DarwinGamepadService(); + gService->Startup(); +} + +void StopGamepadMonitoring() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + if (!gService) { + return; + } + + // Calling Shutdown() will delete gService as well + gService->Shutdown(); +} + +void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, + const uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue) { + MOZ_ASSERT(gService); + if (!gService) { + return; + } + gService->SetLightIndicatorColor(aGamepadHandle, aLightColorIndex, aRed, + aGreen, aBlue); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/fallback/FallbackGamepad.cpp b/dom/gamepad/fallback/FallbackGamepad.cpp new file mode 100644 index 0000000000..4b37732f51 --- /dev/null +++ b/dom/gamepad/fallback/FallbackGamepad.cpp @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdint.h> +#include "mozilla/dom/GamepadHandle.h" + +namespace mozilla { +namespace dom { + +void StartGamepadMonitoring() {} + +void StopGamepadMonitoring() {} + +void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>&, + const Tainted<uint32_t>&, const uint8_t&, + const uint8_t&, const uint8_t&) {} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/ipc/GamepadEventChannelChild.cpp b/dom/gamepad/ipc/GamepadEventChannelChild.cpp new file mode 100644 index 0000000000..ef8528f565 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventChannelChild.cpp @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "GamepadEventChannelChild.h" +#include "mozilla/dom/GamepadManager.h" +#include "mozilla/dom/Promise.h" + +namespace mozilla::dom { + +namespace { + +class GamepadUpdateRunnable final : public Runnable { + public: + explicit GamepadUpdateRunnable(const GamepadChangeEvent& aGamepadEvent) + : Runnable("dom::GamepadUpdateRunnable"), mEvent(aGamepadEvent) {} + NS_IMETHOD Run() override { + RefPtr<GamepadManager> svc(GamepadManager::GetService()); + if (svc) { + svc->Update(mEvent); + } + return NS_OK; + } + + protected: + GamepadChangeEvent mEvent; +}; + +} // namespace + +already_AddRefed<GamepadEventChannelChild> GamepadEventChannelChild::Create() { + return RefPtr<GamepadEventChannelChild>(new GamepadEventChannelChild()) + .forget(); +} + +mozilla::ipc::IPCResult GamepadEventChannelChild::RecvGamepadUpdate( + const GamepadChangeEvent& aGamepadEvent) { + DebugOnly<nsresult> rv = + NS_DispatchToMainThread(new GamepadUpdateRunnable(aGamepadEvent)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed"); + return IPC_OK(); +} + +void GamepadEventChannelChild::AddPromise(const uint32_t& aID, + dom::Promise* aPromise) { + MOZ_ASSERT(!mPromiseList.Contains(aID)); + mPromiseList.InsertOrUpdate(aID, RefPtr{aPromise}); +} + +mozilla::ipc::IPCResult GamepadEventChannelChild::RecvReplyGamepadPromise( + const uint32_t& aPromiseID) { + RefPtr<dom::Promise> p; + if (!mPromiseList.Get(aPromiseID, getter_AddRefs(p))) { + MOZ_CRASH("We should always have a promise."); + } + + p->MaybeResolve(true); + mPromiseList.Remove(aPromiseID); + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/ipc/GamepadEventChannelChild.h b/dom/gamepad/ipc/GamepadEventChannelChild.h new file mode 100644 index 0000000000..97fd0576a4 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventChannelChild.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "mozilla/dom/PGamepadEventChannelChild.h" +#include "nsRefPtrHashtable.h" + +#ifndef mozilla_dom_GamepadEventChannelChild_h_ +# define mozilla_dom_GamepadEventChannelChild_h_ + +namespace mozilla::dom { + +class GamepadEventChannelChild final : public PGamepadEventChannelChild { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadEventChannelChild, override) + + static already_AddRefed<GamepadEventChannelChild> Create(); + + mozilla::ipc::IPCResult RecvGamepadUpdate( + const GamepadChangeEvent& aGamepadEvent); + mozilla::ipc::IPCResult RecvReplyGamepadPromise(const uint32_t& aPromiseID); + void AddPromise(const uint32_t& aID, dom::Promise* aPromise); + + GamepadEventChannelChild(const GamepadEventChannelChild&) = delete; + GamepadEventChannelChild(GamepadEventChannelChild&&) = delete; + GamepadEventChannelChild& operator=(const GamepadEventChannelChild&) = delete; + GamepadEventChannelChild& operator=(GamepadEventChannelChild&&) = delete; + + private: + GamepadEventChannelChild() = default; + ~GamepadEventChannelChild() = default; + + nsRefPtrHashtable<nsUint32HashKey, dom::Promise> mPromiseList; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/gamepad/ipc/GamepadEventChannelParent.cpp b/dom/gamepad/ipc/GamepadEventChannelParent.cpp new file mode 100644 index 0000000000..4a21948c06 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventChannelParent.cpp @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "GamepadEventChannelParent.h" +#include "GamepadPlatformService.h" +#include "mozilla/dom/GamepadMonitoring.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +using namespace mozilla::ipc; + +namespace { + +class SendGamepadUpdateRunnable final : public Runnable { + private: + ~SendGamepadUpdateRunnable() = default; + RefPtr<GamepadEventChannelParent> mParent; + GamepadChangeEvent mEvent; + + public: + SendGamepadUpdateRunnable(GamepadEventChannelParent* aParent, + GamepadChangeEvent aEvent) + : Runnable("dom::SendGamepadUpdateRunnable"), + mParent(aParent), + mEvent(aEvent) { + MOZ_ASSERT(mParent); + } + NS_IMETHOD Run() override { + AssertIsOnBackgroundThread(); + Unused << mParent->SendGamepadUpdate(mEvent); + return NS_OK; + } +}; + +} // namespace + +already_AddRefed<GamepadEventChannelParent> +GamepadEventChannelParent::Create() { + return RefPtr<GamepadEventChannelParent>(new GamepadEventChannelParent()) + .forget(); +} + +GamepadEventChannelParent::GamepadEventChannelParent() : mIsShutdown{false} { + MOZ_DIAGNOSTIC_ASSERT(IsOnBackgroundThread()); + + mBackgroundEventTarget = GetCurrentSerialEventTarget(); + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + + service->AddChannelParent(this); +} + +void GamepadEventChannelParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_DIAGNOSTIC_ASSERT(IsOnBackgroundThread()); + MOZ_DIAGNOSTIC_ASSERT(!mIsShutdown); + + mIsShutdown = true; + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + service->RemoveChannelParent(this); +} + +mozilla::ipc::IPCResult GamepadEventChannelParent::RecvVibrateHaptic( + const Tainted<GamepadHandle>& aHandle, + const Tainted<uint32_t>& aHapticIndex, const Tainted<double>& aIntensity, + const Tainted<double>& aDuration, const uint32_t& aPromiseID) { + // TODO: Bug 680289, implement for standard gamepads + + if (SendReplyGamepadPromise(aPromiseID)) { + return IPC_OK(); + } + + return IPC_FAIL(this, "SendReplyGamepadPromise fail."); +} + +mozilla::ipc::IPCResult GamepadEventChannelParent::RecvStopVibrateHaptic( + const Tainted<GamepadHandle>& aHandle) { + // TODO: Bug 680289, implement for standard gamepads + return IPC_OK(); +} + +mozilla::ipc::IPCResult GamepadEventChannelParent::RecvLightIndicatorColor( + const Tainted<GamepadHandle>& aHandle, + const Tainted<uint32_t>& aLightColorIndex, const uint8_t& aRed, + const uint8_t& aGreen, const uint8_t& aBlue, const uint32_t& aPromiseID) { + SetGamepadLightIndicatorColor(aHandle, aLightColorIndex, aRed, aGreen, aBlue); + + if (SendReplyGamepadPromise(aPromiseID)) { + return IPC_OK(); + } + + return IPC_FAIL(this, "SendReplyGamepadPromise fail."); +} + +void GamepadEventChannelParent::DispatchUpdateEvent( + const GamepadChangeEvent& aEvent) { + mBackgroundEventTarget->Dispatch(new SendGamepadUpdateRunnable(this, aEvent), + NS_DISPATCH_NORMAL); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/ipc/GamepadEventChannelParent.h b/dom/gamepad/ipc/GamepadEventChannelParent.h new file mode 100644 index 0000000000..90bb7e7f4d --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventChannelParent.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "mozilla/dom/PGamepadEventChannelParent.h" + +#ifndef mozilla_dom_GamepadEventChannelParent_h_ +# define mozilla_dom_GamepadEventChannelParent_h_ + +namespace mozilla::dom { + +class GamepadEventChannelParent final : public PGamepadEventChannelParent { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadEventChannelParent, override) + + static already_AddRefed<GamepadEventChannelParent> Create(); + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvVibrateHaptic( + const Tainted<GamepadHandle>& aHandle, + const Tainted<uint32_t>& aHapticIndex, const Tainted<double>& aIntensity, + const Tainted<double>& aDuration, const uint32_t& aPromiseID); + mozilla::ipc::IPCResult RecvStopVibrateHaptic( + const Tainted<GamepadHandle>& aHandle); + mozilla::ipc::IPCResult RecvLightIndicatorColor( + const Tainted<GamepadHandle>& aHandle, + const Tainted<uint32_t>& aLightColorIndex, const uint8_t& aRed, + const uint8_t& aGreen, const uint8_t& aBlue, const uint32_t& aPromiseID); + void DispatchUpdateEvent(const GamepadChangeEvent& aEvent); + + GamepadEventChannelParent(const GamepadEventChannelParent&) = delete; + GamepadEventChannelParent(GamepadEventChannelParent&&) = delete; + GamepadEventChannelParent& operator=(const GamepadEventChannelParent&) = + delete; + GamepadEventChannelParent& operator=(GamepadEventChannelParent&&) = delete; + + private: + GamepadEventChannelParent(); + ~GamepadEventChannelParent() = default; + + bool mIsShutdown; + nsCOMPtr<nsIEventTarget> mBackgroundEventTarget; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/gamepad/ipc/GamepadEventTypes.ipdlh b/dom/gamepad/ipc/GamepadEventTypes.ipdlh new file mode 100644 index 0000000000..4ac1d7dc93 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventTypes.ipdlh @@ -0,0 +1,78 @@ +/* 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/GamepadMessageUtils.h"; + +using mozilla::dom::GamepadPoseState from "mozilla/dom/GamepadPoseState.h"; +using mozilla::dom::GamepadTouchState from "mozilla/dom/GamepadTouchState.h"; +using mozilla::dom::GamepadLightIndicatorType from "mozilla/dom/GamepadLightIndicatorBinding.h"; +using mozilla::dom::GamepadMappingType from "mozilla/dom/GamepadBinding.h"; +using mozilla::dom::GamepadHand from "mozilla/dom/GamepadBinding.h"; +using mozilla::dom::GamepadHandle from "mozilla/dom/GamepadHandle.h"; + +namespace mozilla { +namespace dom { + +struct GamepadAdded { + nsString id; + GamepadMappingType mapping; + GamepadHand hand; + uint32_t display_id; + uint32_t num_buttons; + uint32_t num_axes; + uint32_t num_haptics; + uint32_t num_lights; + uint32_t num_touches; +}; + +struct GamepadRemoved {}; + +struct GamepadAxisInformation { + uint32_t axis; + double value; +}; + +struct GamepadButtonInformation { + uint32_t button; + double value; + bool pressed; + bool touched; +}; + +struct GamepadPoseInformation { + GamepadPoseState pose_state; +}; + +struct GamepadLightIndicatorTypeInformation { + uint32_t light; + GamepadLightIndicatorType type; +}; + +struct GamepadHandInformation { + GamepadHand hand; +}; + +struct GamepadTouchInformation { + uint32_t index; + GamepadTouchState touch_state; +}; + +union GamepadChangeEventBody { + GamepadAdded; + GamepadRemoved; + GamepadAxisInformation; + GamepadButtonInformation; + GamepadHandInformation; + GamepadLightIndicatorTypeInformation; + GamepadPoseInformation; + GamepadTouchInformation; +}; + +struct GamepadChangeEvent { + GamepadHandle handle; + GamepadChangeEventBody body; +}; + +} // namespace dom +} // namespace mozilla
\ No newline at end of file diff --git a/dom/gamepad/ipc/GamepadMessageUtils.h b/dom/gamepad/ipc/GamepadMessageUtils.h new file mode 100644 index 0000000000..c29293d3a5 --- /dev/null +++ b/dom/gamepad/ipc/GamepadMessageUtils.h @@ -0,0 +1,154 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_gamepad_GamepadMessageUtils_h +#define mozilla_dom_gamepad_GamepadMessageUtils_h + +#include "ipc/EnumSerializer.h" +#include "mozilla/dom/GamepadBinding.h" +#include "mozilla/dom/GamepadHandle.h" +#include "mozilla/dom/GamepadLightIndicatorBinding.h" +#include "mozilla/dom/GamepadPoseState.h" +#include "mozilla/dom/GamepadTouchState.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::GamepadLightIndicatorType> + : public ContiguousEnumSerializer< + mozilla::dom::GamepadLightIndicatorType, + mozilla::dom::GamepadLightIndicatorType(0), + mozilla::dom::GamepadLightIndicatorType( + mozilla::dom::GamepadLightIndicatorType::EndGuard_)> {}; + +template <> +struct ParamTraits<mozilla::dom::GamepadMappingType> + : public ContiguousEnumSerializer< + mozilla::dom::GamepadMappingType, mozilla::dom::GamepadMappingType(0), + mozilla::dom::GamepadMappingType( + mozilla::dom::GamepadMappingType::EndGuard_)> {}; + +template <> +struct ParamTraits<mozilla::dom::GamepadHand> + : public ContiguousEnumSerializer< + mozilla::dom::GamepadHand, mozilla::dom::GamepadHand(0), + mozilla::dom::GamepadHand(mozilla::dom::GamepadHand::EndGuard_)> {}; + +template <> +struct ParamTraits<mozilla::dom::GamepadCapabilityFlags> + : public BitFlagsEnumSerializer< + mozilla::dom::GamepadCapabilityFlags, + mozilla::dom::GamepadCapabilityFlags::Cap_All> {}; + +template <> +struct ParamTraits<mozilla::dom::GamepadPoseState> { + typedef mozilla::dom::GamepadPoseState paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.flags); + WriteParam(aWriter, aParam.orientation[0]); + WriteParam(aWriter, aParam.orientation[1]); + WriteParam(aWriter, aParam.orientation[2]); + WriteParam(aWriter, aParam.orientation[3]); + WriteParam(aWriter, aParam.position[0]); + WriteParam(aWriter, aParam.position[1]); + WriteParam(aWriter, aParam.position[2]); + WriteParam(aWriter, aParam.angularVelocity[0]); + WriteParam(aWriter, aParam.angularVelocity[1]); + WriteParam(aWriter, aParam.angularVelocity[2]); + WriteParam(aWriter, aParam.angularAcceleration[0]); + WriteParam(aWriter, aParam.angularAcceleration[1]); + WriteParam(aWriter, aParam.angularAcceleration[2]); + WriteParam(aWriter, aParam.linearVelocity[0]); + WriteParam(aWriter, aParam.linearVelocity[1]); + WriteParam(aWriter, aParam.linearVelocity[2]); + WriteParam(aWriter, aParam.linearAcceleration[0]); + WriteParam(aWriter, aParam.linearAcceleration[1]); + WriteParam(aWriter, aParam.linearAcceleration[2]); + WriteParam(aWriter, aParam.isPositionValid); + WriteParam(aWriter, aParam.isOrientationValid); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &(aResult->flags)) || + !ReadParam(aReader, &(aResult->orientation[0])) || + !ReadParam(aReader, &(aResult->orientation[1])) || + !ReadParam(aReader, &(aResult->orientation[2])) || + !ReadParam(aReader, &(aResult->orientation[3])) || + !ReadParam(aReader, &(aResult->position[0])) || + !ReadParam(aReader, &(aResult->position[1])) || + !ReadParam(aReader, &(aResult->position[2])) || + !ReadParam(aReader, &(aResult->angularVelocity[0])) || + !ReadParam(aReader, &(aResult->angularVelocity[1])) || + !ReadParam(aReader, &(aResult->angularVelocity[2])) || + !ReadParam(aReader, &(aResult->angularAcceleration[0])) || + !ReadParam(aReader, &(aResult->angularAcceleration[1])) || + !ReadParam(aReader, &(aResult->angularAcceleration[2])) || + !ReadParam(aReader, &(aResult->linearVelocity[0])) || + !ReadParam(aReader, &(aResult->linearVelocity[1])) || + !ReadParam(aReader, &(aResult->linearVelocity[2])) || + !ReadParam(aReader, &(aResult->linearAcceleration[0])) || + !ReadParam(aReader, &(aResult->linearAcceleration[1])) || + !ReadParam(aReader, &(aResult->linearAcceleration[2])) || + !ReadParam(aReader, &(aResult->isPositionValid)) || + !ReadParam(aReader, &(aResult->isOrientationValid))) { + return false; + } + return true; + } +}; + +template <> +struct ParamTraits<mozilla::dom::GamepadTouchState> { + typedef mozilla::dom::GamepadTouchState paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.touchId); + WriteParam(aWriter, aParam.surfaceId); + WriteParam(aWriter, aParam.position[0]); + WriteParam(aWriter, aParam.position[1]); + WriteParam(aWriter, aParam.surfaceDimensions[0]); + WriteParam(aWriter, aParam.surfaceDimensions[1]); + WriteParam(aWriter, aParam.isSurfaceDimensionsValid); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &(aResult->touchId)) || + !ReadParam(aReader, &(aResult->surfaceId)) || + !ReadParam(aReader, &(aResult->position[0])) || + !ReadParam(aReader, &(aResult->position[1])) || + !ReadParam(aReader, &(aResult->surfaceDimensions[0])) || + !ReadParam(aReader, &(aResult->surfaceDimensions[1])) || + !ReadParam(aReader, &(aResult->isSurfaceDimensionsValid))) { + return false; + } + return true; + } +}; + +template <> +struct ParamTraits<mozilla::dom::GamepadHandleKind> + : public ContiguousEnumSerializerInclusive< + mozilla::dom::GamepadHandleKind, + mozilla::dom::GamepadHandleKind::GamepadPlatformManager, + mozilla::dom::GamepadHandleKind::VR> {}; + +template <> +struct ParamTraits<mozilla::dom::GamepadHandle> { + typedef mozilla::dom::GamepadHandle paramType; + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mValue); + WriteParam(aWriter, aParam.mKind); + } + static bool Read(MessageReader* aReader, paramType* aParam) { + return ReadParam(aReader, &aParam->mValue) && + ReadParam(aReader, &aParam->mKind); + } +}; + +} // namespace IPC + +#endif // mozilla_dom_gamepad_GamepadMessageUtils_h diff --git a/dom/gamepad/ipc/GamepadTestChannelChild.cpp b/dom/gamepad/ipc/GamepadTestChannelChild.cpp new file mode 100644 index 0000000000..d74e0736ce --- /dev/null +++ b/dom/gamepad/ipc/GamepadTestChannelChild.cpp @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GamepadTestChannelChild.h" + +#include "mozilla/dom/GamepadServiceTest.h" + +namespace mozilla::dom { + +already_AddRefed<GamepadTestChannelChild> GamepadTestChannelChild::Create( + GamepadServiceTest* aGamepadServiceTest) { + return RefPtr<GamepadTestChannelChild>( + new GamepadTestChannelChild(aGamepadServiceTest)) + .forget(); +} + +GamepadTestChannelChild::GamepadTestChannelChild( + GamepadServiceTest* aGamepadServiceTest) + : mGamepadServiceTest(aGamepadServiceTest) {} + +mozilla::ipc::IPCResult GamepadTestChannelChild::RecvReplyGamepadHandle( + const uint32_t& aID, const GamepadHandle& aHandle) { + MOZ_RELEASE_ASSERT( + mGamepadServiceTest, + "Test channel should never outlive the owning GamepadServiceTest"); + + mGamepadServiceTest->ReplyGamepadHandle(aID, aHandle); + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/ipc/GamepadTestChannelChild.h b/dom/gamepad/ipc/GamepadTestChannelChild.h new file mode 100644 index 0000000000..26d499ff8a --- /dev/null +++ b/dom/gamepad/ipc/GamepadTestChannelChild.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GamepadTestChannelChild_h_ +#define mozilla_dom_GamepadTestChannelChild_h_ + +#include "mozilla/dom/PGamepadTestChannelChild.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/WeakPtr.h" +#include "nsRefPtrHashtable.h" + +namespace mozilla::dom { + +class GamepadServiceTest; + +class GamepadTestChannelChild final : public PGamepadTestChannelChild { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadTestChannelChild) + + static already_AddRefed<GamepadTestChannelChild> Create( + GamepadServiceTest* aGamepadServiceTest); + + GamepadTestChannelChild(const GamepadTestChannelChild&) = delete; + GamepadTestChannelChild(GamepadTestChannelChild&&) = delete; + GamepadTestChannelChild& operator=(const GamepadTestChannelChild&) = delete; + GamepadTestChannelChild& operator=(GamepadTestChannelChild&&) = delete; + + private: + explicit GamepadTestChannelChild(GamepadServiceTest* aGamepadServiceTest); + ~GamepadTestChannelChild() = default; + + mozilla::ipc::IPCResult RecvReplyGamepadHandle(const uint32_t& aID, + const GamepadHandle& aHandle); + + WeakPtr<GamepadServiceTest> mGamepadServiceTest; + + friend class PGamepadTestChannelChild; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/gamepad/ipc/GamepadTestChannelParent.cpp b/dom/gamepad/ipc/GamepadTestChannelParent.cpp new file mode 100644 index 0000000000..1c8d67491e --- /dev/null +++ b/dom/gamepad/ipc/GamepadTestChannelParent.cpp @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GamepadTestChannelParent.h" + +#include "mozilla/dom/GamepadPlatformService.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Unused.h" + +namespace mozilla::dom { + +already_AddRefed<GamepadTestChannelParent> GamepadTestChannelParent::Create() { + // Refuse to create the parent actor if this pref is disabled + if (!StaticPrefs::dom_gamepad_test_enabled()) { + return nullptr; + } + + return RefPtr<GamepadTestChannelParent>(new GamepadTestChannelParent()) + .forget(); +} + +GamepadTestChannelParent::GamepadTestChannelParent() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + GamepadMonitoringState::GetSingleton().AddObserver(this); +} + +GamepadTestChannelParent::~GamepadTestChannelParent() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + GamepadMonitoringState::GetSingleton().RemoveObserver(this); +} + +void GamepadTestChannelParent::AddGamepadToPlatformService( + uint32_t aPromiseId, const GamepadAdded& aGamepadAdded) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + + const GamepadAdded& a = aGamepadAdded; + nsCString gamepadID; + LossyCopyUTF16toASCII(a.id(), gamepadID); + GamepadHandle handle = service->AddGamepad( + gamepadID.get(), static_cast<GamepadMappingType>(a.mapping()), a.hand(), + a.num_buttons(), a.num_axes(), a.num_haptics(), a.num_lights(), + a.num_touches()); + + Unused << SendReplyGamepadHandle(aPromiseId, handle); +} + +void GamepadTestChannelParent::OnMonitoringStateChanged(bool aNewState) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (aNewState) { + for (auto& deferredGamepadAdd : mDeferredGamepadAdded) { + AddGamepadToPlatformService(deferredGamepadAdd.promiseId, + deferredGamepadAdd.gamepadAdded); + } + mDeferredGamepadAdded.Clear(); + } +} + +mozilla::ipc::IPCResult GamepadTestChannelParent::RecvGamepadTestEvent( + const uint32_t& aID, const GamepadChangeEvent& aEvent) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + + const GamepadChangeEventBody& body = aEvent.body(); + + // GamepadAdded is special because it will be deferred if monitoring hasn't + // started. Everything else won't be deferred and will fail if monitoring + // isn't running + if (body.type() == GamepadChangeEventBody::TGamepadAdded) { + if (GamepadMonitoringState::GetSingleton().IsMonitoring()) { + AddGamepadToPlatformService(aID, body.get_GamepadAdded()); + } else { + mDeferredGamepadAdded.AppendElement( + DeferredGamepadAdded{aID, body.get_GamepadAdded()}); + } + return IPC_OK(); + } + + if (!GamepadMonitoringState::GetSingleton().IsMonitoring()) { + return IPC_FAIL(this, "Simulated message received while not monitoring"); + } + + GamepadHandle handle = aEvent.handle(); + + switch (body.type()) { + case GamepadChangeEventBody::TGamepadRemoved: + service->RemoveGamepad(handle); + break; + case GamepadChangeEventBody::TGamepadButtonInformation: { + const GamepadButtonInformation& a = body.get_GamepadButtonInformation(); + service->NewButtonEvent(handle, a.button(), a.pressed(), a.touched(), + a.value()); + break; + } + case GamepadChangeEventBody::TGamepadAxisInformation: { + const GamepadAxisInformation& a = body.get_GamepadAxisInformation(); + service->NewAxisMoveEvent(handle, a.axis(), a.value()); + break; + } + case GamepadChangeEventBody::TGamepadPoseInformation: { + const GamepadPoseInformation& a = body.get_GamepadPoseInformation(); + service->NewPoseEvent(handle, a.pose_state()); + break; + } + case GamepadChangeEventBody::TGamepadTouchInformation: { + const GamepadTouchInformation& a = body.get_GamepadTouchInformation(); + service->NewMultiTouchEvent(handle, a.index(), a.touch_state()); + break; + } + default: + NS_WARNING("Unknown event type."); + return IPC_FAIL_NO_REASON(this); + } + Unused << SendReplyGamepadHandle(aID, handle); + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/ipc/GamepadTestChannelParent.h b/dom/gamepad/ipc/GamepadTestChannelParent.h new file mode 100644 index 0000000000..03567727e2 --- /dev/null +++ b/dom/gamepad/ipc/GamepadTestChannelParent.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/PGamepadTestChannelParent.h" +#include "mozilla/WeakPtr.h" + +#ifndef mozilla_dom_GamepadTestChannelParent_h_ +# define mozilla_dom_GamepadTestChannelParent_h_ + +namespace mozilla::dom { + +class GamepadTestChannelParent final : public PGamepadTestChannelParent, + public SupportsWeakPtr { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadTestChannelParent) + + static already_AddRefed<GamepadTestChannelParent> Create(); + + mozilla::ipc::IPCResult RecvGamepadTestEvent( + const uint32_t& aID, const GamepadChangeEvent& aGamepadEvent); + + void OnMonitoringStateChanged(bool aNewState); + + GamepadTestChannelParent(const GamepadTestChannelParent&) = delete; + GamepadTestChannelParent(GamepadTestChannelParent&&) = delete; + GamepadTestChannelParent& operator=(const GamepadTestChannelParent&) = delete; + GamepadTestChannelParent& operator=(GamepadTestChannelParent&&) = delete; + + private: + struct DeferredGamepadAdded { + uint32_t promiseId; + GamepadAdded gamepadAdded; + }; + + GamepadTestChannelParent(); + ~GamepadTestChannelParent(); + + void AddGamepadToPlatformService(uint32_t aPromiseId, + const GamepadAdded& aGamepadAdded); + + nsTArray<DeferredGamepadAdded> mDeferredGamepadAdded; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/gamepad/ipc/PGamepadEventChannel.ipdl b/dom/gamepad/ipc/PGamepadEventChannel.ipdl new file mode 100644 index 0000000000..dba6725bd4 --- /dev/null +++ b/dom/gamepad/ipc/PGamepadEventChannel.ipdl @@ -0,0 +1,31 @@ +/* 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 protocol PBackground; +include "mozilla/dom/GamepadMessageUtils.h"; +include GamepadEventTypes; + +using mozilla::dom::GamepadHandle from "mozilla/dom/GamepadHandle.h"; + +namespace mozilla { +namespace dom { + +protocol PGamepadEventChannel { + manager PBackground; + parent: + async __delete__(); + + [Tainted] async VibrateHaptic(GamepadHandle aHandle, uint32_t aHapticIndex, + double aIntensity, double aDuration, [NoTaint=passback] uint32_t aPromiseID); + [Tainted] async StopVibrateHaptic(GamepadHandle aHandle); + [Tainted] async LightIndicatorColor(GamepadHandle aHandle, uint32_t aLightColorIndex, + [NoTaint=allvalid] uint8_t aRed, [NoTaint=allvalid] uint8_t aGreen, + [NoTaint=allvalid] uint8_t aBlue, [NoTaint=passback] uint32_t aPromiseID); + + child: + async GamepadUpdate(GamepadChangeEvent aGamepadEvent); + async ReplyGamepadPromise(uint32_t aPromiseID); +}; + +} +} diff --git a/dom/gamepad/ipc/PGamepadTestChannel.ipdl b/dom/gamepad/ipc/PGamepadTestChannel.ipdl new file mode 100644 index 0000000000..2be0819491 --- /dev/null +++ b/dom/gamepad/ipc/PGamepadTestChannel.ipdl @@ -0,0 +1,23 @@ +/* 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 protocol PBackground; +include "mozilla/dom/GamepadMessageUtils.h"; +include GamepadEventTypes; + +using mozilla::dom::GamepadHandle from "mozilla/dom/GamepadHandle.h"; + +namespace mozilla { +namespace dom { + +protocol PGamepadTestChannel { + manager PBackground; + parent: + async GamepadTestEvent(uint32_t aID, GamepadChangeEvent aGamepadEvent); + async __delete__(); + child: + async ReplyGamepadHandle(uint32_t aID, GamepadHandle aHandle); +}; + +} +} diff --git a/dom/gamepad/linux/LinuxGamepad.cpp b/dom/gamepad/linux/LinuxGamepad.cpp new file mode 100644 index 0000000000..e7a7071b86 --- /dev/null +++ b/dom/gamepad/linux/LinuxGamepad.cpp @@ -0,0 +1,529 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * LinuxGamepadService: An evdev backend for the GamepadService. + * + * Ref: https://www.kernel.org/doc/html/latest/input/gamepad.html + */ +#include <algorithm> +#include <unordered_map> +#include <cstddef> + +#include <glib.h> +#include <linux/input.h> +#include <stdio.h> +#include <stdint.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include "nscore.h" +#include "mozilla/dom/GamepadHandle.h" +#include "mozilla/dom/GamepadPlatformService.h" +#include "mozilla/dom/GamepadRemapping.h" +#include "mozilla/Tainting.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Sprintf.h" +#include "udev.h" + +#define BITS_PER_LONG ((sizeof(unsigned long)) * 8) +#define BITS_TO_LONGS(x) (((x) + BITS_PER_LONG - 1) / BITS_PER_LONG) + +namespace { + +using namespace mozilla::dom; +using mozilla::MakeUnique; +using mozilla::udev_device; +using mozilla::udev_enumerate; +using mozilla::udev_lib; +using mozilla::udev_list_entry; +using mozilla::udev_monitor; +using mozilla::UniquePtr; + +static const char kEvdevPath[] = "/dev/input/event"; + +static inline bool TestBit(const unsigned long* arr, size_t bit) { + return !!(arr[bit / BITS_PER_LONG] & (1LL << (bit % BITS_PER_LONG))); +} + +static inline double ScaleAxis(const input_absinfo& info, int value) { + return 2.0 * (value - info.minimum) / (double)(info.maximum - info.minimum) - + 1.0; +} + +// TODO: should find a USB identifier for each device so we can +// provide something that persists across connect/disconnect cycles. +struct Gamepad { + GamepadHandle handle; + bool isStandardGamepad = false; + RefPtr<GamepadRemapper> remapper = nullptr; + guint source_id = UINT_MAX; + char idstring[256] = {0}; + char devpath[PATH_MAX] = {0}; + uint8_t key_map[KEY_MAX] = {0}; + uint8_t abs_map[ABS_MAX] = {0}; + std::unordered_map<uint16_t, input_absinfo> abs_info; +}; + +static inline bool LoadAbsInfo(int fd, Gamepad* gamepad, uint16_t code) { + input_absinfo info{0}; + if (ioctl(fd, EVIOCGABS(code), &info) < 0) { + return false; + } + if (info.minimum == info.maximum) { + return false; + } + gamepad->abs_info.emplace(code, std::move(info)); + return true; +} + +class LinuxGamepadService { + public: + LinuxGamepadService() : mMonitor(nullptr), mMonitorSourceID(0) {} + + void Startup(); + void Shutdown(); + + private: + void AddDevice(struct udev_device* dev); + void RemoveDevice(struct udev_device* dev); + void ScanForDevices(); + void AddMonitor(); + void RemoveMonitor(); + bool IsDeviceGamepad(struct udev_device* dev); + void ReadUdevChange(); + + // handler for data from /dev/input/eventN + static gboolean OnGamepadData(GIOChannel* source, GIOCondition condition, + gpointer data); + + // handler for data from udev monitor + static gboolean OnUdevMonitor(GIOChannel* source, GIOCondition condition, + gpointer data); + + udev_lib mUdev; + struct udev_monitor* mMonitor; + guint mMonitorSourceID; + // Information about currently connected gamepads. + AutoTArray<UniquePtr<Gamepad>, 4> mGamepads; +}; + +// singleton instance +LinuxGamepadService* gService = nullptr; + +void LinuxGamepadService::AddDevice(struct udev_device* dev) { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + const char* devpath = mUdev.udev_device_get_devnode(dev); + if (!devpath) { + return; + } + + // Ensure that this device hasn't already been added. + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + if (strcmp(mGamepads[i]->devpath, devpath) == 0) { + return; + } + } + + auto gamepad = MakeUnique<Gamepad>(); + snprintf(gamepad->devpath, sizeof(gamepad->devpath), "%s", devpath); + GIOChannel* channel = g_io_channel_new_file(devpath, "r", nullptr); + if (!channel) { + return; + } + + g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, nullptr); + g_io_channel_set_encoding(channel, nullptr, nullptr); + g_io_channel_set_buffered(channel, FALSE); + int fd = g_io_channel_unix_get_fd(channel); + + input_id id{0}; + if (ioctl(fd, EVIOCGID, &id) < 0) { + return; + } + + char name[128]{0}; + if (ioctl(fd, EVIOCGNAME(sizeof(name)), &name) < 0) { + strcpy(name, "Unknown Device"); + } + + SprintfLiteral(gamepad->idstring, "%04" PRIx16 "-%04" PRIx16 "-%s", id.vendor, + id.product, name); + + unsigned long keyBits[BITS_TO_LONGS(KEY_CNT)] = {0}; + unsigned long absBits[BITS_TO_LONGS(ABS_CNT)] = {0}; + if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBits)), keyBits) < 0 || + ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBits)), absBits) < 0) { + return; + } + + /* Here, we try to support even strange cases where proper semantic + * BTN_GAMEPAD button are combined with arbitrary extra buttons. */ + + /* These are mappings where the index is a CanonicalButtonIndex and the value + * is an evdev code */ + const std::array<uint16_t, BUTTON_INDEX_COUNT> kStandardButtons = { + /* BUTTON_INDEX_PRIMARY = */ BTN_SOUTH, + /* BUTTON_INDEX_SECONDARY = */ BTN_EAST, + /* BUTTON_INDEX_TERTIARY = */ BTN_WEST, + /* BUTTON_INDEX_QUATERNARY = */ BTN_NORTH, + /* BUTTON_INDEX_LEFT_SHOULDER = */ BTN_TL, + /* BUTTON_INDEX_RIGHT_SHOULDER = */ BTN_TR, + /* BUTTON_INDEX_LEFT_TRIGGER = */ BTN_TL2, + /* BUTTON_INDEX_RIGHT_TRIGGER = */ BTN_TR2, + /* BUTTON_INDEX_BACK_SELECT = */ BTN_SELECT, + /* BUTTON_INDEX_START = */ BTN_START, + /* BUTTON_INDEX_LEFT_THUMBSTICK = */ BTN_THUMBL, + /* BUTTON_INDEX_RIGHT_THUMBSTICK = */ BTN_THUMBR, + /* BUTTON_INDEX_DPAD_UP = */ BTN_DPAD_UP, + /* BUTTON_INDEX_DPAD_DOWN = */ BTN_DPAD_DOWN, + /* BUTTON_INDEX_DPAD_LEFT = */ BTN_DPAD_LEFT, + /* BUTTON_INDEX_DPAD_RIGHT = */ BTN_DPAD_RIGHT, + /* BUTTON_INDEX_META = */ BTN_MODE, + }; + const std::array<uint16_t, AXIS_INDEX_COUNT> kStandardAxes = { + /* AXIS_INDEX_LEFT_STICK_X = */ ABS_X, + /* AXIS_INDEX_LEFT_STICK_Y = */ ABS_Y, + /* AXIS_INDEX_RIGHT_STICK_X = */ ABS_RX, + /* AXIS_INDEX_RIGHT_STICK_Y = */ ABS_RY, + }; + + /* + * According to https://www.kernel.org/doc/html/latest/input/gamepad.html, + * "All gamepads that follow the protocol described here map BTN_GAMEPAD", + * so we can use it as a proxy for semantic buttons in general. If it's + * enabled, we're probably going to be acting like a standard gamepad + */ + uint32_t numButtons = 0; + if (TestBit(keyBits, BTN_GAMEPAD)) { + gamepad->isStandardGamepad = true; + for (uint8_t button = 0; button < BUTTON_INDEX_COUNT; button++) { + gamepad->key_map[kStandardButtons[button]] = button; + } + numButtons = BUTTON_INDEX_COUNT; + } + + // Now, go through the non-semantic buttons and handle them as extras + for (uint16_t key = 0; key < KEY_MAX; key++) { + // Skip standard buttons + if (gamepad->isStandardGamepad && + std::find(kStandardButtons.begin(), kStandardButtons.end(), key) != + kStandardButtons.end()) { + continue; + } + + if (TestBit(keyBits, key)) { + gamepad->key_map[key] = numButtons++; + } + } + + uint32_t numAxes = 0; + if (gamepad->isStandardGamepad) { + for (uint8_t i = 0; i < AXIS_INDEX_COUNT; i++) { + gamepad->abs_map[kStandardAxes[i]] = i; + LoadAbsInfo(fd, gamepad.get(), kStandardAxes[i]); + } + numAxes = AXIS_INDEX_COUNT; + + // These are not real axis and get remapped to buttons. + LoadAbsInfo(fd, gamepad.get(), ABS_HAT0X); + LoadAbsInfo(fd, gamepad.get(), ABS_HAT0Y); + } + + for (uint16_t i = 0; i < ABS_MAX; ++i) { + if (gamepad->isStandardGamepad && + (std::find(kStandardAxes.begin(), kStandardAxes.end(), i) != + kStandardAxes.end() || + i == ABS_HAT0X || i == ABS_HAT0Y)) { + continue; + } + + if (TestBit(absBits, i)) { + if (LoadAbsInfo(fd, gamepad.get(), i)) { + gamepad->abs_map[i] = numAxes++; + } + } + } + + if (numAxes == 0) { + NS_WARNING("Gamepad with zero axes detected?"); + } + if (numButtons == 0) { + NS_WARNING("Gamepad with zero buttons detected?"); + } + + // NOTE: This almost always true, so we basically never use the remapping + // code. + if (gamepad->isStandardGamepad) { + gamepad->handle = + service->AddGamepad(gamepad->idstring, GamepadMappingType::Standard, + GamepadHand::_empty, numButtons, numAxes, 0, 0, 0); + } else { + bool defaultRemapper = false; + RefPtr<GamepadRemapper> remapper = + GetGamepadRemapper(id.vendor, id.product, defaultRemapper); + MOZ_ASSERT(remapper); + remapper->SetAxisCount(numAxes); + remapper->SetButtonCount(numButtons); + + gamepad->handle = service->AddGamepad( + gamepad->idstring, remapper->GetMappingType(), GamepadHand::_empty, + remapper->GetButtonCount(), remapper->GetAxisCount(), 0, + remapper->GetLightIndicatorCount(), remapper->GetTouchEventCount()); + gamepad->remapper = remapper.forget(); + } + // TODO: Bug 680289, implement gamepad haptics for Linux. + // TODO: Bug 1523355, implement gamepad lighindicator and touch for Linux. + + gamepad->source_id = + g_io_add_watch(channel, GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), + OnGamepadData, gamepad.get()); + g_io_channel_unref(channel); + + mGamepads.AppendElement(std::move(gamepad)); +} + +void LinuxGamepadService::RemoveDevice(struct udev_device* dev) { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + const char* devpath = mUdev.udev_device_get_devnode(dev); + if (!devpath) { + return; + } + + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + if (strcmp(mGamepads[i]->devpath, devpath) == 0) { + auto gamepad = std::move(mGamepads[i]); + mGamepads.RemoveElementAt(i); + + g_source_remove(gamepad->source_id); + service->RemoveGamepad(gamepad->handle); + + break; + } + } +} + +void LinuxGamepadService::ScanForDevices() { + struct udev_enumerate* en = mUdev.udev_enumerate_new(mUdev.udev); + mUdev.udev_enumerate_add_match_subsystem(en, "input"); + mUdev.udev_enumerate_scan_devices(en); + + struct udev_list_entry* dev_list_entry; + for (dev_list_entry = mUdev.udev_enumerate_get_list_entry(en); + dev_list_entry != nullptr; + dev_list_entry = mUdev.udev_list_entry_get_next(dev_list_entry)) { + const char* path = mUdev.udev_list_entry_get_name(dev_list_entry); + struct udev_device* dev = + mUdev.udev_device_new_from_syspath(mUdev.udev, path); + if (IsDeviceGamepad(dev)) { + AddDevice(dev); + } + + mUdev.udev_device_unref(dev); + } + + mUdev.udev_enumerate_unref(en); +} + +void LinuxGamepadService::AddMonitor() { + // Add a monitor to watch for device changes + mMonitor = mUdev.udev_monitor_new_from_netlink(mUdev.udev, "udev"); + if (!mMonitor) { + // Not much we can do here. + return; + } + mUdev.udev_monitor_filter_add_match_subsystem_devtype(mMonitor, "input", + nullptr); + + int monitor_fd = mUdev.udev_monitor_get_fd(mMonitor); + GIOChannel* monitor_channel = g_io_channel_unix_new(monitor_fd); + mMonitorSourceID = g_io_add_watch(monitor_channel, + GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), + OnUdevMonitor, nullptr); + g_io_channel_unref(monitor_channel); + + mUdev.udev_monitor_enable_receiving(mMonitor); +} + +void LinuxGamepadService::RemoveMonitor() { + if (mMonitorSourceID) { + g_source_remove(mMonitorSourceID); + mMonitorSourceID = 0; + } + if (mMonitor) { + mUdev.udev_monitor_unref(mMonitor); + mMonitor = nullptr; + } +} + +void LinuxGamepadService::Startup() { + // Don't bother starting up if libudev couldn't be loaded or initialized. + if (!mUdev) { + return; + } + + AddMonitor(); + ScanForDevices(); +} + +void LinuxGamepadService::Shutdown() { + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + g_source_remove(mGamepads[i]->source_id); + } + mGamepads.Clear(); + RemoveMonitor(); +} + +bool LinuxGamepadService::IsDeviceGamepad(struct udev_device* aDev) { + if (!mUdev.udev_device_get_property_value(aDev, "ID_INPUT_JOYSTICK")) { + return false; + } + + const char* devpath = mUdev.udev_device_get_devnode(aDev); + if (!devpath) { + return false; + } + + return strncmp(devpath, kEvdevPath, strlen(kEvdevPath)) == 0; +} + +void LinuxGamepadService::ReadUdevChange() { + struct udev_device* dev = mUdev.udev_monitor_receive_device(mMonitor); + if (IsDeviceGamepad(dev)) { + const char* action = mUdev.udev_device_get_action(dev); + if (strcmp(action, "add") == 0) { + AddDevice(dev); + } else if (strcmp(action, "remove") == 0) { + RemoveDevice(dev); + } + } + mUdev.udev_device_unref(dev); +} + +// static +gboolean LinuxGamepadService::OnGamepadData(GIOChannel* source, + GIOCondition condition, + gpointer data) { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return TRUE; + } + auto* gamepad = static_cast<Gamepad*>(data); + + // TODO: remove gamepad? + if (condition & (G_IO_ERR | G_IO_HUP)) { + return FALSE; + } + + while (true) { + struct input_event event {}; + gsize count; + GError* err = nullptr; + if (g_io_channel_read_chars(source, (gchar*)&event, sizeof(event), &count, + &err) != G_IO_STATUS_NORMAL || + count == 0) { + break; + } + + switch (event.type) { + case EV_KEY: + if (gamepad->isStandardGamepad) { + service->NewButtonEvent(gamepad->handle, gamepad->key_map[event.code], + !!event.value); + } else { + gamepad->remapper->RemapButtonEvent( + gamepad->handle, gamepad->key_map[event.code], !!event.value); + } + break; + case EV_ABS: { + if (!gamepad->abs_info.count(event.code)) { + continue; + } + + double scaledValue = + ScaleAxis(gamepad->abs_info[event.code], event.value); + if (gamepad->isStandardGamepad) { + switch (event.code) { + case ABS_HAT0X: + service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_LEFT, + AxisNegativeAsButton(scaledValue)); + service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_RIGHT, + AxisPositiveAsButton(scaledValue)); + break; + case ABS_HAT0Y: + service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_UP, + AxisNegativeAsButton(scaledValue)); + service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_DOWN, + AxisPositiveAsButton(scaledValue)); + break; + default: + service->NewAxisMoveEvent( + gamepad->handle, gamepad->abs_map[event.code], scaledValue); + break; + } + } else { + gamepad->remapper->RemapAxisMoveEvent( + gamepad->handle, gamepad->abs_map[event.code], scaledValue); + } + } break; + } + } + + return TRUE; +} + +// static +gboolean LinuxGamepadService::OnUdevMonitor(GIOChannel* source, + GIOCondition condition, + gpointer data) { + if (condition & (G_IO_ERR | G_IO_HUP)) { + return FALSE; + } + + gService->ReadUdevChange(); + return TRUE; +} + +} // namespace + +namespace mozilla::dom { + +void StartGamepadMonitoring() { + if (gService) { + return; + } + gService = new LinuxGamepadService(); + gService->Startup(); +} + +void StopGamepadMonitoring() { + if (!gService) { + return; + } + gService->Shutdown(); + delete gService; + gService = nullptr; +} + +void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, + const uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue) { + // TODO: Bug 1523355. + NS_WARNING("Linux doesn't support gamepad light indicator."); +} + +} // namespace mozilla::dom diff --git a/dom/gamepad/linux/udev.h b/dom/gamepad/linux/udev.h new file mode 100644 index 0000000000..4e3f0b7865 --- /dev/null +++ b/dom/gamepad/linux/udev.h @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file defines a wrapper around libudev so we can avoid + * linking directly to it and use dlopen instead. + */ + +#ifndef HAL_LINUX_UDEV_H_ +#define HAL_LINUX_UDEV_H_ + +#include <dlfcn.h> + +#include "mozilla/ArrayUtils.h" + +namespace mozilla { + +struct udev; +struct udev_device; +struct udev_enumerate; +struct udev_list_entry; +struct udev_monitor; + +class udev_lib { + public: + udev_lib() : lib(nullptr), udev(nullptr) { + // Be careful about ABI compat! 0 -> 1 didn't change any + // symbols this code relies on, per: + // https://lists.fedoraproject.org/pipermail/devel/2012-June/168227.html + const char* lib_names[] = {"libudev.so.0", "libudev.so.1"}; + // Check whether a library is already loaded so we don't load two + // conflicting libs. + for (unsigned i = 0; i < ArrayLength(lib_names); i++) { + lib = dlopen(lib_names[i], RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + if (lib) { + break; + } + } + // If nothing loads the first time through, it means no version of libudev + // was already loaded. + if (!lib) { + for (unsigned i = 0; i < ArrayLength(lib_names); i++) { + lib = dlopen(lib_names[i], RTLD_LAZY | RTLD_GLOBAL); + if (lib) { + break; + } + } + } + if (lib && LoadSymbols()) { + udev = udev_new(); + } + } + + ~udev_lib() { + if (udev) { + udev_unref(udev); + } + + if (lib) { + dlclose(lib); + } + } + + explicit operator bool() { return udev; } + + private: +#define DLSYM(s) \ + do { \ + (s) = (decltype(s))dlsym(lib, #s); \ + if (!(s)) return false; \ + } while (0) + + bool LoadSymbols() { + DLSYM(udev_new); + DLSYM(udev_unref); + DLSYM(udev_device_unref); + DLSYM(udev_device_new_from_syspath); + DLSYM(udev_device_get_devnode); + DLSYM(udev_device_get_parent_with_subsystem_devtype); + DLSYM(udev_device_get_property_value); + DLSYM(udev_device_get_action); + DLSYM(udev_device_get_sysattr_value); + DLSYM(udev_enumerate_new); + DLSYM(udev_enumerate_unref); + DLSYM(udev_enumerate_add_match_subsystem); + DLSYM(udev_enumerate_scan_devices); + DLSYM(udev_enumerate_get_list_entry); + DLSYM(udev_list_entry_get_next); + DLSYM(udev_list_entry_get_name); + DLSYM(udev_monitor_new_from_netlink); + DLSYM(udev_monitor_filter_add_match_subsystem_devtype); + DLSYM(udev_monitor_enable_receiving); + DLSYM(udev_monitor_get_fd); + DLSYM(udev_monitor_receive_device); + DLSYM(udev_monitor_unref); + + return true; + } + +#undef DLSYM + + void* lib; + + public: + struct udev* udev; + + // Function pointers returned from dlsym. + struct udev* (*udev_new)(void); + void (*udev_unref)(struct udev*); + + void (*udev_device_unref)(struct udev_device*); + struct udev_device* (*udev_device_new_from_syspath)(struct udev*, + const char*); + const char* (*udev_device_get_devnode)(struct udev_device*); + struct udev_device* (*udev_device_get_parent_with_subsystem_devtype)( + struct udev_device*, const char*, const char*); + const char* (*udev_device_get_property_value)(struct udev_device*, + const char*); + const char* (*udev_device_get_action)(struct udev_device*); + const char* (*udev_device_get_sysattr_value)(struct udev_device*, + const char*); + + struct udev_enumerate* (*udev_enumerate_new)(struct udev*); + void (*udev_enumerate_unref)(struct udev_enumerate*); + int (*udev_enumerate_add_match_subsystem)(struct udev_enumerate*, + const char*); + int (*udev_enumerate_scan_devices)(struct udev_enumerate*); + struct udev_list_entry* (*udev_enumerate_get_list_entry)( + struct udev_enumerate*); + + struct udev_list_entry* (*udev_list_entry_get_next)(struct udev_list_entry*); + const char* (*udev_list_entry_get_name)(struct udev_list_entry*); + + struct udev_monitor* (*udev_monitor_new_from_netlink)(struct udev*, + const char*); + int (*udev_monitor_filter_add_match_subsystem_devtype)(struct udev_monitor*, + const char*, + const char*); + int (*udev_monitor_enable_receiving)(struct udev_monitor*); + int (*udev_monitor_get_fd)(struct udev_monitor*); + struct udev_device* (*udev_monitor_receive_device)(struct udev_monitor*); + void (*udev_monitor_unref)(struct udev_monitor*); +}; + +} // namespace mozilla + +#endif // HAL_LINUX_UDEV_H_ diff --git a/dom/gamepad/moz.build b/dom/gamepad/moz.build new file mode 100644 index 0000000000..db77ef8da6 --- /dev/null +++ b/dom/gamepad/moz.build @@ -0,0 +1,79 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + +IPDL_SOURCES += [ + "ipc/GamepadEventTypes.ipdlh", + "ipc/PGamepadEventChannel.ipdl", + "ipc/PGamepadTestChannel.ipdl", +] + +EXPORTS.mozilla.dom += [ + "Gamepad.h", + "GamepadButton.h", + "GamepadHandle.h", + "GamepadHapticActuator.h", + "GamepadLightIndicator.h", + "GamepadManager.h", + "GamepadMonitoring.h", + "GamepadPlatformService.h", + "GamepadPose.h", + "GamepadPoseState.h", + "GamepadRemapping.h", + "GamepadServiceTest.h", + "GamepadTouch.h", + "GamepadTouchState.h", + "ipc/GamepadEventChannelChild.h", + "ipc/GamepadEventChannelParent.h", + "ipc/GamepadMessageUtils.h", + "ipc/GamepadTestChannelChild.h", + "ipc/GamepadTestChannelParent.h", +] + +UNIFIED_SOURCES = [ + "Gamepad.cpp", + "GamepadButton.cpp", + "GamepadHandle.cpp", + "GamepadHapticActuator.cpp", + "GamepadLightIndicator.cpp", + "GamepadManager.cpp", + "GamepadPlatformService.cpp", + "GamepadPose.cpp", + "GamepadRemapping.cpp", + "GamepadServiceTest.cpp", + "GamepadTouch.cpp", + "ipc/GamepadEventChannelChild.cpp", + "ipc/GamepadEventChannelParent.cpp", + "ipc/GamepadTestChannelChild.cpp", + "ipc/GamepadTestChannelParent.cpp", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + UNIFIED_SOURCES += ["cocoa/CocoaGamepad.cpp"] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + UNIFIED_SOURCES += ["windows/WindowsGamepad.cpp"] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + UNIFIED_SOURCES += ["android/AndroidGamepad.cpp"] +elif CONFIG["OS_ARCH"] in ("Linux", "FreeBSD", "DragonFly"): + UNIFIED_SOURCES += ["linux/LinuxGamepad.cpp"] +else: + UNIFIED_SOURCES += ["fallback/FallbackGamepad.cpp"] + +LOCAL_INCLUDES += [ + "ipc", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/base", +] + +CFLAGS += CONFIG["GLIB_CFLAGS"] +CXXFLAGS += CONFIG["GLIB_CFLAGS"] diff --git a/dom/gamepad/windows/WindowsGamepad.cpp b/dom/gamepad/windows/WindowsGamepad.cpp new file mode 100644 index 0000000000..d5584eaea8 --- /dev/null +++ b/dom/gamepad/windows/WindowsGamepad.cpp @@ -0,0 +1,1136 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <algorithm> +#include <cstddef> + +#ifndef UNICODE +# define UNICODE +#endif +#include <windows.h> +#include <hidsdi.h> +#include <stdio.h> +#include <xinput.h> + +#include "nsITimer.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsWindowsHelpers.h" + +#include "mozilla/ArrayUtils.h" + +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/dom/GamepadPlatformService.h" +#include "mozilla/dom/GamepadRemapping.h" +#include "Gamepad.h" + +namespace { + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::ArrayLength; + +// USB HID usage tables, page 1, 0x30 = X +const uint32_t kAxisMinimumUsageNumber = 0x30; +// USB HID usage tables, page 1 (Hat switch) +const uint32_t kAxesLengthCap = 16; + +// USB HID usage tables +const uint32_t kDesktopUsagePage = 0x1; +const uint32_t kButtonUsagePage = 0x9; + +// Multiple devices-changed notifications can be sent when a device +// is connected, because USB devices consist of multiple logical devices. +// Therefore, we wait a bit after receiving one before looking for +// device changes. +const uint32_t kDevicesChangedStableDelay = 200; +// Both DirectInput and XInput are polling-driven here, +// so we need to poll it periodically. +// 4ms, or 250 Hz, is consistent with Chrome's gamepad implementation. +const uint32_t kWindowsGamepadPollInterval = 4; + +const UINT kRawInputError = (UINT)-1; + +// In XInputGetState, we can't get the state of Xbox Guide button. +// We need to go through the undocumented XInputGetStateEx method +// to get that button's state. +const LPCSTR kXInputGetStateExOrdinal = (LPCSTR)100; +// Bitmask for the Guide button in XInputGamepadEx.wButtons. +const int XINPUT_GAMEPAD_Guide = 0x0400; + +#ifndef XUSER_MAX_COUNT +# define XUSER_MAX_COUNT 4 +#endif + +const struct { + int usagePage; + int usage; +} kUsagePages[] = { + // USB HID usage tables, page 1 + {kDesktopUsagePage, 4}, // Joystick + {kDesktopUsagePage, 5} // Gamepad +}; + +const struct { + WORD button; + int mapped; +} kXIButtonMap[] = {{XINPUT_GAMEPAD_DPAD_UP, 12}, + {XINPUT_GAMEPAD_DPAD_DOWN, 13}, + {XINPUT_GAMEPAD_DPAD_LEFT, 14}, + {XINPUT_GAMEPAD_DPAD_RIGHT, 15}, + {XINPUT_GAMEPAD_START, 9}, + {XINPUT_GAMEPAD_BACK, 8}, + {XINPUT_GAMEPAD_LEFT_THUMB, 10}, + {XINPUT_GAMEPAD_RIGHT_THUMB, 11}, + {XINPUT_GAMEPAD_LEFT_SHOULDER, 4}, + {XINPUT_GAMEPAD_RIGHT_SHOULDER, 5}, + {XINPUT_GAMEPAD_Guide, 16}, + {XINPUT_GAMEPAD_A, 0}, + {XINPUT_GAMEPAD_B, 1}, + {XINPUT_GAMEPAD_X, 2}, + {XINPUT_GAMEPAD_Y, 3}}; +const size_t kNumMappings = ArrayLength(kXIButtonMap); + +enum GamepadType { kNoGamepad = 0, kRawInputGamepad, kXInputGamepad }; + +class WindowsGamepadService; +// This pointer holds a windows gamepad backend service, +// it will be created and destroyed by background thread and +// used by gMonitorThread +WindowsGamepadService* MOZ_NON_OWNING_REF gService = nullptr; +nsCOMPtr<nsIThread> gMonitorThread = nullptr; +static bool sIsShutdown = false; + +class Gamepad { + public: + GamepadType type; + + // Handle to raw input device + HANDLE handle; + + // XInput Index of the user's controller. Passed to XInputGetState. + DWORD userIndex; + + // Last-known state of the controller. + XINPUT_STATE state; + + // Handle from the GamepadService + GamepadHandle gamepadHandle; + + // Information about the physical device. + unsigned numAxes; + unsigned numButtons; + + nsTArray<bool> buttons; + struct axisValue { + HIDP_VALUE_CAPS caps; + double value; + bool active; + + axisValue() : value(0.0f), active(false) {} + explicit axisValue(const HIDP_VALUE_CAPS& aCaps) + : caps(aCaps), value(0.0f), active(true) {} + }; + nsTArray<axisValue> axes; + + RefPtr<GamepadRemapper> remapper; + + // Used during rescan to find devices that were disconnected. + bool present; + + Gamepad(uint32_t aNumAxes, uint32_t aNumButtons, GamepadType aType) + : type(aType), numAxes(aNumAxes), numButtons(aNumButtons), present(true) { + buttons.SetLength(numButtons); + axes.SetLength(numAxes); + } + + private: + Gamepad() {} +}; + +// Drop this in favor of decltype when we require a new enough SDK. +using XInputEnable_func = void(WINAPI*)(BOOL); + +// RAII class to wrap loading the XInput DLL +class XInputLoader { + public: + XInputLoader() + : module(nullptr), mXInputGetState(nullptr), mXInputEnable(nullptr) { + // xinput1_4.dll exists on Windows 8 + // xinput9_1_0.dll exists on Windows 7 and Vista + // xinput1_3.dll shipped with the DirectX SDK + const wchar_t* dlls[] = {L"xinput1_4.dll", L"xinput9_1_0.dll", + L"xinput1_3.dll"}; + const size_t kNumDLLs = ArrayLength(dlls); + for (size_t i = 0; i < kNumDLLs; ++i) { + module = LoadLibraryW(dlls[i]); + if (module) { + mXInputEnable = reinterpret_cast<XInputEnable_func>( + GetProcAddress(module, "XInputEnable")); + // Checking if `XInputGetStateEx` is available. If not, + // we will fallback to use `XInputGetState`. + mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>( + GetProcAddress(module, kXInputGetStateExOrdinal)); + if (!mXInputGetState) { + mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>( + GetProcAddress(module, "XInputGetState")); + } + MOZ_ASSERT(mXInputGetState && + "XInputGetState must be linked successfully."); + + if (mXInputEnable) { + mXInputEnable(TRUE); + } + break; + } + } + } + + ~XInputLoader() { + mXInputEnable = nullptr; + mXInputGetState = nullptr; + + if (module) { + FreeLibrary(module); + } + } + + explicit operator bool() { return module && mXInputGetState; } + + HMODULE module; + decltype(XInputGetState)* mXInputGetState; + XInputEnable_func mXInputEnable; +}; + +bool GetPreparsedData(HANDLE handle, nsTArray<uint8_t>& data) { + UINT size; + if (GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, nullptr, &size) == + kRawInputError) { + return false; + } + data.SetLength(size); + return GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, data.Elements(), + &size) > 0; +} + +/* + * Given an axis value and a minimum and maximum range, + * scale it to be in the range -1.0 .. 1.0. + */ +double ScaleAxis(ULONG value, LONG min, LONG max) { + return 2.0 * (value - min) / (max - min) - 1.0; +} + +/* + * Return true if this USB HID usage page and usage are of a type we + * know how to handle. + */ +bool SupportedUsage(USHORT page, USHORT usage) { + for (unsigned i = 0; i < ArrayLength(kUsagePages); i++) { + if (page == kUsagePages[i].usagePage && usage == kUsagePages[i].usage) { + return true; + } + } + return false; +} + +class HIDLoader { + public: + HIDLoader() + : mHidD_GetProductString(nullptr), + mHidP_GetCaps(nullptr), + mHidP_GetButtonCaps(nullptr), + mHidP_GetValueCaps(nullptr), + mHidP_GetUsages(nullptr), + mHidP_GetUsageValue(nullptr), + mHidP_GetScaledUsageValue(nullptr), + mModule(LoadLibraryW(L"hid.dll")) { + if (mModule) { + mHidD_GetProductString = + reinterpret_cast<decltype(HidD_GetProductString)*>( + GetProcAddress(mModule, "HidD_GetProductString")); + mHidP_GetCaps = reinterpret_cast<decltype(HidP_GetCaps)*>( + GetProcAddress(mModule, "HidP_GetCaps")); + mHidP_GetButtonCaps = reinterpret_cast<decltype(HidP_GetButtonCaps)*>( + GetProcAddress(mModule, "HidP_GetButtonCaps")); + mHidP_GetValueCaps = reinterpret_cast<decltype(HidP_GetValueCaps)*>( + GetProcAddress(mModule, "HidP_GetValueCaps")); + mHidP_GetUsages = reinterpret_cast<decltype(HidP_GetUsages)*>( + GetProcAddress(mModule, "HidP_GetUsages")); + mHidP_GetUsageValue = reinterpret_cast<decltype(HidP_GetUsageValue)*>( + GetProcAddress(mModule, "HidP_GetUsageValue")); + mHidP_GetScaledUsageValue = + reinterpret_cast<decltype(HidP_GetScaledUsageValue)*>( + GetProcAddress(mModule, "HidP_GetScaledUsageValue")); + } + } + + ~HIDLoader() { + if (mModule) { + FreeLibrary(mModule); + } + } + + explicit operator bool() { + return mModule && mHidD_GetProductString && mHidP_GetCaps && + mHidP_GetButtonCaps && mHidP_GetValueCaps && mHidP_GetUsages && + mHidP_GetUsageValue && mHidP_GetScaledUsageValue; + } + + decltype(HidD_GetProductString)* mHidD_GetProductString; + decltype(HidP_GetCaps)* mHidP_GetCaps; + decltype(HidP_GetButtonCaps)* mHidP_GetButtonCaps; + decltype(HidP_GetValueCaps)* mHidP_GetValueCaps; + decltype(HidP_GetUsages)* mHidP_GetUsages; + decltype(HidP_GetUsageValue)* mHidP_GetUsageValue; + decltype(HidP_GetScaledUsageValue)* mHidP_GetScaledUsageValue; + + private: + HMODULE mModule; +}; + +HWND sHWnd = nullptr; + +static void DirectInputMessageLoopOnceCallback(nsITimer* aTimer, + void* aClosure) { + MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread); + MSG msg; + while (PeekMessageW(&msg, sHWnd, 0, 0, PM_REMOVE) > 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + aTimer->Cancel(); + if (!sIsShutdown) { + aTimer->InitWithNamedFuncCallback(DirectInputMessageLoopOnceCallback, + nullptr, kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT, + "DirectInputMessageLoopOnceCallback"); + } +} + +class WindowsGamepadService { + public: + WindowsGamepadService() { + mDirectInputTimer = NS_NewTimer(); + mXInputTimer = NS_NewTimer(); + mDeviceChangeTimer = NS_NewTimer(); + } + virtual ~WindowsGamepadService() { Cleanup(); } + + void DevicesChanged(bool aIsStablizing); + + void StartMessageLoop() { + MOZ_ASSERT(mDirectInputTimer); + mDirectInputTimer->InitWithNamedFuncCallback( + DirectInputMessageLoopOnceCallback, nullptr, + kWindowsGamepadPollInterval, nsITimer::TYPE_ONE_SHOT, + "DirectInputMessageLoopOnceCallback"); + } + + void Startup(); + void Shutdown(); + // Parse gamepad input from a WM_INPUT message. + bool HandleRawInput(HRAWINPUT handle); + void SetLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, + const uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue); + size_t WriteOutputReport(const std::vector<uint8_t>& aReport); + static void XInputMessageLoopOnceCallback(nsITimer* aTimer, void* aClosure); + static void DevicesChangeCallback(nsITimer* aTimer, void* aService); + + private: + void ScanForDevices(); + // Look for connected raw input devices. + void ScanForRawInputDevices(); + // Look for connected XInput devices. + bool ScanForXInputDevices(); + bool HaveXInputGamepad(unsigned int userIndex); + + bool mIsXInputMonitoring; + void PollXInput(); + void CheckXInputChanges(Gamepad& gamepad, XINPUT_STATE& state); + + // Get information about a raw input gamepad. + bool GetRawGamepad(HANDLE handle); + void Cleanup(); + + // List of connected devices. + nsTArray<Gamepad> mGamepads; + + HIDLoader mHID; + nsAutoHandle mHidHandle; + XInputLoader mXInput; + + nsCOMPtr<nsITimer> mDirectInputTimer; + nsCOMPtr<nsITimer> mXInputTimer; + nsCOMPtr<nsITimer> mDeviceChangeTimer; +}; + +void WindowsGamepadService::ScanForRawInputDevices() { + if (!mHID) { + return; + } + + UINT numDevices; + if (GetRawInputDeviceList(nullptr, &numDevices, sizeof(RAWINPUTDEVICELIST)) == + kRawInputError) { + return; + } + nsTArray<RAWINPUTDEVICELIST> devices(numDevices); + devices.SetLength(numDevices); + if (GetRawInputDeviceList(devices.Elements(), &numDevices, + sizeof(RAWINPUTDEVICELIST)) == kRawInputError) { + return; + } + + for (unsigned i = 0; i < devices.Length(); i++) { + if (devices[i].dwType == RIM_TYPEHID) { + GetRawGamepad(devices[i].hDevice); + } + } +} + +// static +void WindowsGamepadService::XInputMessageLoopOnceCallback(nsITimer* aTimer, + void* aService) { + MOZ_ASSERT(aService); + WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService); + self->PollXInput(); + if (self->mIsXInputMonitoring) { + aTimer->Cancel(); + aTimer->InitWithNamedFuncCallback( + XInputMessageLoopOnceCallback, self, kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT, "XInputMessageLoopOnceCallback"); + } +} + +// static +void WindowsGamepadService::DevicesChangeCallback(nsITimer* aTimer, + void* aService) { + MOZ_ASSERT(aService); + WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService); + self->DevicesChanged(false); +} + +bool WindowsGamepadService::HaveXInputGamepad(unsigned int userIndex) { + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + if (mGamepads[i].type == kXInputGamepad && + mGamepads[i].userIndex == userIndex) { + mGamepads[i].present = true; + return true; + } + } + return false; +} + +bool WindowsGamepadService::ScanForXInputDevices() { + MOZ_ASSERT(mXInput, "XInput should be present!"); + + bool found = false; + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return found; + } + + for (unsigned int i = 0; i < XUSER_MAX_COUNT; i++) { + XINPUT_STATE state = {}; + + if (!mXInput.mXInputGetState || + mXInput.mXInputGetState(i, &state) != ERROR_SUCCESS) { + continue; + } + + found = true; + // See if this device is already present in our list. + if (HaveXInputGamepad(i)) { + continue; + } + + // Not already present, add it. + Gamepad gamepad(kStandardGamepadAxes, kStandardGamepadButtons, + kXInputGamepad); + gamepad.userIndex = i; + gamepad.state = state; + gamepad.gamepadHandle = service->AddGamepad( + "xinput", GamepadMappingType::Standard, GamepadHand::_empty, + kStandardGamepadButtons, kStandardGamepadAxes, 0, 0, + 0); // TODO: Bug 680289, implement gamepad haptics for Windows. + mGamepads.AppendElement(std::move(gamepad)); + } + + return found; +} + +void WindowsGamepadService::ScanForDevices() { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + for (int i = mGamepads.Length() - 1; i >= 0; i--) { + mGamepads[i].present = false; + } + + if (mHID) { + ScanForRawInputDevices(); + } + if (mXInput) { + mXInputTimer->Cancel(); + if (ScanForXInputDevices()) { + mIsXInputMonitoring = true; + mXInputTimer->InitWithNamedFuncCallback( + XInputMessageLoopOnceCallback, this, kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT, "XInputMessageLoopOnceCallback"); + } else { + mIsXInputMonitoring = false; + } + } + + // Look for devices that are no longer present and remove them. + for (int i = mGamepads.Length() - 1; i >= 0; i--) { + if (!mGamepads[i].present) { + service->RemoveGamepad(mGamepads[i].gamepadHandle); + mGamepads.RemoveElementAt(i); + } + } +} + +void WindowsGamepadService::PollXInput() { + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + if (mGamepads[i].type != kXInputGamepad) { + continue; + } + + XINPUT_STATE state = {}; + + if (!mXInput.mXInputGetState || + mXInput.mXInputGetState(i, &state) != ERROR_SUCCESS) { + continue; + } + + if (state.dwPacketNumber != mGamepads[i].state.dwPacketNumber) { + CheckXInputChanges(mGamepads[i], state); + } + } +} + +void WindowsGamepadService::CheckXInputChanges(Gamepad& gamepad, + XINPUT_STATE& state) { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + // Handle digital buttons first + for (size_t b = 0; b < kNumMappings; b++) { + if (state.Gamepad.wButtons & kXIButtonMap[b].button && + !(gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button)) { + // Button pressed + service->NewButtonEvent(gamepad.gamepadHandle, kXIButtonMap[b].mapped, + true); + } else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) && + gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) { + // Button released + service->NewButtonEvent(gamepad.gamepadHandle, kXIButtonMap[b].mapped, + false); + } + } + + // Then triggers + if (state.Gamepad.bLeftTrigger != gamepad.state.Gamepad.bLeftTrigger) { + const bool pressed = + state.Gamepad.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD; + service->NewButtonEvent(gamepad.gamepadHandle, kButtonLeftTrigger, pressed, + state.Gamepad.bLeftTrigger / 255.0); + } + if (state.Gamepad.bRightTrigger != gamepad.state.Gamepad.bRightTrigger) { + const bool pressed = + state.Gamepad.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD; + service->NewButtonEvent(gamepad.gamepadHandle, kButtonRightTrigger, pressed, + state.Gamepad.bRightTrigger / 255.0); + } + + // Finally deal with analog sticks + // TODO: bug 1001955 - Support deadzones. + if (state.Gamepad.sThumbLX != gamepad.state.Gamepad.sThumbLX) { + const float div = state.Gamepad.sThumbLX > 0 ? 32767.0 : 32768.0; + service->NewAxisMoveEvent(gamepad.gamepadHandle, kLeftStickXAxis, + state.Gamepad.sThumbLX / div); + } + if (state.Gamepad.sThumbLY != gamepad.state.Gamepad.sThumbLY) { + const float div = state.Gamepad.sThumbLY > 0 ? 32767.0 : 32768.0; + service->NewAxisMoveEvent(gamepad.gamepadHandle, kLeftStickYAxis, + -1.0 * state.Gamepad.sThumbLY / div); + } + if (state.Gamepad.sThumbRX != gamepad.state.Gamepad.sThumbRX) { + const float div = state.Gamepad.sThumbRX > 0 ? 32767.0 : 32768.0; + service->NewAxisMoveEvent(gamepad.gamepadHandle, kRightStickXAxis, + state.Gamepad.sThumbRX / div); + } + if (state.Gamepad.sThumbRY != gamepad.state.Gamepad.sThumbRY) { + const float div = state.Gamepad.sThumbRY > 0 ? 32767.0 : 32768.0; + service->NewAxisMoveEvent(gamepad.gamepadHandle, kRightStickYAxis, + -1.0 * state.Gamepad.sThumbRY / div); + } + gamepad.state = state; +} + +// Used to sort a list of axes by HID usage. +class HidValueComparator { + public: + bool Equals(const Gamepad::axisValue& c1, + const Gamepad::axisValue& c2) const { + return c1.caps.UsagePage == c2.caps.UsagePage && + c1.caps.Range.UsageMin == c2.caps.Range.UsageMin; + } + bool LessThan(const Gamepad::axisValue& c1, + const Gamepad::axisValue& c2) const { + if (c1.caps.UsagePage == c2.caps.UsagePage) { + return c1.caps.Range.UsageMin < c2.caps.Range.UsageMin; + } + return c1.caps.UsagePage < c2.caps.UsagePage; + } +}; + +// GetRawGamepad() processes its raw data from HID and +// then trying to remapping buttons and axes based on +// the mapping rules that are defined for different gamepad products. +bool WindowsGamepadService::GetRawGamepad(HANDLE handle) { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return true; + } + + if (!mHID) { + return false; + } + + for (unsigned i = 0; i < mGamepads.Length(); i++) { + if (mGamepads[i].type == kRawInputGamepad && + mGamepads[i].handle == handle) { + mGamepads[i].present = true; + return true; + } + } + + RID_DEVICE_INFO rdi = {}; + UINT size = rdi.cbSize = sizeof(RID_DEVICE_INFO); + if (GetRawInputDeviceInfo(handle, RIDI_DEVICEINFO, &rdi, &size) == + kRawInputError) { + return false; + } + // Ensure that this is a device we care about + if (!SupportedUsage(rdi.hid.usUsagePage, rdi.hid.usUsage)) { + return false; + } + + // Device name is a mostly-opaque string. + if (GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, nullptr, &size) == + kRawInputError) { + return false; + } + + nsTArray<wchar_t> devname(size); + devname.SetLength(size); + if (GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, devname.Elements(), + &size) == kRawInputError) { + return false; + } + + // Per http://msdn.microsoft.com/en-us/library/windows/desktop/ee417014.aspx + // device names containing "IG_" are XInput controllers. Ignore those + // devices since we'll handle them with XInput. + if (wcsstr(devname.Elements(), L"IG_")) { + return false; + } + + // Product string is a human-readable name. + // Per + // http://msdn.microsoft.com/en-us/library/windows/hardware/ff539681%28v=vs.85%29.aspx + // "For USB devices, the maximum string length is 126 wide characters (not + // including the terminating NULL character)." + wchar_t name[128] = {0}; + size = sizeof(name); + nsTArray<char> gamepad_name; + // Creating this file with FILE_FLAG_OVERLAPPED to perform + // an asynchronous request in WriteOutputReport. + mHidHandle.own(CreateFile(devname.Elements(), GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr)); + if (mHidHandle != INVALID_HANDLE_VALUE) { + if (mHID.mHidD_GetProductString(mHidHandle, &name, size)) { + int bytes = WideCharToMultiByte(CP_UTF8, 0, name, -1, nullptr, 0, nullptr, + nullptr); + gamepad_name.SetLength(bytes); + WideCharToMultiByte(CP_UTF8, 0, name, -1, gamepad_name.Elements(), bytes, + nullptr, nullptr); + } + } + if (gamepad_name.Length() == 0 || !gamepad_name[0]) { + const char kUnknown[] = "Unknown Gamepad"; + gamepad_name.SetLength(ArrayLength(kUnknown)); + strcpy_s(gamepad_name.Elements(), gamepad_name.Length(), kUnknown); + } + + char gamepad_id[256] = {0}; + _snprintf_s(gamepad_id, _TRUNCATE, "%04x-%04x-%s", rdi.hid.dwVendorId, + rdi.hid.dwProductId, gamepad_name.Elements()); + + nsTArray<uint8_t> preparsedbytes; + if (!GetPreparsedData(handle, preparsedbytes)) { + return false; + } + + PHIDP_PREPARSED_DATA parsed = + reinterpret_cast<PHIDP_PREPARSED_DATA>(preparsedbytes.Elements()); + HIDP_CAPS caps; + if (mHID.mHidP_GetCaps(parsed, &caps) != HIDP_STATUS_SUCCESS) { + return false; + } + + // Enumerate buttons. + USHORT count = caps.NumberInputButtonCaps; + nsTArray<HIDP_BUTTON_CAPS> buttonCaps(count); + buttonCaps.SetLength(count); + if (mHID.mHidP_GetButtonCaps(HidP_Input, buttonCaps.Elements(), &count, + parsed) != HIDP_STATUS_SUCCESS) { + return false; + } + uint32_t numButtons = 0; + for (unsigned i = 0; i < count; i++) { + // Each buttonCaps is typically a range of buttons. + numButtons += + buttonCaps[i].Range.UsageMax - buttonCaps[i].Range.UsageMin + 1; + } + + // Enumerate value caps, which represent axes and d-pads. + count = caps.NumberInputValueCaps; + nsTArray<HIDP_VALUE_CAPS> axisCaps(count); + axisCaps.SetLength(count); + if (mHID.mHidP_GetValueCaps(HidP_Input, axisCaps.Elements(), &count, + parsed) != HIDP_STATUS_SUCCESS) { + return false; + } + + size_t numAxes = 0; + nsTArray<Gamepad::axisValue> axes(kAxesLengthCap); + // We store these value caps and handle the dpad info in GamepadRemapper + // later. + axes.SetLength(kAxesLengthCap); + + // Looking for the exisiting ramapping rule. + bool defaultRemapper = false; + RefPtr<GamepadRemapper> remapper = GetGamepadRemapper( + rdi.hid.dwVendorId, rdi.hid.dwProductId, defaultRemapper); + MOZ_ASSERT(remapper); + + for (size_t i = 0; i < count; i++) { + const size_t axisIndex = + axisCaps[i].Range.UsageMin - kAxisMinimumUsageNumber; + if (axisIndex < kAxesLengthCap && !axes[axisIndex].active) { + axes[axisIndex].caps = axisCaps[i]; + axes[axisIndex].active = true; + numAxes = std::max(numAxes, axisIndex + 1); + } + } + + // Not already present, add it. + + remapper->SetAxisCount(numAxes); + remapper->SetButtonCount(numButtons); + Gamepad gamepad(numAxes, numButtons, kRawInputGamepad); + gamepad.handle = handle; + + for (unsigned i = 0; i < gamepad.numAxes; i++) { + gamepad.axes[i] = axes[i]; + } + + gamepad.remapper = remapper.forget(); + // TODO: Bug 680289, implement gamepad haptics for Windows. + gamepad.gamepadHandle = service->AddGamepad( + gamepad_id, gamepad.remapper->GetMappingType(), GamepadHand::_empty, + gamepad.remapper->GetButtonCount(), gamepad.remapper->GetAxisCount(), 0, + gamepad.remapper->GetLightIndicatorCount(), + gamepad.remapper->GetTouchEventCount()); + + nsTArray<GamepadLightIndicatorType> lightTypes; + gamepad.remapper->GetLightIndicators(lightTypes); + for (uint32_t i = 0; i < lightTypes.Length(); ++i) { + if (lightTypes[i] != GamepadLightIndicator::DefaultType()) { + service->NewLightIndicatorTypeEvent(gamepad.gamepadHandle, i, + lightTypes[i]); + } + } + + mGamepads.AppendElement(std::move(gamepad)); + return true; +} + +bool WindowsGamepadService::HandleRawInput(HRAWINPUT handle) { + if (!mHID) { + return false; + } + + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return false; + } + + // First, get data from the handle + UINT size; + GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)); + nsTArray<uint8_t> data(size); + data.SetLength(size); + if (GetRawInputData(handle, RID_INPUT, data.Elements(), &size, + sizeof(RAWINPUTHEADER)) == kRawInputError) { + return false; + } + PRAWINPUT raw = reinterpret_cast<PRAWINPUT>(data.Elements()); + + Gamepad* gamepad = nullptr; + for (unsigned i = 0; i < mGamepads.Length(); i++) { + if (mGamepads[i].type == kRawInputGamepad && + mGamepads[i].handle == raw->header.hDevice) { + gamepad = &mGamepads[i]; + break; + } + } + if (gamepad == nullptr) { + return false; + } + + // Second, get the preparsed data + nsTArray<uint8_t> parsedbytes; + if (!GetPreparsedData(raw->header.hDevice, parsedbytes)) { + return false; + } + PHIDP_PREPARSED_DATA parsed = + reinterpret_cast<PHIDP_PREPARSED_DATA>(parsedbytes.Elements()); + + // Get all the pressed buttons. + nsTArray<USAGE> usages(gamepad->numButtons); + usages.SetLength(gamepad->numButtons); + ULONG usageLength = gamepad->numButtons; + if (mHID.mHidP_GetUsages(HidP_Input, kButtonUsagePage, 0, usages.Elements(), + &usageLength, parsed, (PCHAR)raw->data.hid.bRawData, + raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) { + return false; + } + + nsTArray<bool> buttons(gamepad->numButtons); + buttons.SetLength(gamepad->numButtons); + // If we don't zero out the buttons array first, sometimes it can reuse + // values. + memset(buttons.Elements(), 0, gamepad->numButtons * sizeof(bool)); + + for (unsigned i = 0; i < usageLength; i++) { + // The button index in usages may be larger than what we detected when + // enumerating gamepads. If so, warn and continue. + // + // Usage ID of 0 is reserved, so it should always be 1 or higher. + if (NS_WARN_IF((usages[i] - 1u) >= buttons.Length())) { + continue; + } + buttons[usages[i] - 1u] = true; + } + + for (unsigned i = 0; i < gamepad->numButtons; i++) { + if (gamepad->buttons[i] != buttons[i]) { + gamepad->remapper->RemapButtonEvent(gamepad->gamepadHandle, i, + buttons[i]); + gamepad->buttons[i] = buttons[i]; + } + } + + // Get all axis values. + for (unsigned i = 0; i < gamepad->numAxes; i++) { + double new_value; + if (gamepad->axes[i].caps.LogicalMin < 0) { + LONG value; + if (mHID.mHidP_GetScaledUsageValue( + HidP_Input, gamepad->axes[i].caps.UsagePage, 0, + gamepad->axes[i].caps.Range.UsageMin, &value, parsed, + (PCHAR)raw->data.hid.bRawData, + raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) { + continue; + } + new_value = ScaleAxis(value, gamepad->axes[i].caps.LogicalMin, + gamepad->axes[i].caps.LogicalMax); + } else { + ULONG value; + if (mHID.mHidP_GetUsageValue( + HidP_Input, gamepad->axes[i].caps.UsagePage, 0, + gamepad->axes[i].caps.Range.UsageMin, &value, parsed, + (PCHAR)raw->data.hid.bRawData, + raw->data.hid.dwSizeHid) != HIDP_STATUS_SUCCESS) { + continue; + } + + new_value = ScaleAxis(value, gamepad->axes[i].caps.LogicalMin, + gamepad->axes[i].caps.LogicalMax); + } + if (gamepad->axes[i].value != new_value) { + gamepad->remapper->RemapAxisMoveEvent(gamepad->gamepadHandle, i, + new_value); + gamepad->axes[i].value = new_value; + } + } + + BYTE* rawData = raw->data.hid.bRawData; + gamepad->remapper->ProcessTouchData(gamepad->gamepadHandle, rawData); + + return true; +} + +void WindowsGamepadService::SetLightIndicatorColor( + const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, const uint8_t& aRed, + const uint8_t& aGreen, const uint8_t& aBlue) { + // We get aControllerIdx from GamepadPlatformService::AddGamepad(), + // It begins from 1 and is stored at Gamepad.id. + const Gamepad* gamepad = (MOZ_FIND_AND_VALIDATE( + aGamepadHandle, list_item.gamepadHandle == aGamepadHandle, mGamepads)); + if (!gamepad) { + MOZ_ASSERT(false); + return; + } + + RefPtr<GamepadRemapper> remapper = gamepad->remapper; + if (!remapper || + MOZ_IS_VALID(aLightColorIndex, + remapper->GetLightIndicatorCount() <= aLightColorIndex)) { + MOZ_ASSERT(false); + return; + } + + std::vector<uint8_t> report; + remapper->GetLightColorReport(aRed, aGreen, aBlue, report); + WriteOutputReport(report); +} + +size_t WindowsGamepadService::WriteOutputReport( + const std::vector<uint8_t>& aReport) { + DCHECK(static_cast<const void*>(aReport.data())); + DCHECK_GE(aReport.size(), 1U); + if (!mHidHandle) return 0; + + nsAutoHandle eventHandle(::CreateEvent(nullptr, FALSE, FALSE, nullptr)); + OVERLAPPED overlapped = {0}; + overlapped.hEvent = eventHandle; + + // Doing an asynchronous write to allows us to time out + // if the write takes too long. + DWORD bytesWritten = 0; + BOOL writeSuccess = + ::WriteFile(mHidHandle, static_cast<const void*>(aReport.data()), + aReport.size(), &bytesWritten, &overlapped); + if (!writeSuccess) { + DWORD error = ::GetLastError(); + if (error == ERROR_IO_PENDING) { + // Wait for the write to complete. This causes WriteOutputReport to behave + // synchronously but with a timeout. + DWORD wait_object = ::WaitForSingleObject(overlapped.hEvent, 100); + if (wait_object == WAIT_OBJECT_0) { + if (!::GetOverlappedResult(mHidHandle, &overlapped, &bytesWritten, + TRUE)) { + return 0; + } + } else { + // Wait failed, or the timeout was exceeded before the write completed. + // Cancel the write request. + if (::CancelIo(mHidHandle)) { + wait_object = ::WaitForSingleObject(overlapped.hEvent, INFINITE); + MOZ_ASSERT(wait_object == WAIT_OBJECT_0); + } + } + } + } + return writeSuccess ? bytesWritten : 0; +} + +void WindowsGamepadService::Startup() { ScanForDevices(); } + +void WindowsGamepadService::Shutdown() { Cleanup(); } + +void WindowsGamepadService::Cleanup() { + mIsXInputMonitoring = false; + if (mDirectInputTimer) { + mDirectInputTimer->Cancel(); + } + if (mXInputTimer) { + mXInputTimer->Cancel(); + } + if (mDeviceChangeTimer) { + mDeviceChangeTimer->Cancel(); + } + + mGamepads.Clear(); +} + +void WindowsGamepadService::DevicesChanged(bool aIsStablizing) { + if (aIsStablizing) { + mDeviceChangeTimer->Cancel(); + mDeviceChangeTimer->InitWithNamedFuncCallback( + DevicesChangeCallback, this, kDevicesChangedStableDelay, + nsITimer::TYPE_ONE_SHOT, "DevicesChangeCallback"); + } else { + ScanForDevices(); + } +} + +bool RegisterRawInput(HWND hwnd, bool enable) { + nsTArray<RAWINPUTDEVICE> rid(ArrayLength(kUsagePages)); + rid.SetLength(ArrayLength(kUsagePages)); + + for (unsigned i = 0; i < rid.Length(); i++) { + rid[i].usUsagePage = kUsagePages[i].usagePage; + rid[i].usUsage = kUsagePages[i].usage; + rid[i].dwFlags = + enable ? RIDEV_EXINPUTSINK | RIDEV_DEVNOTIFY : RIDEV_REMOVE; + rid[i].hwndTarget = hwnd; + } + + if (!RegisterRawInputDevices(rid.Elements(), rid.Length(), + sizeof(RAWINPUTDEVICE))) { + return false; + } + return true; +} + +static LRESULT CALLBACK GamepadWindowProc(HWND hwnd, UINT msg, WPARAM wParam, + LPARAM lParam) { + const unsigned int DBT_DEVICEARRIVAL = 0x8000; + const unsigned int DBT_DEVICEREMOVECOMPLETE = 0x8004; + const unsigned int DBT_DEVNODES_CHANGED = 0x7; + + switch (msg) { + case WM_DEVICECHANGE: + if (wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE || + wParam == DBT_DEVNODES_CHANGED) { + if (gService) { + gService->DevicesChanged(true); + } + } + break; + case WM_INPUT: + if (gService) { + gService->HandleRawInput(reinterpret_cast<HRAWINPUT>(lParam)); + } + break; + } + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +class StartWindowsGamepadServiceRunnable final : public Runnable { + public: + StartWindowsGamepadServiceRunnable() + : Runnable("StartWindowsGamepadServiceRunnable") {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread); + gService = new WindowsGamepadService(); + gService->Startup(); + + if (sHWnd == nullptr) { + WNDCLASSW wc; + HMODULE hSelf = GetModuleHandle(nullptr); + + if (!GetClassInfoW(hSelf, L"MozillaGamepadClass", &wc)) { + ZeroMemory(&wc, sizeof(WNDCLASSW)); + wc.hInstance = hSelf; + wc.lpfnWndProc = GamepadWindowProc; + wc.lpszClassName = L"MozillaGamepadClass"; + RegisterClassW(&wc); + } + + sHWnd = CreateWindowW(L"MozillaGamepadClass", L"Gamepad Watcher", 0, 0, 0, + 0, 0, nullptr, nullptr, hSelf, nullptr); + RegisterRawInput(sHWnd, true); + } + + // Explicitly start the message loop + gService->StartMessageLoop(); + + return NS_OK; + } + + private: + ~StartWindowsGamepadServiceRunnable() {} +}; + +class StopWindowsGamepadServiceRunnable final : public Runnable { + public: + StopWindowsGamepadServiceRunnable() + : Runnable("StopWindowsGamepadServiceRunnable") {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread); + if (sHWnd) { + RegisterRawInput(sHWnd, false); + DestroyWindow(sHWnd); + sHWnd = nullptr; + } + + gService->Shutdown(); + delete gService; + gService = nullptr; + + return NS_OK; + } + + private: + ~StopWindowsGamepadServiceRunnable() {} +}; + +} // namespace + +namespace mozilla::dom { + +using namespace mozilla::ipc; + +void StartGamepadMonitoring() { + AssertIsOnBackgroundThread(); + + if (gMonitorThread || gService) { + return; + } + sIsShutdown = false; + NS_NewNamedThread("Gamepad", getter_AddRefs(gMonitorThread)); + gMonitorThread->Dispatch(new StartWindowsGamepadServiceRunnable(), + NS_DISPATCH_NORMAL); +} + +void StopGamepadMonitoring() { + AssertIsOnBackgroundThread(); + + if (sIsShutdown) { + return; + } + sIsShutdown = true; + gMonitorThread->Dispatch(new StopWindowsGamepadServiceRunnable(), + NS_DISPATCH_NORMAL); + gMonitorThread->Shutdown(); + gMonitorThread = nullptr; +} + +void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle, + const Tainted<uint32_t>& aLightColorIndex, + const uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue) { + MOZ_ASSERT(gService); + if (!gService) { + return; + } + gService->SetLightIndicatorColor(aGamepadHandle, aLightColorIndex, aRed, + aGreen, aBlue); +} + +} // namespace mozilla::dom |