diff options
Diffstat (limited to 'dom/gamepad')
35 files changed, 5268 insertions, 0 deletions
diff --git a/dom/gamepad/Gamepad.cpp b/dom/gamepad/Gamepad.cpp new file mode 100644 index 000000000..0c59b190e --- /dev/null +++ b/dom/gamepad/Gamepad.cpp @@ -0,0 +1,147 @@ +/* -*- 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/dom/GamepadBinding.h" + +namespace mozilla { +namespace 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) + +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, uint32_t aIndex, + GamepadMappingType aMapping, + uint32_t aNumButtons, uint32_t aNumAxes) + : mParent(aParent), + mID(aID), + mIndex(aIndex), + mMapping(aMapping), + 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); + UpdateTimestamp(); +} + +void +Gamepad::SetIndex(uint32_t aIndex) +{ + mIndex = aIndex; +} + +void +Gamepad::SetConnected(bool aConnected) +{ + mConnected = aConnected; +} + +void +Gamepad::SetButton(uint32_t aButton, bool aPressed, double aValue) +{ + MOZ_ASSERT(aButton < mButtons.Length()); + mButtons[aButton]->SetPressed(aPressed); + 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; + GamepadBinding::ClearCachedAxesValue(this); + } + UpdateTimestamp(); +} + +void +Gamepad::SetPose(const GamepadPoseState& aPose) +{ + mPose->SetPoseState(aPose); +} + +void +Gamepad::SyncState(Gamepad* aOther) +{ + const char* kGamepadExtEnabledPref = "dom.gamepad.extensions.enabled"; + + 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]->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) { + GamepadBinding::ClearCachedAxesValue(this); + } + + if (Preferences::GetBool(kGamepadExtEnabledPref)) { + MOZ_ASSERT(aOther->GetPose()); + mPose->SetPoseState(aOther->GetPose()->GetPoseState()); + } + + UpdateTimestamp(); +} + +already_AddRefed<Gamepad> +Gamepad::Clone(nsISupports* aParent) +{ + RefPtr<Gamepad> out = + new Gamepad(aParent, mID, mIndex, mMapping, + mButtons.Length(), mAxes.Length()); + out->SyncState(this); + return out.forget(); +} + +/* virtual */ JSObject* +Gamepad::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return GamepadBinding::Wrap(aCx, this, aGivenProto); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/Gamepad.h b/dom/gamepad/Gamepad.h new file mode 100644 index 000000000..e388348f0 --- /dev/null +++ b/dom/gamepad/Gamepad.h @@ -0,0 +1,134 @@ +/* -*- 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/ErrorResult.h" +#include "mozilla/dom/GamepadBinding.h" +#include "mozilla/dom/GamepadButton.h" +#include "mozilla/dom/GamepadPose.h" +#include "mozilla/dom/Performance.h" +#include <stdint.h> +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +namespace mozilla { +namespace dom { + +// 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, uint32_t aIndex, + GamepadMappingType aMapping, + uint32_t aNumButtons, uint32_t aNumAxes); + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Gamepad) + + void SetConnected(bool aConnected); + void SetButton(uint32_t aButton, bool aPressed, double aValue); + void SetAxis(uint32_t aAxis, double aValue); + void SetIndex(uint32_t aIndex); + void SetPose(const GamepadPoseState& aPose); + + // 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; + } + + bool Connected() const + { + return mConnected; + } + + uint32_t Index() const + { + return mIndex; + } + + void GetButtons(nsTArray<RefPtr<GamepadButton>>& aButtons) const + { + aButtons = mButtons; + } + + void GetAxes(nsTArray<double>& aAxes) const + { + aAxes = mAxes; + } + + GamepadPose* GetPose() const + { + return mPose; + } + +private: + virtual ~Gamepad() {} + void UpdateTimestamp(); + +protected: + nsCOMPtr<nsISupports> mParent; + nsString mID; + uint32_t mIndex; + + // The mapping in use. + GamepadMappingType mMapping; + + // 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; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_gamepad_Gamepad_h diff --git a/dom/gamepad/GamepadButton.cpp b/dom/gamepad/GamepadButton.cpp new file mode 100644 index 000000000..154a13985 --- /dev/null +++ b/dom/gamepad/GamepadButton.cpp @@ -0,0 +1,30 @@ +/* -*- 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 { +namespace 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 GamepadButtonBinding::Wrap(aCx, this, aGivenProto); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/GamepadButton.h b/dom/gamepad/GamepadButton.h new file mode 100644 index 000000000..2855959e5 --- /dev/null +++ b/dom/gamepad/GamepadButton.h @@ -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/. */ + +#ifndef mozilla_dom_gamepad_GamepadButton_h +#define mozilla_dom_gamepad_GamepadButton_h + +#include <stdint.h> +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" + +namespace mozilla { +namespace dom { + +class GamepadButton : public nsISupports, + public nsWrapperCache +{ +public: + explicit GamepadButton(nsISupports* aParent) : mParent(aParent), + mPressed(false), + mValue(0) + { + } + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(GamepadButton) + + nsISupports* GetParentObject() const + { + return mParent; + } + + virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + void SetPressed(bool aPressed) + { + mPressed = aPressed; + } + + void SetValue(double aValue) + { + mValue = aValue; + } + + bool Pressed() const + { + return mPressed; + } + + double Value() const + { + return mValue; + } + +private: + virtual ~GamepadButton() {} + +protected: + nsCOMPtr<nsISupports> mParent; + bool mPressed; + double mValue; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_gamepad_GamepadButton_h diff --git a/dom/gamepad/GamepadManager.cpp b/dom/gamepad/GamepadManager.cpp new file mode 100644 index 000000000..dde71dd0a --- /dev/null +++ b/dom/gamepad/GamepadManager.cpp @@ -0,0 +1,699 @@ +/* -*- 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/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPtr.h" + +#include "nsAutoPtr.h" +#include "nsGlobalWindow.h" +#include "nsIDOMEvent.h" +#include "nsIDOMDocument.h" +#include "nsIDOMWindow.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIServiceManager.h" +#include "nsThreadUtils.h" +#include "VRManagerChild.h" +#include "mozilla/Services.h" +#include "mozilla/Unused.h" + +#include <cstddef> + +using namespace mozilla::ipc; + +namespace mozilla { +namespace dom { + +namespace { + +const char* kGamepadEnabledPref = "dom.gamepad.enabled"; +const char* kGamepadEventsEnabledPref = + "dom.gamepad.non_standard_events.enabled"; + +const nsTArray<RefPtr<nsGlobalWindow>>::index_type NoIndex = + nsTArray<RefPtr<nsGlobalWindow>>::NoIndex; + +bool sShutdown = false; + +StaticRefPtr<GamepadManager> gGamepadManagerSingleton; +const uint32_t VR_GAMEPAD_IDX_OFFSET = 0x01 << 16; + +} // namespace + +NS_IMPL_ISUPPORTS(GamepadManager, nsIObserver) + +GamepadManager::GamepadManager() + : mEnabled(false), + mNonstandardEventsEnabled(false), + mShuttingDown(false) +{} + +nsresult +GamepadManager::Init() +{ + mEnabled = IsAPIEnabled(); + mNonstandardEventsEnabled = + Preferences::GetBool(kGamepadEventsEnabledPref, false); + 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() +{ + for (uint32_t i = 0; i < mChannelChildren.Length(); ++i) { + mChannelChildren[i]->SendGamepadListenerRemoved(); + } + mChannelChildren.Clear(); + mGamepads.Clear(); + +#if defined(XP_WIN) || defined(XP_MACOSX) || defined(XP_LINUX) + mVRChannelChild = gfx::VRManagerChild::Get(); + mVRChannelChild->SendControllerListenerRemoved(); +#endif +} + +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(nsGlobalWindow* aWindow) +{ + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsInnerWindow()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mEnabled || mShuttingDown) { + return; + } + + if (mListeners.IndexOf(aWindow) != NoIndex) { + return; // already exists + } + + mListeners.AppendElement(aWindow); + + // IPDL child has been created + if (!mChannelChildren.IsEmpty()) { + return; + } + + PBackgroundChild *actor = BackgroundChild::GetForCurrentThread(); + //Try to get the PBackground Child actor + if (actor) { + ActorCreated(actor); + } else { + Unused << BackgroundChild::GetOrCreateForCurrentThread(this); + } +} + +void +GamepadManager::RemoveListener(nsGlobalWindow* aWindow) +{ + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsInnerWindow()); + + 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 (auto iter = mGamepads.Iter(); !iter.Done(); iter.Next()) { + aWindow->RemoveGamepad(iter.Key()); + } + + mListeners.RemoveElement(aWindow); + + if (mListeners.IsEmpty()) { + StopMonitoring(); + } +} + +already_AddRefed<Gamepad> +GamepadManager::GetGamepad(uint32_t aIndex) const +{ + RefPtr<Gamepad> gamepad; + if (mGamepads.Get(aIndex, getter_AddRefs(gamepad))) { + return gamepad.forget(); + } + + return nullptr; +} + +uint32_t GamepadManager::GetGamepadIndexWithServiceType(uint32_t aIndex, + GamepadServiceType aServiceType) +{ + uint32_t newIndex = 0; + + switch (aServiceType) { + case GamepadServiceType::Standard: + { + MOZ_ASSERT(aIndex <= VR_GAMEPAD_IDX_OFFSET); + newIndex = aIndex; + break; + } + case GamepadServiceType::VR: + { + newIndex = aIndex + VR_GAMEPAD_IDX_OFFSET; + break; + } + default: + MOZ_ASSERT(false); + break; + } + + return newIndex; +} + +void +GamepadManager::AddGamepad(uint32_t aIndex, + const nsAString& aId, + GamepadMappingType aMapping, + GamepadServiceType aServiceType, + uint32_t aNumButtons, + uint32_t aNumAxes) +{ + //TODO: bug 852258: get initial button/axis state + RefPtr<Gamepad> gamepad = + new Gamepad(nullptr, + aId, + 0, // index is set by global window + aMapping, + aNumButtons, + aNumAxes); + + uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType); + + // We store the gamepad related to its index given by the parent process, + // and no duplicate index is allowed. + MOZ_ASSERT(!mGamepads.Get(newIndex, nullptr)); + mGamepads.Put(newIndex, gamepad); + NewConnectionEvent(newIndex, true); +} + +void +GamepadManager::RemoveGamepad(uint32_t aIndex, GamepadServiceType aServiceType) +{ + uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType); + + RefPtr<Gamepad> gamepad = GetGamepad(newIndex); + if (!gamepad) { + NS_WARNING("Trying to delete gamepad with invalid index"); + return; + } + gamepad->SetConnected(false); + NewConnectionEvent(newIndex, false); + mGamepads.Remove(newIndex); +} + +void +GamepadManager::NewButtonEvent(uint32_t aIndex, GamepadServiceType aServiceType, + uint32_t aButton, bool aPressed, double aValue) +{ + if (mShuttingDown) { + return; + } + + uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType); + + RefPtr<Gamepad> gamepad = GetGamepad(newIndex); + if (!gamepad) { + return; + } + + gamepad->SetButton(aButton, aPressed, aValue); + + // Hold on to listeners in a separate array because firing events + // can mutate the mListeners array. + nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners); + MOZ_ASSERT(!listeners.IsEmpty()); + + for (uint32_t i = 0; i < listeners.Length(); i++) { + + MOZ_ASSERT(listeners[i]->IsInnerWindow()); + + // Only send events to non-background windows + if (!listeners[i]->AsInner()->IsCurrentInnerWindow() || + listeners[i]->GetOuterWindow()->IsBackground()) { + continue; + } + + bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex); + + RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex); + if (listenerGamepad) { + listenerGamepad->SetButton(aButton, aPressed, aValue); + if (firstTime) { + FireConnectionEvent(listeners[i], listenerGamepad, true); + } + if (mNonstandardEventsEnabled) { + // Fire event + FireButtonEvent(listeners[i], listenerGamepad, aButton, aValue); + } + } + } +} + +void +GamepadManager::FireButtonEvent(EventTarget* aTarget, + Gamepad* aGamepad, + uint32_t aButton, + double aValue) +{ + nsString name = aValue == 1.0L ? NS_LITERAL_STRING("gamepadbuttondown") : + NS_LITERAL_STRING("gamepadbuttonup"); + 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); + + bool defaultActionEnabled = true; + aTarget->DispatchEvent(event, &defaultActionEnabled); +} + +void +GamepadManager::NewAxisMoveEvent(uint32_t aIndex, GamepadServiceType aServiceType, + uint32_t aAxis, double aValue) +{ + if (mShuttingDown) { + return; + } + + uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType); + + RefPtr<Gamepad> gamepad = GetGamepad(newIndex); + if (!gamepad) { + return; + } + gamepad->SetAxis(aAxis, aValue); + + // Hold on to listeners in a separate array because firing events + // can mutate the mListeners array. + nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners); + MOZ_ASSERT(!listeners.IsEmpty()); + + for (uint32_t i = 0; i < listeners.Length(); i++) { + + MOZ_ASSERT(listeners[i]->IsInnerWindow()); + + // Only send events to non-background windows + if (!listeners[i]->AsInner()->IsCurrentInnerWindow() || + listeners[i]->GetOuterWindow()->IsBackground()) { + continue; + } + + bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex); + + RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex); + if (listenerGamepad) { + listenerGamepad->SetAxis(aAxis, aValue); + if (firstTime) { + FireConnectionEvent(listeners[i], listenerGamepad, true); + } + if (mNonstandardEventsEnabled) { + // Fire event + FireAxisMoveEvent(listeners[i], listenerGamepad, aAxis, aValue); + } + } + } +} + +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, + NS_LITERAL_STRING("gamepadaxismove"), + init); + + event->SetTrusted(true); + + bool defaultActionEnabled = true; + aTarget->DispatchEvent(event, &defaultActionEnabled); +} + +void +GamepadManager::NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType, + const GamepadPoseState& aPose) +{ + if (mShuttingDown) { + return; + } + + uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType); + + RefPtr<Gamepad> gamepad = GetGamepad(newIndex); + if (!gamepad) { + return; + } + gamepad->SetPose(aPose); + + // Hold on to listeners in a separate array because firing events + // can mutate the mListeners array. + nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners); + MOZ_ASSERT(!listeners.IsEmpty()); + + for (uint32_t i = 0; i < listeners.Length(); i++) { + + MOZ_ASSERT(listeners[i]->IsInnerWindow()); + + // Only send events to non-background windows + if (!listeners[i]->AsInner()->IsCurrentInnerWindow() || + listeners[i]->GetOuterWindow()->IsBackground()) { + continue; + } + + bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex); + + RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex); + if (listenerGamepad) { + listenerGamepad->SetPose(aPose); + if (firstTime) { + FireConnectionEvent(listeners[i], listenerGamepad, true); + } + } + } +} + +void +GamepadManager::NewConnectionEvent(uint32_t aIndex, bool aConnected) +{ + if (mShuttingDown) { + return; + } + + RefPtr<Gamepad> gamepad = GetGamepad(aIndex); + if (!gamepad) { + return; + } + + // Hold on to listeners in a separate array because firing events + // can mutate the mListeners array. + nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners); + MOZ_ASSERT(!listeners.IsEmpty()); + + if (aConnected) { + for (uint32_t i = 0; i < listeners.Length(); i++) { + + MOZ_ASSERT(listeners[i]->IsInnerWindow()); + + // Only send events to non-background windows + if (!listeners[i]->AsInner()->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], aIndex); + + RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex); + 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. + + if (WindowHasSeenGamepad(listeners[i], aIndex)) { + RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex); + if (listenerGamepad) { + listenerGamepad->SetConnected(false); + // Fire event + FireConnectionEvent(listeners[i], listenerGamepad, false); + listeners[i]->RemoveGamepad(aIndex); + } + } + } + } +} + +void +GamepadManager::FireConnectionEvent(EventTarget* aTarget, + Gamepad* aGamepad, + bool aConnected) +{ + nsString name = aConnected ? NS_LITERAL_STRING("gamepadconnected") : + NS_LITERAL_STRING("gamepaddisconnected"); + GamepadEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mGamepad = aGamepad; + RefPtr<GamepadEvent> event = + GamepadEvent::Constructor(aTarget, name, init); + + event->SetTrusted(true); + + bool defaultActionEnabled = true; + aTarget->DispatchEvent(event, &defaultActionEnabled); +} + +void +GamepadManager::SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad) +{ + if (mShuttingDown || !mEnabled) { + return; + } + + RefPtr<Gamepad> gamepad = GetGamepad(aIndex); + 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(); +} + +// static +bool +GamepadManager::IsAPIEnabled() { + return Preferences::GetBool(kGamepadEnabledPref, false); +} + +bool +GamepadManager::MaybeWindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) +{ + if (!WindowHasSeenGamepad(aWindow, aIndex)) { + // This window hasn't seen this gamepad before, so + // send a connection event first. + SetWindowHasSeenGamepad(aWindow, aIndex); + return true; + } + return false; +} + +bool +GamepadManager::WindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) const +{ + RefPtr<Gamepad> gamepad = aWindow->GetGamepad(aIndex); + return gamepad != nullptr; +} + +void +GamepadManager::SetWindowHasSeenGamepad(nsGlobalWindow* aWindow, + uint32_t aIndex, + bool aHasSeen) +{ + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsInnerWindow()); + + 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(aIndex); + if (!gamepad) { + return; + } + RefPtr<Gamepad> clonedGamepad = gamepad->Clone(window); + aWindow->AddGamepad(aIndex, clonedGamepad); + } else { + aWindow->RemoveGamepad(aIndex); + } +} + +void +GamepadManager::Update(const GamepadChangeEvent& aEvent) +{ + if (aEvent.type() == GamepadChangeEvent::TGamepadAdded) { + const GamepadAdded& a = aEvent.get_GamepadAdded(); + AddGamepad(a.index(), a.id(), + static_cast<GamepadMappingType>(a.mapping()), + a.service_type(), + a.num_buttons(), a.num_axes()); + return; + } + if (aEvent.type() == GamepadChangeEvent::TGamepadRemoved) { + const GamepadRemoved& a = aEvent.get_GamepadRemoved(); + RemoveGamepad(a.index(), a.service_type()); + return; + } + if (aEvent.type() == GamepadChangeEvent::TGamepadButtonInformation) { + const GamepadButtonInformation& a = aEvent.get_GamepadButtonInformation(); + NewButtonEvent(a.index(), a.service_type(), a.button(), + a.pressed(), a.value()); + return; + } + if (aEvent.type() == GamepadChangeEvent::TGamepadAxisInformation) { + const GamepadAxisInformation& a = aEvent.get_GamepadAxisInformation(); + NewAxisMoveEvent(a.index(), a.service_type(), a.axis(), a.value()); + return; + } + if (aEvent.type() == GamepadChangeEvent::TGamepadPoseInformation) { + const GamepadPoseInformation& a = aEvent.get_GamepadPoseInformation(); + NewPoseEvent(a.index(), a.service_type(), a.pose_state()); + return; + } + + MOZ_CRASH("We shouldn't be here!"); + +} + +//Override nsIIPCBackgroundChildCreateCallback +void +GamepadManager::ActorCreated(PBackgroundChild *aActor) +{ + MOZ_ASSERT(aActor); + GamepadEventChannelChild *child = new GamepadEventChannelChild(); + PGamepadEventChannelChild *initedChild = + aActor->SendPGamepadEventChannelConstructor(child); + if (NS_WARN_IF(!initedChild)) { + ActorFailed(); + return; + } + MOZ_ASSERT(initedChild == child); + child->SendGamepadListenerAdded(); + mChannelChildren.AppendElement(child); + +#if defined(XP_WIN) || defined(XP_MACOSX) || defined(XP_LINUX) + // Construct VRManagerChannel and ask adding the connected + // VR controllers to GamepadManager + mVRChannelChild = gfx::VRManagerChild::Get(); + mVRChannelChild->SendControllerListenerAdded(); +#endif +} + +//Override nsIIPCBackgroundChildCreateCallback +void +GamepadManager::ActorFailed() +{ + MOZ_CRASH("Gamepad IPC actor create failed!"); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/GamepadManager.h b/dom/gamepad/GamepadManager.h new file mode 100644 index 000000000..1bb437d8f --- /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 "nsIIPCBackgroundChildCreateCallback.h" +#include "nsIObserver.h" +// Needed for GamepadMappingType +#include "mozilla/dom/GamepadBinding.h" +#include "mozilla/dom/GamepadServiceType.h" + +class nsGlobalWindow; + +namespace mozilla { +namespace gfx { +class VRManagerChild; +} // namespace gfx +namespace dom { + +class EventTarget; +class Gamepad; +class GamepadChangeEvent; +class GamepadEventChannelChild; + +class GamepadManager final : public nsIObserver, + public nsIIPCBackgroundChildCreateCallback +{ + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK + + // Returns true if we actually have a service up and running + static bool IsServiceRunning(); + // Get the singleton service + static already_AddRefed<GamepadManager> GetService(); + // Return true if the API is preffed on. + static bool IsAPIEnabled(); + + void BeginShutdown(); + void StopMonitoring(); + + // Indicate that |aWindow| wants to receive gamepad events. + void AddListener(nsGlobalWindow* aWindow); + // Indicate that |aWindow| should no longer receive gamepad events. + void RemoveListener(nsGlobalWindow* aWindow); + + // Add a gamepad to the list of known gamepads. + void AddGamepad(uint32_t aIndex, const nsAString& aID, GamepadMappingType aMapping, + GamepadServiceType aServiceType, uint32_t aNumButtons, uint32_t aNumAxes); + + // Remove the gamepad at |aIndex| from the list of known gamepads. + void RemoveGamepad(uint32_t aIndex, GamepadServiceType aServiceType); + + // Update the state of |aButton| for the gamepad at |aIndex| 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, aValue is for analog buttons. + void NewButtonEvent(uint32_t aIndex, GamepadServiceType aServiceType, uint32_t aButton, + bool aPressed, double aValue); + + // Update the state of |aAxis| for the gamepad at |aIndex| for all + // windows that are listening and visible, and fire a gamepadaxismove + // event at them as well. + void NewAxisMoveEvent(uint32_t aIndex, GamepadServiceType aServiceType, + uint32_t aAxis, double aValue); + + // Update the state of |aState| for the gamepad at |aIndex| for all + // windows that are listening and visible. + void NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType, + const GamepadPoseState& aState); + + // Synchronize the state of |aGamepad| to match the gamepad stored at |aIndex| + void SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad); + + // Returns gamepad object if index exists, null otherwise + already_AddRefed<Gamepad> GetGamepad(uint32_t aIndex) const; + + // Receive GamepadChangeEvent messages from parent process to fire DOM events + void Update(const GamepadChangeEvent& aGamepadEvent); + + protected: + GamepadManager(); + ~GamepadManager() {}; + + // Fire a gamepadconnected or gamepaddisconnected event for the gamepad + // at |aIndex| to all windows that are listening and have received + // gamepad input. + void NewConnectionEvent(uint32_t aIndex, 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; + + // Gamepad IPDL child + // This pointer is only used by this singleton instance and + // will be destroyed during the IPDL shutdown chain, so we + // don't need to refcount it here. + nsTArray<GamepadEventChannelChild *> mChannelChildren; + gfx::VRManagerChild* mVRChannelChild; + + private: + + nsresult Init(); + + bool MaybeWindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex); + // 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(nsGlobalWindow* aWindow, uint32_t aIndex) const; + // Indicate that a window has received data from a gamepad. + void SetWindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex, + bool aHasSeen = true); + // Our gamepad index has VR_GAMEPAD_IDX_OFFSET while GamepadChannelType + // is from VRManager. + uint32_t GetGamepadIndexWithServiceType(uint32_t aIndex, GamepadServiceType aServiceType); + + // Gamepads connected to the system. Copies of these are handed out + // to each window. + nsRefPtrHashtable<nsUint32HashKey, Gamepad> mGamepads; + // Inner windows that are listening for gamepad events. + // has been sent to that window. + nsTArray<RefPtr<nsGlobalWindow>> mListeners; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_GamepadManager_h_ diff --git a/dom/gamepad/GamepadMonitoring.cpp b/dom/gamepad/GamepadMonitoring.cpp new file mode 100644 index 000000000..ac45aea57 --- /dev/null +++ b/dom/gamepad/GamepadMonitoring.cpp @@ -0,0 +1,32 @@ +/* -*- 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/GamepadMonitoring.h" +#include "mozilla/dom/GamepadPlatformService.h" +#include "mozilla/ipc/BackgroundParent.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace dom { + +void +MaybeStopGamepadMonitoring() +{ + AssertIsOnBackgroundThread(); + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + if(service->HasGamepadListeners()) { + return; + } + StopGamepadMonitoring(); + service->ResetGamepadIndexes(); + service->MaybeShutdown(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/GamepadMonitoring.h b/dom/gamepad/GamepadMonitoring.h new file mode 100644 index 000000000..bf231c4dd --- /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_ + +namespace mozilla { +namespace dom { +// Functions for platform specific gamepad monitoring. + +void MaybeStopGamepadMonitoring(); + +// These two functions are implemented in the platform specific service files +// (linux/LinuxGamepad.cpp, cocoa/CocoaGamepad.cpp, etc) +void StartGamepadMonitoring(); +void StopGamepadMonitoring(); + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/gamepad/GamepadPlatformService.cpp b/dom/gamepad/GamepadPlatformService.cpp new file mode 100644 index 000000000..3df967aec --- /dev/null +++ b/dom/gamepad/GamepadPlatformService.cpp @@ -0,0 +1,265 @@ +/* -*- 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/ipc/BackgroundParent.h" +#include "mozilla/Mutex.h" +#include "mozilla/Unused.h" + +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsIThread.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace dom { + +namespace { + +// This is the singleton instance of GamepadPlatformService, can be called +// by both background and monitor thread. +StaticRefPtr<GamepadPlatformService> gGamepadPlatformServiceSingleton; + +} //namepsace + +GamepadPlatformService::GamepadPlatformService() + : mGamepadIndex(0), + 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(const T& aInfo) +{ + // This method is called by monitor populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + + GamepadChangeEvent e(aInfo); + + // mChannelParents may be accessed by background thread in the + // same time, we use mutex to prevent possible race condtion + MutexAutoLock autoLock(mMutex); + + // Buffer all events if we have no Channel to dispatch, which + // may happen when performing Mochitest. + if (mChannelParents.IsEmpty()) { + mPendingEvents.AppendElement(e); + return; + } + + for(uint32_t i = 0; i < mChannelParents.Length(); ++i) { + mChannelParents[i]->DispatchUpdateEvent(e); + } +} + +uint32_t +GamepadPlatformService::AddGamepad(const char* aID, + GamepadMappingType aMapping, + uint32_t aNumButtons, uint32_t aNumAxes) +{ + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + + uint32_t index = ++mGamepadIndex; + GamepadAdded a(NS_ConvertUTF8toUTF16(nsDependentCString(aID)), index, + static_cast<uint32_t>(aMapping), GamepadServiceType::Standard, aNumButtons, aNumAxes); + NotifyGamepadChange<GamepadAdded>(a); + return index; +} + +void +GamepadPlatformService::RemoveGamepad(uint32_t aIndex) +{ + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + GamepadRemoved a(aIndex, GamepadServiceType::Standard); + NotifyGamepadChange<GamepadRemoved>(a); +} + +void +GamepadPlatformService::NewButtonEvent(uint32_t aIndex, 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()); + GamepadButtonInformation a(aIndex, GamepadServiceType::Standard, + aButton, aPressed, aValue); + NotifyGamepadChange<GamepadButtonInformation>(a); +} + +void +GamepadPlatformService::NewButtonEvent(uint32_t aIndex, 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(aIndex, aButton, aPressed, aPressed ? 1.0L : 0.0L); +} + +void +GamepadPlatformService::NewAxisMoveEvent(uint32_t aIndex, 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(aIndex, GamepadServiceType::Standard, + aAxis, aValue); + NotifyGamepadChange<GamepadAxisInformation>(a); +} + +void +GamepadPlatformService::ResetGamepadIndexes() +{ + // This method is called by monitor thread populated in + // platform-dependent backends + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + mGamepadIndex = 0; +} + +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); + FlushPendingEvents(); +} + +void +GamepadPlatformService::FlushPendingEvents() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mChannelParents.IsEmpty()); + + if (mPendingEvents.IsEmpty()) { + return; + } + + // NOTE: This method must be called with mMutex held because it accesses + // mChannelParents. + for (uint32_t i=0; i<mChannelParents.Length(); ++i) { + for (uint32_t j=0; j<mPendingEvents.Length();++j) { + mChannelParents[i]->DispatchUpdateEvent(mPendingEvents[j]); + } + } + mPendingEvents.Clear(); +} + +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); +} + +bool +GamepadPlatformService::HasGamepadListeners() +{ + // mChannelParents may be accessed by background thread in the + // same time, we use mutex to prevent possible race condtion + AssertIsOnBackgroundThread(); + + // We use mutex here to prevent race condition with monitor thread + MutexAutoLock autoLock(mMutex); + for (uint32_t i = 0; i < mChannelParents.Length(); i++) { + if(mChannelParents[i]->HasGamepadListener()) { + return true; + } + } + return false; +} + +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; + } + } +} + +void +GamepadPlatformService::Cleanup() +{ + // This method is called when GamepadPlatformService is + // successfully distructed in background thread + AssertIsOnBackgroundThread(); + + MutexAutoLock autoLock(mMutex); + mChannelParents.Clear(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/GamepadPlatformService.h b/dom/gamepad/GamepadPlatformService.h new file mode 100644 index 000000000..0a61281e4 --- /dev/null +++ b/dom/gamepad/GamepadPlatformService.h @@ -0,0 +1,104 @@ +/* -*- 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/Mutex.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +namespace dom { + +class GamepadEventChannelParent; + +// 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 index. + uint32_t AddGamepad(const char* aID, GamepadMappingType aMapping, + uint32_t aNumButtons, uint32_t aNumAxes); + // Remove the gamepad at |aIndex| from the list of known gamepads. + void RemoveGamepad(uint32_t aIndex); + + // Update the state of |aButton| for the gamepad at |aIndex| 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, aValue is for analog buttons. + void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed, + double aValue); + // When only a digital button is available the value will be synthesized. + void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed); + + // Update the state of |aAxis| for the gamepad at |aIndex| for all + // windows that are listening and visible, and fire a gamepadaxismove + // event at them as well. + void NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue); + + // 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); + + bool HasGamepadListeners(); + + void MaybeShutdown(); + + private: + GamepadPlatformService(); + ~GamepadPlatformService(); + template<class T> void NotifyGamepadChange(const T& aInfo); + + // Flush all pending events buffered in mPendingEvents, must be called + // with mMutex held + void FlushPendingEvents(); + void Cleanup(); + + // mGamepadIndex can only be accessed by monitor thread + uint32_t mGamepadIndex; + + // 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; + + // In mochitest, it is possible that the test Events is synthesized + // before GamepadEventChannel created, we need to buffer all events + // until the channel is created if that happens. + nsTArray<GamepadChangeEvent> mPendingEvents; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/gamepad/GamepadPose.cpp b/dom/gamepad/GamepadPose.cpp new file mode 100644 index 000000000..25d157b8b --- /dev/null +++ b/dom/gamepad/GamepadPose.cpp @@ -0,0 +1,120 @@ +/* -*- 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 { +namespace 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 GamepadPoseBinding::Wrap(aJSContext, this, aGivenProto); +} + +bool +GamepadPose::HasOrientation() const +{ + return bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position); +} + +bool +GamepadPose::HasPosition() const +{ + return bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation); +} + +void +GamepadPose::GetPosition(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) +{ + SetFloat32Array(aJSContext, aRetval, mPosition, mPoseState.position, 3, + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position), aRv); +} + +void +GamepadPose::GetLinearVelocity(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) +{ + SetFloat32Array(aJSContext, aRetval, mLinearVelocity, mPoseState.linearVelocity, 3, + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position), aRv); +} + +void +GamepadPose::GetLinearAcceleration(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) +{ + SetFloat32Array(aJSContext, aRetval, mLinearAcceleration, mPoseState.linearAcceleration, 3, + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_LinearAcceleration), aRv); +} + +void +GamepadPose::GetOrientation(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) +{ + SetFloat32Array(aJSContext, aRetval, mOrientation, mPoseState.orientation, 4, + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation), aRv); +} + +void +GamepadPose::GetAngularVelocity(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) +{ + SetFloat32Array(aJSContext, aRetval, mAngularVelocity, mPoseState.angularVelocity, 3, + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation), aRv); +} + +void +GamepadPose::GetAngularAcceleration(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) +{ + SetFloat32Array(aJSContext, aRetval, mAngularAcceleration, mPoseState.angularAcceleration, 3, + bool(mPoseState.flags & GamepadCapabilityFlags::Cap_AngularAcceleration), aRv); +} + +void +GamepadPose::SetPoseState(const GamepadPoseState& aPose) +{ + mPoseState = aPose; +} + +const GamepadPoseState& +GamepadPose::GetPoseState() +{ + return mPoseState; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/GamepadPose.h b/dom/gamepad/GamepadPose.h new file mode 100644 index 000000000..fa32df3fc --- /dev/null +++ b/dom/gamepad/GamepadPose.h @@ -0,0 +1,58 @@ +/* -*- 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" + +namespace mozilla { +namespace 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 dom +} // namespace mozilla + +#endif // mozilla_dom_gamepad_GamepadPose_h diff --git a/dom/gamepad/GamepadPoseState.h b/dom/gamepad/GamepadPoseState.h new file mode 100644 index 000000000..958b26139 --- /dev/null +++ b/dom/gamepad/GamepadPoseState.h @@ -0,0 +1,88 @@ + +#ifndef mozilla_dom_gamepad_GamepadPoseState_h_ +#define mozilla_dom_gamepad_GamepadPoseState_h_ + +namespace mozilla{ +namespace 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_All used for validity checking during IPC serialization + */ + Cap_All = (1 << 5) - 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]; + + GamepadPoseState() + { + Clear(); + } + + 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]; + } + + bool operator!=(const GamepadPoseState& aPose) const + { + return !(*this == aPose); + } + + void Clear() { + memset(this, 0, sizeof(GamepadPoseState)); + } +}; + +}// namespace dom +}// namespace mozilla + +#endif // mozilla_dom_gamepad_GamepadPoseState_h_
\ No newline at end of file diff --git a/dom/gamepad/GamepadServiceTest.cpp b/dom/gamepad/GamepadServiceTest.cpp new file mode 100644 index 000000000..a6fde58f0 --- /dev/null +++ b/dom/gamepad/GamepadServiceTest.cpp @@ -0,0 +1,283 @@ +/* -*- 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" + +#include "mozilla/Unused.h" + +#include "nsIObserver.h" +#include "nsIObserverService.h" + +namespace mozilla { +namespace 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. + */ + +NS_IMPL_CYCLE_COLLECTION_CLASS(GamepadServiceTest) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(GamepadServiceTest, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(GamepadServiceTest, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(GamepadServiceTest) + NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback) +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() {} + +void +GamepadServiceTest::InitPBackgroundActor() +{ + MOZ_ASSERT(!mChild); + PBackgroundChild *actor = BackgroundChild::GetForCurrentThread(); + //Try to get the PBackground Child actor + if (actor) { + ActorCreated(actor); + } else { + Unused << BackgroundChild::GetOrCreateForCurrentThread(this); + } +} + +void +GamepadServiceTest::DestroyPBackgroundActor() +{ + if (mChild) { + // If mChild exists, which means that IPDL channel + // has been created, our pending operations should + // be empty. + MOZ_ASSERT(mPendingOperations.IsEmpty()); + mChild->SendShutdownChannel(); + mChild = nullptr; + } else { + // If the IPDL channel has not been created and we + // want to destroy it now, just cancel all pending + // operations. + mPendingOperations.Clear(); + } +} + +already_AddRefed<Promise> +GamepadServiceTest::AddGamepad(const nsAString& aID, + uint32_t aMapping, + uint32_t aNumButtons, + uint32_t aNumAxes, + ErrorResult& aRv) +{ + if (mShuttingDown) { + return nullptr; + } + + GamepadAdded a(nsString(aID), 0, + aMapping, + GamepadServiceType::Standard, + aNumButtons, aNumAxes); + GamepadChangeEvent e(a); + nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(mWindow); + + RefPtr<Promise> p = Promise::Create(go, aRv); + if (aRv.Failed()) { + return nullptr; + } + + uint32_t id = ++mEventNumber; + if (mChild) { + mChild->AddPromise(id, p); + mChild->SendGamepadTestEvent(id, e); + } else { + PendingOperation op(id, e, p); + mPendingOperations.AppendElement(op); + } + return p.forget(); +} + +void +GamepadServiceTest::RemoveGamepad(uint32_t aIndex) +{ + if (mShuttingDown) { + return; + } + + GamepadRemoved a(aIndex, GamepadServiceType::Standard); + GamepadChangeEvent e(a); + + uint32_t id = ++mEventNumber; + if (mChild) { + mChild->SendGamepadTestEvent(id, e); + } else { + PendingOperation op(id, e); + mPendingOperations.AppendElement(op); + } +} + +void +GamepadServiceTest::NewButtonEvent(uint32_t aIndex, + uint32_t aButton, + bool aPressed) +{ + if (mShuttingDown) { + return; + } + + GamepadButtonInformation a(aIndex, GamepadServiceType::Standard, + aButton, aPressed, aPressed ? 1.0 : 0); + GamepadChangeEvent e(a); + + uint32_t id = ++mEventNumber; + if (mChild) { + mChild->SendGamepadTestEvent(id, e); + } else { + PendingOperation op(id, e); + mPendingOperations.AppendElement(op); + } +} + +void +GamepadServiceTest::NewButtonValueEvent(uint32_t aIndex, + uint32_t aButton, + bool aPressed, + double aValue) +{ + if (mShuttingDown) { + return; + } + + GamepadButtonInformation a(aIndex, GamepadServiceType::Standard, + aButton, aPressed, aValue); + GamepadChangeEvent e(a); + + uint32_t id = ++mEventNumber; + if (mChild) { + mChild->SendGamepadTestEvent(id, e); + } else { + PendingOperation op(id, e); + mPendingOperations.AppendElement(op); + } +} + +void +GamepadServiceTest::NewAxisMoveEvent(uint32_t aIndex, + uint32_t aAxis, + double aValue) +{ + if (mShuttingDown) { + return; + } + + GamepadAxisInformation a(aIndex, GamepadServiceType::Standard, + aAxis, aValue); + GamepadChangeEvent e(a); + + uint32_t id = ++mEventNumber; + if (mChild) { + mChild->SendGamepadTestEvent(id, e); + } else { + PendingOperation op(id, e); + mPendingOperations.AppendElement(op); + } +} + +void +GamepadServiceTest::FlushPendingOperations() +{ + for (uint32_t i=0; i < mPendingOperations.Length(); ++i) { + PendingOperation op = mPendingOperations[i]; + if (op.mPromise) { + mChild->AddPromise(op.mID, op.mPromise); + } + mChild->SendGamepadTestEvent(op.mID, op.mEvent); + } + mPendingOperations.Clear(); +} + +void +GamepadServiceTest::ActorCreated(PBackgroundChild* aActor) +{ + MOZ_ASSERT(aActor); + // If we are shutting down, we don't need to create the + // IPDL child/parent pair anymore. + if (mShuttingDown) { + // mPendingOperations should be cleared in + // DestroyPBackgroundActor() + MOZ_ASSERT(mPendingOperations.IsEmpty()); + return; + } + + mChild = new GamepadTestChannelChild(); + PGamepadTestChannelChild* initedChild = + aActor->SendPGamepadTestChannelConstructor(mChild); + if (NS_WARN_IF(!initedChild)) { + ActorFailed(); + return; + } + FlushPendingOperations(); +} + +void +GamepadServiceTest::ActorFailed() +{ + MOZ_CRASH("Failed to create background child actor!"); +} + +JSObject* +GamepadServiceTest::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) +{ + return GamepadServiceTestBinding::Wrap(aCx, this, aGivenProto); +} + +} // dom +} // mozilla diff --git a/dom/gamepad/GamepadServiceTest.h b/dom/gamepad/GamepadServiceTest.h new file mode 100644 index 000000000..6ef9fcc19 --- /dev/null +++ b/dom/gamepad/GamepadServiceTest.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/. */ + +#ifndef mozilla_dom_GamepadServiceTest_h_ +#define mozilla_dom_GamepadServiceTest_h_ + +#include "nsIIPCBackgroundChildCreateCallback.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/GamepadBinding.h" + +namespace mozilla { +namespace dom { + +class GamepadChangeEvent; +class GamepadManager; +class GamepadTestChannelChild; +class Promise; + +// Service for testing purposes +class GamepadServiceTest final : public DOMEventTargetHelper, + public nsIIPCBackgroundChildCreateCallback +{ +public: + NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(GamepadServiceTest, + DOMEventTargetHelper) + + uint32_t NoMapping() const { return 0; } + uint32_t StandardMapping() const { return 1; } + + already_AddRefed<Promise> AddGamepad(const nsAString& aID, + uint32_t aMapping, + uint32_t aNumButtons, + uint32_t aNumAxes, + ErrorResult& aRv); + void RemoveGamepad(uint32_t aIndex); + void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed); + void NewButtonValueEvent(uint32_t aIndex, uint32_t aButton, bool aPressed, double aValue); + void NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue); + void Shutdown(); + + static already_AddRefed<GamepadServiceTest> CreateTestService(nsPIDOMWindowInner* aWindow); + nsPIDOMWindowInner* GetParentObject() const { return mWindow; } + JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override; + +private: + + // We need to asynchronously create IPDL channel, it is possible that + // we send commands before the channel is created, so we have to buffer + // them until the channel is created in that case. + struct PendingOperation { + explicit PendingOperation(const uint32_t& aID, + const GamepadChangeEvent& aEvent, + Promise* aPromise = nullptr) + : mID(aID), mEvent(aEvent), mPromise(aPromise) {} + uint32_t mID; + const GamepadChangeEvent& mEvent; + RefPtr<Promise> mPromise; + }; + + // 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; + nsTArray<PendingOperation> mPendingOperations; + 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 + GamepadTestChannelChild* MOZ_NON_OWNING_REF mChild; + + explicit GamepadServiceTest(nsPIDOMWindowInner* aWindow); + ~GamepadServiceTest(); + void InitPBackgroundActor(); + void DestroyPBackgroundActor(); + void FlushPendingOperations(); + +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/gamepad/android/AndroidGamepad.cpp b/dom/gamepad/android/AndroidGamepad.cpp new file mode 100644 index 000000000..706d02617 --- /dev/null +++ b/dom/gamepad/android/AndroidGamepad.cpp @@ -0,0 +1,84 @@ +/* -*- 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 "GeneratedJNIWrappers.h" +#include "GeneratedJNINatives.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace dom { + +class AndroidGamepadManager final + : public java::AndroidGamepadManager::Natives<AndroidGamepadManager> +{ + AndroidGamepadManager() = delete; + +public: + static void + OnGamepadChange(int32_t aID, bool aAdded) + { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + if (aAdded) { + const int svc_id = service->AddGamepad( + "android", GamepadMappingType::Standard, + kStandardGamepadButtons, kStandardGamepadAxes); + java::AndroidGamepadManager::OnGamepadAdded(aID, svc_id); + + } else { + service->RemoveGamepad(aID); + } + } + + static void + OnButtonChange(int32_t aID, int32_t aButton, bool aPressed, float aValue) + { + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + service->NewButtonEvent(aID, aButton, aPressed, aValue); + } + + static void + OnAxisChange(int32_t aID, jni::BooleanArray::Param aValid, + jni::FloatArray::Param aValues) + { + 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++) { + service->NewAxisMoveEvent(aID, i, values[i]); + } + } +}; + +void StartGamepadMonitoring() +{ + AndroidGamepadManager::Init(); + java::AndroidGamepadManager::Start(); +} + +void StopGamepadMonitoring() +{ + java::AndroidGamepadManager::Stop(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/cocoa/CocoaGamepad.cpp b/dom/gamepad/cocoa/CocoaGamepad.cpp new file mode 100644 index 000000000..e7c986e22 --- /dev/null +++ b/dom/gamepad/cocoa/CocoaGamepad.cpp @@ -0,0 +1,589 @@ +/* -*- 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/GamepadPlatformService.h" +#include "mozilla/ArrayUtils.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; +using std::vector; + +struct Button { + int id; + bool analog; + IOHIDElementRef element; + CFIndex min; + CFIndex max; + + Button(int aId, IOHIDElementRef aElement, CFIndex aMin, CFIndex aMax) : + id(aId), + analog((aMax - aMin) > 1), + element(aElement), + min(aMin), + max(aMax) {} +}; + +struct Axis { + int id; + IOHIDElementRef element; + uint32_t usagePage; + uint32_t usage; + CFIndex min; + CFIndex max; +}; + +typedef bool dpad_buttons[4]; + +// 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; + + +class Gamepad { + private: + IOHIDDeviceRef mDevice; + nsTArray<Button> buttons; + nsTArray<Axis> axes; + IOHIDElementRef mDpad; + dpad_buttons mDpadState; + + public: + Gamepad() : mDevice(nullptr), mDpad(nullptr), mSuperIndex(-1) {} + bool operator==(IOHIDDeviceRef device) const { return mDevice == device; } + bool empty() const { return mDevice == nullptr; } + void clear() + { + mDevice = nullptr; + buttons.Clear(); + axes.Clear(); + mDpad = nullptr; + mSuperIndex = -1; + } + void init(IOHIDDeviceRef device); + size_t numButtons() { return buttons.Length() + (mDpad ? 4 : 0); } + size_t numAxes() { return axes.Length(); } + + // Index given by our superclass. + uint32_t mSuperIndex; + + bool isDpad(IOHIDElementRef element) const + { + return element == mDpad; + } + + const dpad_buttons& getDpadState() const + { + return mDpadState; + } + + void setDpadState(const dpad_buttons& dpadState) + { + for (unsigned i = 0; i < ArrayLength(mDpadState); i++) { + mDpadState[i] = dpadState[i]; + } + } + + const Button* lookupButton(IOHIDElementRef element) const + { + for (unsigned i = 0; i < buttons.Length(); i++) { + if (buttons[i].element == element) + return &buttons[i]; + } + return nullptr; + } + + const Axis* lookupAxis(IOHIDElementRef element) const + { + for (unsigned i = 0; i < axes.Length(); i++) { + if (axes[i].element == element) + return &axes[i]; + } + return nullptr; + } +}; + +class AxisComparator { +public: + bool Equals(const Axis& a1, const Axis& a2) const + { + return a1.usagePage == a2.usagePage && a1.usage == a2.usage; + } + bool LessThan(const Axis& a1, const Axis& a2) const + { + if (a1.usagePage == a2.usagePage) { + return a1.usage < a2.usage; + } + return a1.usagePage < a2.usagePage; + } +}; + +void Gamepad::init(IOHIDDeviceRef device) +{ + clear(); + mDevice = device; + + CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device, + 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 = { int(axes.Length()), + 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) { + mDpad = element; + } else if ((usagePage == kSimUsagePage && + (usage == kAcceleratorUsage || + usage == kBrakeUsage)) || + (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 + } + } + + AxisComparator comparator; + axes.Sort(comparator); + for (unsigned i = 0; i < axes.Length(); i++) { + axes[i].id = i; + } +} + +class DarwinGamepadService { + private: + IOHIDManagerRef mManager; + vector<Gamepad> mGamepads; + + //Workaround to support running in background thread + CFRunLoopRef mMonitorRunLoop; + nsCOMPtr<nsIThread> mMonitorThread; + + 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); + + void DeviceAdded(IOHIDDeviceRef device); + void DeviceRemoved(IOHIDDeviceRef device); + void InputValueChanged(IOHIDValueRef value); + void StartupInternal(); + + public: + DarwinGamepadService(); + ~DarwinGamepadService(); + void Startup(); + void Shutdown(); + friend class DarwinGamepadServiceStartupRunnable; +}; + +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) + : mService(service) {} + NS_IMETHOD Run() override + { + MOZ_ASSERT(mService); + mService->StartupInternal(); + 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.size(); i++) { + if (mGamepads[i] == device) + return; + if (slot == size_t(-1) && mGamepads[i].empty()) + slot = i; + } + + if (slot == size_t(-1)) { + slot = mGamepads.size(); + mGamepads.push_back(Gamepad()); + } + mGamepads[slot].init(device); + + // 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]; + sprintf(buffer, "%x-%x-%s", vendorId, productId, product_name); + uint32_t index = service->AddGamepad(buffer, + mozilla::dom::GamepadMappingType::_empty, + (int)mGamepads[slot].numButtons(), + (int)mGamepads[slot].numAxes()); + mGamepads[slot].mSuperIndex = index; +} + +void +DarwinGamepadService::DeviceRemoved(IOHIDDeviceRef device) +{ + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + for (size_t i = 0; i < mGamepads.size(); i++) { + if (mGamepads[i] == device) { + service->RemoveGamepad(mGamepads[i].mSuperIndex); + mGamepads[i].clear(); + return; + } + } +} + +/* + * Given a value from a d-pad (POV hat in USB HID terminology), + * represent it as 4 buttons, one for each cardinal direction. + */ +static void +UnpackDpad(int dpad_value, int min, int max, dpad_buttons& buttons) +{ + const unsigned kUp = 0; + const unsigned kDown = 1; + const unsigned kLeft = 2; + const unsigned kRight = 3; + + // Different controllers have different ways of representing + // "nothing is pressed", but they're all outside the range of values. + if (dpad_value < min || dpad_value > max) { + // Nothing is pressed. + return; + } + + // Normalize value to start at 0. + int value = dpad_value - min; + + // Value will be in the range 0-7. The value represents the + // position of the d-pad around a circle, with 0 being straight up, + // 2 being right, 4 being straight down, and 6 being left. + if (value < 2 || value > 6) { + buttons[kUp] = true; + } + if (value > 2 && value < 6) { + buttons[kDown] = true; + } + if (value > 4) { + buttons[kLeft] = true; + } + if (value > 0 && value < 4) { + buttons[kRight] = true; + } +} + +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 (unsigned i = 0; i < mGamepads.size(); i++) { + Gamepad &gamepad = mGamepads[i]; + if (gamepad == device) { + if (gamepad.isDpad(element)) { + const dpad_buttons& oldState = gamepad.getDpadState(); + dpad_buttons newState = { false, false, false, false }; + UnpackDpad(IOHIDValueGetIntegerValue(value), + IOHIDElementGetLogicalMin(element), + IOHIDElementGetLogicalMax(element), + newState); + const int numButtons = gamepad.numButtons(); + for (unsigned b = 0; b < ArrayLength(newState); b++) { + if (newState[b] != oldState[b]) { + service->NewButtonEvent(gamepad.mSuperIndex, + numButtons - 4 + b, + newState[b]); + } + } + gamepad.setDpadState(newState); + } else if (const Axis* axis = gamepad.lookupAxis(element)) { + double d = IOHIDValueGetIntegerValue(value); + double v = 2.0f * (d - axis->min) / + (double)(axis->max - axis->min) - 1.0f; + service->NewAxisMoveEvent(gamepad.mSuperIndex, axis->id, v); + } else if (const Button* button = gamepad.lookupButton(element)) { + int iv = IOHIDValueGetIntegerValue(value); + bool pressed = iv != 0; + double v = 0; + if (button->analog) { + double dv = iv; + v = (dv - button->min) / (double)(button->max - button->min); + } else { + v = pressed ? 1.0 : 0.0; + } + service->NewButtonEvent(gamepad.mSuperIndex, button->id, pressed, v); + } + 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); +} + +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) {} + +DarwinGamepadService::~DarwinGamepadService() +{ + if (mManager != nullptr) + CFRelease(mManager); +} + +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; + + // We held the handle of the CFRunLoop to make sure we + // can shut it down explicitly by CFRunLoopStop in another + // thread. + mMonitorRunLoop = CFRunLoopGetCurrent(); + + // CFRunLoopRun() is a blocking message loop when it's called in + // non-main thread so this thread cannot receive any other runnables + // and nsITimer timeout events after it's called. + CFRunLoopRun(); +} + +void DarwinGamepadService::Startup() +{ + Unused << NS_NewThread(getter_AddRefs(mMonitorThread), + new DarwinGamepadServiceStartupRunnable(this)); +} + +void DarwinGamepadService::Shutdown() +{ + IOHIDManagerRef manager = (IOHIDManagerRef)mManager; + CFRunLoopStop(mMonitorRunLoop); + if (manager) { + IOHIDManagerClose(manager, 0); + CFRelease(manager); + mManager = nullptr; + } + mMonitorThread->Shutdown(); +} + +} // namespace + +namespace mozilla { +namespace dom { + +DarwinGamepadService* gService = nullptr; + +void StartGamepadMonitoring() +{ + if (gService) { + return; + } + + gService = new DarwinGamepadService(); + gService->Startup(); +} + +void StopGamepadMonitoring() +{ + if (!gService) { + return; + } + + gService->Shutdown(); + delete gService; + gService = nullptr; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/fallback/FallbackGamepad.cpp b/dom/gamepad/fallback/FallbackGamepad.cpp new file mode 100644 index 000000000..f843a243c --- /dev/null +++ b/dom/gamepad/fallback/FallbackGamepad.cpp @@ -0,0 +1,20 @@ +/* -*- 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/. */ + + +namespace mozilla { +namespace dom { + +void StartGamepadMonitoring() +{ +} + +void StopGamepadMonitoring() +{ +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/ipc/GamepadEventChannelChild.cpp b/dom/gamepad/ipc/GamepadEventChannelChild.cpp new file mode 100644 index 000000000..4cdb55a92 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventChannelChild.cpp @@ -0,0 +1,42 @@ +/* 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" + +namespace mozilla { +namespace dom{ + +namespace { + +class GamepadUpdateRunnable final : public Runnable +{ + public: + explicit GamepadUpdateRunnable(const GamepadChangeEvent& aGamepadEvent) + : mEvent(aGamepadEvent) {} + NS_IMETHOD Run() override + { + RefPtr<GamepadManager> svc(GamepadManager::GetService()); + if (svc) { + svc->Update(mEvent); + } + return NS_OK; + } + protected: + GamepadChangeEvent mEvent; +}; + +} // namespace + +bool +GamepadEventChannelChild::RecvGamepadUpdate( + const GamepadChangeEvent& aGamepadEvent) +{ + DebugOnly<nsresult> rv = + NS_DispatchToMainThread(new GamepadUpdateRunnable(aGamepadEvent)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed"); + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/ipc/GamepadEventChannelChild.h b/dom/gamepad/ipc/GamepadEventChannelChild.h new file mode 100644 index 000000000..686cbd640 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventChannelChild.h @@ -0,0 +1,24 @@ +/* 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" + +#ifndef mozilla_dom_GamepadEventChannelChild_h_ +#define mozilla_dom_GamepadEventChannelChild_h_ + +namespace mozilla{ +namespace dom{ + +class GamepadEventChannelChild final : public PGamepadEventChannelChild +{ + public: + GamepadEventChannelChild() {} + ~GamepadEventChannelChild() {} + virtual bool + RecvGamepadUpdate(const GamepadChangeEvent& aGamepadEvent) override; +}; + +}// namespace dom +}// namespace mozilla + +#endif diff --git a/dom/gamepad/ipc/GamepadEventChannelParent.cpp b/dom/gamepad/ipc/GamepadEventChannelParent.cpp new file mode 100644 index 000000000..c3c8fd2c8 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventChannelParent.cpp @@ -0,0 +1,101 @@ +/* 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 "nsThreadUtils.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::ipc; + +namespace { + +class SendGamepadUpdateRunnable final : public Runnable +{ + private: + ~SendGamepadUpdateRunnable() {} + RefPtr<GamepadEventChannelParent> mParent; + GamepadChangeEvent mEvent; + public: + SendGamepadUpdateRunnable(GamepadEventChannelParent* aParent, + GamepadChangeEvent aEvent) + : mEvent(aEvent) + { + MOZ_ASSERT(aParent); + mParent = aParent; + } + NS_IMETHOD Run() override + { + AssertIsOnBackgroundThread(); + if(mParent->HasGamepadListener()) { + Unused << mParent->SendGamepadUpdate(mEvent); + } + return NS_OK; + } +}; + +} // namespace + +GamepadEventChannelParent::GamepadEventChannelParent() + : mHasGamepadListener(false) +{ + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + service->AddChannelParent(this); + mBackgroundThread = NS_GetCurrentThread(); +} + +bool +GamepadEventChannelParent::RecvGamepadListenerAdded() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mHasGamepadListener); + mHasGamepadListener = true; + StartGamepadMonitoring(); + return true; +} + +bool +GamepadEventChannelParent::RecvGamepadListenerRemoved() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mHasGamepadListener); + mHasGamepadListener = false; + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + service->RemoveChannelParent(this); + Unused << Send__delete__(this); + return true; +} + +void +GamepadEventChannelParent::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + + // It may be called because IPDL child side crashed, we'll + // not receive RecvGamepadListenerRemoved in that case + if (mHasGamepadListener) { + mHasGamepadListener = false; + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + service->RemoveChannelParent(this); + } + MaybeStopGamepadMonitoring(); +} + +void +GamepadEventChannelParent::DispatchUpdateEvent(const GamepadChangeEvent& aEvent) +{ + mBackgroundThread->Dispatch(new SendGamepadUpdateRunnable(this, aEvent), + NS_DISPATCH_NORMAL); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/ipc/GamepadEventChannelParent.h b/dom/gamepad/ipc/GamepadEventChannelParent.h new file mode 100644 index 000000000..2ba005c35 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventChannelParent.h @@ -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 "mozilla/dom/PGamepadEventChannelParent.h" + +#ifndef mozilla_dom_GamepadEventChannelParent_h_ +#define mozilla_dom_GamepadEventChannelParent_h_ + +namespace mozilla{ +namespace dom{ + +class GamepadEventChannelParent final : public PGamepadEventChannelParent +{ + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadEventChannelParent) + GamepadEventChannelParent(); + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + virtual bool RecvGamepadListenerAdded() override; + virtual bool RecvGamepadListenerRemoved() override; + void DispatchUpdateEvent(const GamepadChangeEvent& aEvent); + bool HasGamepadListener() const { return mHasGamepadListener; } + private: + ~GamepadEventChannelParent() {} + bool mHasGamepadListener; + nsCOMPtr<nsIThread> mBackgroundThread; +}; + +}// namespace dom +}// namespace mozilla + +#endif diff --git a/dom/gamepad/ipc/GamepadEventTypes.ipdlh b/dom/gamepad/ipc/GamepadEventTypes.ipdlh new file mode 100644 index 000000000..68ca36016 --- /dev/null +++ b/dom/gamepad/ipc/GamepadEventTypes.ipdlh @@ -0,0 +1,59 @@ +/* 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/. */ + +using mozilla::dom::GamepadServiceType from "mozilla/dom/GamepadMessageUtils.h"; +using mozilla::dom::GamepadPoseState from "mozilla/dom/GamepadMessageUtils.h"; + + +namespace mozilla { +namespace dom { + +struct GamepadAdded { + nsString id; + uint32_t index; + // Ideally, mapping should be a GamepadMappingType + // But, we have dependency problems in non MOZ_GAMEPAD + // platforms. Therefore, we make it as an uint32_t here. + uint32_t mapping; + GamepadServiceType service_type; + uint32_t num_buttons; + uint32_t num_axes; +}; + +struct GamepadRemoved { + uint32_t index; + GamepadServiceType service_type; +}; + +struct GamepadAxisInformation { + uint32_t index; + GamepadServiceType service_type; + uint32_t axis; + double value; +}; + +struct GamepadButtonInformation { + uint32_t index; + GamepadServiceType service_type; + uint32_t button; + bool pressed; + double value; +}; + +struct GamepadPoseInformation { + uint32_t index; + GamepadServiceType service_type; + GamepadPoseState pose_state; +}; + +union GamepadChangeEvent { + GamepadAdded; + GamepadRemoved; + GamepadAxisInformation; + GamepadButtonInformation; + GamepadPoseInformation; +}; + +} // 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 000000000..b9af6db9f --- /dev/null +++ b/dom/gamepad/ipc/GamepadMessageUtils.h @@ -0,0 +1,82 @@ + +#ifndef mozilla_dom_gamepad_GamepadMessageUtils_h +#define mozilla_dom_gamepad_GamepadMessageUtils_h + +#include "ipc/IPCMessageUtils.h" +#include "mozilla/dom/GamepadServiceType.h" +#include "mozilla/dom/GamepadPoseState.h" + +namespace IPC { + +template<> +struct ParamTraits<mozilla::dom::GamepadServiceType> : + public ContiguousEnumSerializer<mozilla::dom::GamepadServiceType, + mozilla::dom::GamepadServiceType(0), + mozilla::dom::GamepadServiceType( + mozilla::dom::GamepadServiceType::NumGamepadServiceType)> {}; + +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(Message* aMsg, const paramType& aParam) + { + WriteParam(aMsg, aParam.flags); + WriteParam(aMsg, aParam.orientation[0]); + WriteParam(aMsg, aParam.orientation[1]); + WriteParam(aMsg, aParam.orientation[2]); + WriteParam(aMsg, aParam.orientation[3]); + WriteParam(aMsg, aParam.position[0]); + WriteParam(aMsg, aParam.position[1]); + WriteParam(aMsg, aParam.position[2]); + WriteParam(aMsg, aParam.angularVelocity[0]); + WriteParam(aMsg, aParam.angularVelocity[1]); + WriteParam(aMsg, aParam.angularVelocity[2]); + WriteParam(aMsg, aParam.angularAcceleration[0]); + WriteParam(aMsg, aParam.angularAcceleration[1]); + WriteParam(aMsg, aParam.angularAcceleration[2]); + WriteParam(aMsg, aParam.linearVelocity[0]); + WriteParam(aMsg, aParam.linearVelocity[1]); + WriteParam(aMsg, aParam.linearVelocity[2]); + WriteParam(aMsg, aParam.linearAcceleration[0]); + WriteParam(aMsg, aParam.linearAcceleration[1]); + WriteParam(aMsg, aParam.linearAcceleration[2]); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult) + { + if (!ReadParam(aMsg, aIter, &(aResult->flags)) || + !ReadParam(aMsg, aIter, &(aResult->orientation[0])) || + !ReadParam(aMsg, aIter, &(aResult->orientation[1])) || + !ReadParam(aMsg, aIter, &(aResult->orientation[2])) || + !ReadParam(aMsg, aIter, &(aResult->orientation[3])) || + !ReadParam(aMsg, aIter, &(aResult->position[0])) || + !ReadParam(aMsg, aIter, &(aResult->position[1])) || + !ReadParam(aMsg, aIter, &(aResult->position[2])) || + !ReadParam(aMsg, aIter, &(aResult->angularVelocity[0])) || + !ReadParam(aMsg, aIter, &(aResult->angularVelocity[1])) || + !ReadParam(aMsg, aIter, &(aResult->angularVelocity[2])) || + !ReadParam(aMsg, aIter, &(aResult->angularAcceleration[0])) || + !ReadParam(aMsg, aIter, &(aResult->angularAcceleration[1])) || + !ReadParam(aMsg, aIter, &(aResult->angularAcceleration[2])) || + !ReadParam(aMsg, aIter, &(aResult->linearVelocity[0])) || + !ReadParam(aMsg, aIter, &(aResult->linearVelocity[1])) || + !ReadParam(aMsg, aIter, &(aResult->linearVelocity[2])) || + !ReadParam(aMsg, aIter, &(aResult->linearAcceleration[0])) || + !ReadParam(aMsg, aIter, &(aResult->linearAcceleration[1])) || + !ReadParam(aMsg, aIter, &(aResult->linearAcceleration[2]))) { + return false; + } + return true; + } +}; + +} // namespace IPC + +#endif // mozilla_dom_gamepad_GamepadMessageUtils_h
\ No newline at end of file diff --git a/dom/gamepad/ipc/GamepadServiceType.h b/dom/gamepad/ipc/GamepadServiceType.h new file mode 100644 index 000000000..acc0967d1 --- /dev/null +++ b/dom/gamepad/ipc/GamepadServiceType.h @@ -0,0 +1,20 @@ + +#ifndef mozilla_dom_GamepadServiceType_h_ +#define mozilla_dom_GamepadServiceType_h_ + +namespace mozilla{ +namespace dom{ + +// Standard channel is used for managing gamepads that +// are from GamepadPlatformService. VR channel +// is for gamepads that are from VRManagerChild. +enum class GamepadServiceType : uint16_t { + Standard, + VR, + NumGamepadServiceType +}; + +}// namespace dom +}// namespace mozilla + +#endif // mozilla_dom_GamepadServiceType_h_
\ No newline at end of file diff --git a/dom/gamepad/ipc/GamepadTestChannelChild.cpp b/dom/gamepad/ipc/GamepadTestChannelChild.cpp new file mode 100644 index 000000000..3fec9a7ea --- /dev/null +++ b/dom/gamepad/ipc/GamepadTestChannelChild.cpp @@ -0,0 +1,32 @@ +/* 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" + +namespace mozilla { +namespace dom { + +void +GamepadTestChannelChild::AddPromise(const uint32_t& aID, Promise* aPromise) +{ + MOZ_ASSERT(!mPromiseList.Get(aID, nullptr)); + mPromiseList.Put(aID, aPromise); +} + +bool +GamepadTestChannelChild::RecvReplyGamepadIndex(const uint32_t& aID, + const uint32_t& aIndex) +{ + RefPtr<Promise> p; + if (!mPromiseList.Get(aID, getter_AddRefs(p))) { + MOZ_CRASH("We should always have a promise."); + } + + p->MaybeResolve(aIndex); + mPromiseList.Remove(aID); + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/ipc/GamepadTestChannelChild.h b/dom/gamepad/ipc/GamepadTestChannelChild.h new file mode 100644 index 000000000..f2c771fe3 --- /dev/null +++ b/dom/gamepad/ipc/GamepadTestChannelChild.h @@ -0,0 +1,29 @@ +/* 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/PGamepadTestChannelChild.h" +#include "mozilla/dom/Promise.h" + +#ifndef mozilla_dom_GamepadTestChannelChild_h_ +#define mozilla_dom_GamepadTestChannelChild_h_ + +namespace mozilla { +namespace dom { + +class GamepadTestChannelChild final : public PGamepadTestChannelChild +{ + public: + GamepadTestChannelChild() {} + ~GamepadTestChannelChild() {} + void AddPromise(const uint32_t& aID, Promise* aPromise); + private: + virtual bool RecvReplyGamepadIndex(const uint32_t& aID, + const uint32_t& aIndex) override; + nsRefPtrHashtable<nsUint32HashKey, Promise> mPromiseList; +}; + +}// namespace dom +}// namespace mozilla + +#endif diff --git a/dom/gamepad/ipc/GamepadTestChannelParent.cpp b/dom/gamepad/ipc/GamepadTestChannelParent.cpp new file mode 100644 index 000000000..421447fe0 --- /dev/null +++ b/dom/gamepad/ipc/GamepadTestChannelParent.cpp @@ -0,0 +1,63 @@ +/* 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/Unused.h" + +namespace mozilla { +namespace dom { + +bool +GamepadTestChannelParent::RecvGamepadTestEvent(const uint32_t& aID, + const GamepadChangeEvent& aEvent) +{ + mozilla::ipc::AssertIsOnBackgroundThread(); + RefPtr<GamepadPlatformService> service = + GamepadPlatformService::GetParentService(); + MOZ_ASSERT(service); + if (aEvent.type() == GamepadChangeEvent::TGamepadAdded) { + const GamepadAdded& a = aEvent.get_GamepadAdded(); + nsCString gamepadID; + LossyCopyUTF16toASCII(a.id(), gamepadID); + uint32_t index = service->AddGamepad(gamepadID.get(), + static_cast<GamepadMappingType>(a.mapping()), + a.num_buttons(), + a.num_axes()); + if (!mShuttingdown) { + Unused << SendReplyGamepadIndex(aID, index); + } + return true; + } + if (aEvent.type() == GamepadChangeEvent::TGamepadRemoved) { + const GamepadRemoved& a = aEvent.get_GamepadRemoved(); + service->RemoveGamepad(a.index()); + return true; + } + if (aEvent.type() == GamepadChangeEvent::TGamepadButtonInformation) { + const GamepadButtonInformation& a = aEvent.get_GamepadButtonInformation(); + service->NewButtonEvent(a.index(), a.button(), a.pressed(), a.value()); + return true; + } + if (aEvent.type() == GamepadChangeEvent::TGamepadAxisInformation) { + const GamepadAxisInformation& a = aEvent.get_GamepadAxisInformation(); + service->NewAxisMoveEvent(a.index(), a.axis(), a.value()); + return true; + } + + NS_WARNING("Unknown event type."); + return false; +} + +bool +GamepadTestChannelParent::RecvShutdownChannel() +{ + mShuttingdown = true; + Unused << Send__delete__(this); + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/ipc/GamepadTestChannelParent.h b/dom/gamepad/ipc/GamepadTestChannelParent.h new file mode 100644 index 000000000..ce66667b8 --- /dev/null +++ b/dom/gamepad/ipc/GamepadTestChannelParent.h @@ -0,0 +1,33 @@ +/* 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" + +#ifndef mozilla_dom_GamepadTestChannelParent_h_ +#define mozilla_dom_GamepadTestChannelParent_h_ + +namespace mozilla { +namespace dom { + +class GamepadTestChannelParent final : public PGamepadTestChannelParent +{ + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadTestChannelParent) + GamepadTestChannelParent() + : mShuttingdown(false) {} + virtual void ActorDestroy(ActorDestroyReason aWhy) override {} + virtual bool + RecvGamepadTestEvent(const uint32_t& aID, + const GamepadChangeEvent& aGamepadEvent) override; + virtual bool + RecvShutdownChannel() override; + private: + ~GamepadTestChannelParent() {} + bool mShuttingdown; +}; + +}// namespace dom +}// namespace mozilla + +#endif diff --git a/dom/gamepad/ipc/PGamepadEventChannel.ipdl b/dom/gamepad/ipc/PGamepadEventChannel.ipdl new file mode 100644 index 000000000..78e389a91 --- /dev/null +++ b/dom/gamepad/ipc/PGamepadEventChannel.ipdl @@ -0,0 +1,21 @@ +/* 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 GamepadEventTypes; + +namespace mozilla { +namespace dom { + +async protocol PGamepadEventChannel { + manager PBackground; + parent: + async GamepadListenerAdded(); + async GamepadListenerRemoved(); + child: + async __delete__(); + async GamepadUpdate(GamepadChangeEvent aGamepadEvent); +}; + +} +} diff --git a/dom/gamepad/ipc/PGamepadTestChannel.ipdl b/dom/gamepad/ipc/PGamepadTestChannel.ipdl new file mode 100644 index 000000000..57b694acc --- /dev/null +++ b/dom/gamepad/ipc/PGamepadTestChannel.ipdl @@ -0,0 +1,21 @@ +/* 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 GamepadEventTypes; + +namespace mozilla { +namespace dom { + +async protocol PGamepadTestChannel { + manager PBackground; + parent: + async GamepadTestEvent(uint32_t aID, GamepadChangeEvent aGamepadEvent); + async ShutdownChannel(); + child: + async __delete__(); + async ReplyGamepadIndex(uint32_t aID, uint32_t aIndex); +}; + +} +} diff --git a/dom/gamepad/linux/LinuxGamepad.cpp b/dom/gamepad/linux/LinuxGamepad.cpp new file mode 100644 index 000000000..c45f4174a --- /dev/null +++ b/dom/gamepad/linux/LinuxGamepad.cpp @@ -0,0 +1,390 @@ +/* -*- 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: A Linux backend for the GamepadService. + * Derived from the kernel documentation at + * http://www.kernel.org/doc/Documentation/input/joystick-api.txt + */ +#include <algorithm> +#include <cstddef> + +#include <glib.h> +#include <linux/joystick.h> +#include <stdio.h> +#include <stdint.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include "nscore.h" +#include "mozilla/dom/GamepadPlatformService.h" +#include "udev.h" + +namespace { + +using namespace mozilla::dom; +using mozilla::udev_lib; +using mozilla::udev_device; +using mozilla::udev_list_entry; +using mozilla::udev_enumerate; +using mozilla::udev_monitor; + +static const float kMaxAxisValue = 32767.0; +static const char kJoystickPath[] = "/dev/input/js"; + +//TODO: should find a USB identifier for each device so we can +// provide something that persists across connect/disconnect cycles. +typedef struct { + int index; + guint source_id; + int numAxes; + int numButtons; + char idstring[128]; + char devpath[PATH_MAX]; +} Gamepad; + +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 is_gamepad(struct udev_device* dev); + void ReadUdevChange(); + + // handler for data from /dev/input/jsN + 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<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; + } + } + + Gamepad 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); + char name[128]; + if (ioctl(fd, JSIOCGNAME(sizeof(name)), &name) == -1) { + strcpy(name, "unknown"); + } + const char* vendor_id = + mUdev.udev_device_get_property_value(dev, "ID_VENDOR_ID"); + const char* model_id = + mUdev.udev_device_get_property_value(dev, "ID_MODEL_ID"); + if (!vendor_id || !model_id) { + struct udev_device* parent = + mUdev.udev_device_get_parent_with_subsystem_devtype(dev, + "input", + nullptr); + if (parent) { + vendor_id = mUdev.udev_device_get_sysattr_value(parent, "id/vendor"); + model_id = mUdev.udev_device_get_sysattr_value(parent, "id/product"); + } + } + snprintf(gamepad.idstring, sizeof(gamepad.idstring), + "%s-%s-%s", + vendor_id ? vendor_id : "unknown", + model_id ? model_id : "unknown", + name); + + char numAxes = 0, numButtons = 0; + ioctl(fd, JSIOCGAXES, &numAxes); + gamepad.numAxes = numAxes; + ioctl(fd, JSIOCGBUTTONS, &numButtons); + gamepad.numButtons = numButtons; + + gamepad.index = service->AddGamepad(gamepad.idstring, + mozilla::dom::GamepadMappingType::_empty, + gamepad.numButtons, + gamepad.numAxes); + + gamepad.source_id = + g_io_add_watch(channel, + GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), + OnGamepadData, + GINT_TO_POINTER(gamepad.index)); + g_io_channel_unref(channel); + + mGamepads.AppendElement(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) { + g_source_remove(mGamepads[i].source_id); + service->RemoveGamepad(mGamepads[i].index); + mGamepads.RemoveElementAt(i); + 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 (is_gamepad(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::is_gamepad(struct udev_device* dev) +{ + if (!mUdev.udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK")) + return false; + + const char* devpath = mUdev.udev_device_get_devnode(dev); + if (!devpath) { + return false; + } + if (strncmp(kJoystickPath, devpath, sizeof(kJoystickPath) - 1) != 0) { + return false; + } + + return true; +} + +void +LinuxGamepadService::ReadUdevChange() +{ + struct udev_device* dev = + mUdev.udev_monitor_receive_device(mMonitor); + const char* action = mUdev.udev_device_get_action(dev); + if (is_gamepad(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; + } + int index = GPOINTER_TO_INT(data); + //TODO: remove gamepad? + if (condition & G_IO_ERR || condition & G_IO_HUP) + return FALSE; + + while (true) { + struct js_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; + } + + //TODO: store device state? + if (event.type & JS_EVENT_INIT) { + continue; + } + + switch (event.type) { + case JS_EVENT_BUTTON: + service->NewButtonEvent(index, event.number, !!event.value); + break; + case JS_EVENT_AXIS: + service->NewAxisMoveEvent(index, event.number, + ((float)event.value) / kMaxAxisValue); + break; + } + } + + return TRUE; +} + +// static +gboolean +LinuxGamepadService::OnUdevMonitor(GIOChannel* source, + GIOCondition condition, + gpointer data) +{ + if (condition & G_IO_ERR || condition & G_IO_HUP) + return FALSE; + + gService->ReadUdevChange(); + return TRUE; +} + +} // namespace + +namespace mozilla { +namespace dom { + +void StartGamepadMonitoring() +{ + if (gService) { + return; + } + gService = new LinuxGamepadService(); + gService->Startup(); +} + +void StopGamepadMonitoring() +{ + if (!gService) { + return; + } + gService->Shutdown(); + delete gService; + gService = nullptr; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/gamepad/linux/udev.h b/dom/gamepad/linux/udev.h new file mode 100644 index 000000000..18da9f7dc --- /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: + bool LoadSymbols() { +#define DLSYM(s) \ + do { \ + s = (typeof(s))dlsym(lib, #s); \ + if (!s) return false; \ + } while (0) + + 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); +#undef DLSYM + return true; + } + + 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 000000000..a809d1eba --- /dev/null +++ b/dom/gamepad/moz.build @@ -0,0 +1,83 @@ +# -*- 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/. + +IPDL_SOURCES += [ + 'ipc/GamepadEventTypes.ipdlh', + 'ipc/PGamepadEventChannel.ipdl', + 'ipc/PGamepadTestChannel.ipdl' +] + +EXPORTS.mozilla.dom += [ + 'GamepadPoseState.h', + 'ipc/GamepadMessageUtils.h', + 'ipc/GamepadServiceType.h' +] + +if CONFIG['MOZ_GAMEPAD']: + EXPORTS.mozilla.dom += [ + 'Gamepad.h', + 'GamepadButton.h', + 'GamepadManager.h', + 'GamepadMonitoring.h', + 'GamepadPlatformService.h', + 'GamepadPose.h', + 'GamepadServiceTest.h', + 'ipc/GamepadEventChannelChild.h', + 'ipc/GamepadEventChannelParent.h', + 'ipc/GamepadTestChannelChild.h', + 'ipc/GamepadTestChannelParent.h' + ] + + UNIFIED_SOURCES = [ + 'Gamepad.cpp', + 'GamepadButton.cpp', + 'GamepadManager.cpp', + 'GamepadMonitoring.cpp', + 'GamepadPlatformService.cpp', + 'GamepadPose.cpp', + 'GamepadServiceTest.cpp', + 'ipc/GamepadEventChannelChild.cpp', + 'ipc/GamepadEventChannelParent.cpp', + 'ipc/GamepadTestChannelChild.cpp', + 'ipc/GamepadTestChannelParent.cpp' + ] + + if CONFIG['MOZ_GAMEPAD_BACKEND'] == 'stub': + UNIFIED_SOURCES += [ + 'fallback/FallbackGamepad.cpp' + ] + elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'cocoa': + UNIFIED_SOURCES += [ + 'cocoa/CocoaGamepad.cpp' + ] + elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'windows': + UNIFIED_SOURCES += [ + 'windows/WindowsGamepad.cpp' + ] + elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'linux': + UNIFIED_SOURCES += [ + 'linux/LinuxGamepad.cpp' + ] + elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'android': + UNIFIED_SOURCES += [ + 'android/AndroidGamepad.cpp' + ] + + LOCAL_INCLUDES += [ + 'ipc', + ] + + include('/ipc/chromium/chromium-config.mozbuild') + + FINAL_LIBRARY = 'xul' + LOCAL_INCLUDES += [ + '/dom/base', + ] + + CFLAGS += CONFIG['GLIB_CFLAGS'] + CFLAGS += CONFIG['MOZ_DBUS_GLIB_CFLAGS'] + CXXFLAGS += CONFIG['GLIB_CFLAGS'] + CXXFLAGS += CONFIG['MOZ_DBUS_GLIB_CFLAGS'] diff --git a/dom/gamepad/windows/WindowsGamepad.cpp b/dom/gamepad/windows/WindowsGamepad.cpp new file mode 100644 index 000000000..e1965c00c --- /dev/null +++ b/dom/gamepad/windows/WindowsGamepad.cpp @@ -0,0 +1,1096 @@ +/* -*- 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 "nsIComponentManager.h" +#include "nsITimer.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Services.h" + +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/dom/GamepadPlatformService.h" + +namespace { + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::ArrayLength; + +// USB HID usage tables, page 1 (Hat switch) +const unsigned kUsageDpad = 0x39; +// USB HID usage tables, page 1, 0x30 = X +const unsigned kFirstAxis = 0x30; + +// USB HID usage tables +const unsigned kDesktopUsagePage = 0x1; +const unsigned 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. +// 50ms is arbitrarily chosen. +const uint32_t kWindowsGamepadPollInterval = 50; + +const UINT kRawInputError = (UINT)-1; + +#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_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; + + // ID from the GamepadService, also used as the index into + // WindowsGamepadService::mGamepads. + int id; + + + // Information about the physical device. + unsigned numAxes; + unsigned numButtons; + bool hasDpad; + HIDP_VALUE_CAPS dpadCaps; + + nsTArray<bool> buttons; + struct axisValue { + HIDP_VALUE_CAPS caps; + double value; + }; + nsTArray<axisValue> axes; + + // Used during rescan to find devices that were disconnected. + bool present; + + Gamepad(uint32_t aNumAxes, + uint32_t aNumButtons, + bool aHasDpad, + GamepadType aType) : + numAxes(aNumAxes), + numButtons(aNumButtons), + hasDpad(aHasDpad), + type(aType), + present(true) + { + buttons.SetLength(numButtons); + axes.SetLength(numAxes); + } +private: + Gamepad() {} +}; + +// Drop this in favor of decltype when we require a new enough SDK. +typedef void (WINAPI *XInputEnable_func)(BOOL); + +// RAII class to wrap loading the XInput DLL +class XInputLoader { +public: + XInputLoader() : module(nullptr), + mXInputEnable(nullptr), + mXInputGetState(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")); + mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>( + GetProcAddress(module, "XInputGetState")); + if (mXInputEnable) { + mXInputEnable(TRUE); + } + break; + } + } + } + + ~XInputLoader() { + //mXInputEnable = nullptr; + mXInputGetState = nullptr; + + if (module) { + FreeLibrary(module); + } + } + + 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; +} + +/* + * Given a value from a d-pad (POV hat in USB HID terminology), + * represent it as 4 buttons, one for each cardinal direction. + */ +void +UnpackDpad(LONG dpad_value, const Gamepad* gamepad, nsTArray<bool>& buttons) +{ + const unsigned kUp = gamepad->numButtons - 4; + const unsigned kDown = gamepad->numButtons - 3; + const unsigned kLeft = gamepad->numButtons - 2; + const unsigned kRight = gamepad->numButtons - 1; + + // Different controllers have different ways of representing + // "nothing is pressed", but they're all outside the range of values. + if (dpad_value < gamepad->dpadCaps.LogicalMin + || dpad_value > gamepad->dpadCaps.LogicalMax) { + // Nothing is pressed. + return; + } + + // Normalize value to start at 0. + int value = dpad_value - gamepad->dpadCaps.LogicalMin; + + // Value will be in the range 0-7. The value represents the + // position of the d-pad around a circle, with 0 being straight up, + // 2 being right, 4 being straight down, and 6 being left. + if (value < 2 || value > 6) { + buttons[kUp] = true; + } + if (value > 2 && value < 6) { + buttons[kDown] = true; + } + if (value > 4) { + buttons[kLeft] = true; + } + if (value > 0 && value < 4) { + buttons[kRight] = true; + } +} + +/* + * 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() : mModule(LoadLibraryW(L"hid.dll")), + mHidD_GetProductString(nullptr), + mHidP_GetCaps(nullptr), + mHidP_GetButtonCaps(nullptr), + mHidP_GetValueCaps(nullptr), + mHidP_GetUsages(nullptr), + mHidP_GetUsageValue(nullptr), + mHidP_GetScaledUsageValue(nullptr) + { + 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); + } + } + + 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->InitWithFuncCallback(DirectInputMessageLoopOnceCallback, + nullptr, kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT); + } +} + +class WindowsGamepadService +{ + public: + WindowsGamepadService() + { + mDirectInputTimer = do_CreateInstance("@mozilla.org/timer;1"); + mXInputTimer = do_CreateInstance("@mozilla.org/timer;1"); + mDeviceChangeTimer = do_CreateInstance("@mozilla.org/timer;1"); + } + virtual ~WindowsGamepadService() + { + Cleanup(); + } + + void DevicesChanged(bool aIsStablizing); + + void StartMessageLoop() + { + MOZ_ASSERT(mDirectInputTimer); + mDirectInputTimer->InitWithFuncCallback(DirectInputMessageLoopOnceCallback, + nullptr, kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT); + } + + void Startup(); + void Shutdown(); + // Parse gamepad input from a WM_INPUT message. + bool HandleRawInput(HRAWINPUT handle); + + 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(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; + 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->InitWithFuncCallback(XInputMessageLoopOnceCallback, self, + kWindowsGamepadPollInterval, nsITimer::TYPE_ONE_SHOT); + } +} + +// static +void +WindowsGamepadService::DevicesChangeCallback(nsITimer *aTimer, void* aService) +{ + MOZ_ASSERT(aService); + WindowsGamepadService* self = static_cast<WindowsGamepadService*>(aService); + self->DevicesChanged(false); +} + +bool +WindowsGamepadService::HaveXInputGamepad(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 (int i = 0; i < XUSER_MAX_COUNT; i++) { + XINPUT_STATE state = {}; + if (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, + true, + kXInputGamepad); + gamepad.userIndex = i; + gamepad.state = state; + gamepad.id = service->AddGamepad("xinput", + GamepadMappingType::Standard, + kStandardGamepadButtons, + kStandardGamepadAxes); + mGamepads.AppendElement(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->InitWithFuncCallback(XInputMessageLoopOnceCallback, this, + kWindowsGamepadPollInterval, + nsITimer::TYPE_ONE_SHOT); + } 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].id); + mGamepads.RemoveElementAt(i); + } + } +} + +void +WindowsGamepadService::PollXInput() +{ + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + if (mGamepads[i].type != kXInputGamepad) { + continue; + } + + XINPUT_STATE state = {}; + DWORD res = mXInput.mXInputGetState(mGamepads[i].userIndex, &state); + if (res == ERROR_SUCCESS + && 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.id, kXIButtonMap[b].mapped, true); + } else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) && + gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) { + // Button released + service->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, false); + } + } + + // Then triggers + if (state.Gamepad.bLeftTrigger != gamepad.state.Gamepad.bLeftTrigger) { + bool pressed = + state.Gamepad.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD; + service->NewButtonEvent(gamepad.id, kButtonLeftTrigger, + pressed, state.Gamepad.bLeftTrigger / 255.0); + } + if (state.Gamepad.bRightTrigger != gamepad.state.Gamepad.bRightTrigger) { + bool pressed = + state.Gamepad.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD; + service->NewButtonEvent(gamepad.id, 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) { + service->NewAxisMoveEvent(gamepad.id, kLeftStickXAxis, + state.Gamepad.sThumbLX / 32767.0); + } + if (state.Gamepad.sThumbLY != gamepad.state.Gamepad.sThumbLY) { + service->NewAxisMoveEvent(gamepad.id, kLeftStickYAxis, + -1.0 * state.Gamepad.sThumbLY / 32767.0); + } + if (state.Gamepad.sThumbRX != gamepad.state.Gamepad.sThumbRX) { + service->NewAxisMoveEvent(gamepad.id, kRightStickXAxis, + state.Gamepad.sThumbRX / 32767.0); + } + if (state.Gamepad.sThumbRY != gamepad.state.Gamepad.sThumbRY) { + service->NewAxisMoveEvent(gamepad.id, kRightStickYAxis, + -1.0 * state.Gamepad.sThumbRY / 32767.0); + } + gamepad.state = state; +} + +// Used to sort a list of axes by HID usage. +class HidValueComparator { +public: + bool Equals(const HIDP_VALUE_CAPS& c1, const HIDP_VALUE_CAPS& c2) const + { + return c1.UsagePage == c2.UsagePage && c1.Range.UsageMin == c2.Range.UsageMin; + } + bool LessThan(const HIDP_VALUE_CAPS& c1, const HIDP_VALUE_CAPS& c2) const + { + if (c1.UsagePage == c2.UsagePage) { + return c1.Range.UsageMin < c2.Range.UsageMin; + } + return c1.UsagePage < c2.UsagePage; + } +}; + +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; + HANDLE hid_handle = CreateFile(devname.Elements(), GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); + if (hid_handle) { + if (mHID.mHidD_GetProductString(hid_handle, &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); + } + CloseHandle(hid_handle); + } + 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> valueCaps(count); + valueCaps.SetLength(count); + if (mHID.mHidP_GetValueCaps(HidP_Input, valueCaps.Elements(), &count, parsed) + != HIDP_STATUS_SUCCESS) { + return false; + } + nsTArray<HIDP_VALUE_CAPS> axes; + // Sort the axes by usagePage and usage to expose a consistent ordering. + bool hasDpad; + HIDP_VALUE_CAPS dpadCaps; + + HidValueComparator comparator; + for (unsigned i = 0; i < count; i++) { + if (valueCaps[i].UsagePage == kDesktopUsagePage + && valueCaps[i].Range.UsageMin == kUsageDpad + // Don't know how to handle d-pads that return weird values. + && valueCaps[i].LogicalMax - valueCaps[i].LogicalMin == 7) { + // d-pad gets special handling. + // Ostensibly HID devices can expose multiple d-pads, but this + // doesn't happen in practice. + hasDpad = true; + dpadCaps = valueCaps[i]; + // Expose d-pad as 4 additional buttons. + numButtons += 4; + } else { + axes.InsertElementSorted(valueCaps[i], comparator); + } + } + + uint32_t numAxes = axes.Length(); + + // Not already present, add it. + Gamepad gamepad(numAxes, + numButtons, + true, + kRawInputGamepad); + + gamepad.handle = handle; + + for (unsigned i = 0; i < gamepad.numAxes; i++) { + gamepad.axes[i].caps = axes[i]; + } + + gamepad.id = service->AddGamepad(gamepad_id, + GamepadMappingType::_empty, + gamepad.numButtons, + gamepad.numAxes); + mGamepads.AppendElement(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++) { + buttons[usages[i] - 1] = true; + } + + if (gamepad->hasDpad) { + // Get d-pad position as 4 buttons. + ULONG value; + if (mHID.mHidP_GetUsageValue(HidP_Input, gamepad->dpadCaps.UsagePage, 0, gamepad->dpadCaps.Range.UsageMin, &value, parsed, (PCHAR)raw->data.hid.bRawData, raw->data.hid.dwSizeHid) == HIDP_STATUS_SUCCESS) { + UnpackDpad(static_cast<LONG>(value), gamepad, buttons); + } + } + + for (unsigned i = 0; i < gamepad->numButtons; i++) { + if (gamepad->buttons[i] != buttons[i]) { + service->NewButtonEvent(gamepad->id, 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) { + service->NewAxisMoveEvent(gamepad->id, i, new_value); + gamepad->axes[i].value = new_value; + } + } + + return true; +} + +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->InitWithFuncCallback(DevicesChangeCallback, this, + kDevicesChangedStableDelay, + nsITimer::TYPE_ONE_SHOT); + } 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() {} + + 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() {} + + 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 { +namespace dom { + +using namespace mozilla::ipc; + +void +StartGamepadMonitoring() +{ + AssertIsOnBackgroundThread(); + + if (gMonitorThread || gService) { + return; + } + sIsShutdown = false; + NS_NewThread(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; +} + +} // namespace dom +} // namespace mozilla |