diff options
Diffstat (limited to '')
74 files changed, 8016 insertions, 0 deletions
diff --git a/dom/vr/VRDisplay.cpp b/dom/vr/VRDisplay.cpp new file mode 100644 index 0000000000..474513dc5d --- /dev/null +++ b/dom/vr/VRDisplay.cpp @@ -0,0 +1,780 @@ +/* -*- 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/dom/Element.h" +#include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/VRDisplay.h" +#include "mozilla/dom/VRDisplayBinding.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/Base64.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" +#include "Navigator.h" +#include "gfxUtils.h" +#include "gfxVR.h" +#include "VRDisplayClient.h" +#include "VRManagerChild.h" +#include "VRDisplayPresentation.h" +#include "nsIObserverService.h" +#include "nsIFrame.h" +#include "nsISupportsPrimitives.h" + +using namespace mozilla::gfx; + +namespace mozilla::dom { + +VRFieldOfView::VRFieldOfView(nsISupports* aParent, double aUpDegrees, + double aRightDegrees, double aDownDegrees, + double aLeftDegrees) + : mParent(aParent), + mUpDegrees(aUpDegrees), + mRightDegrees(aRightDegrees), + mDownDegrees(aDownDegrees), + mLeftDegrees(aLeftDegrees) {} + +VRFieldOfView::VRFieldOfView(nsISupports* aParent, + const gfx::VRFieldOfView& aSrc) + : mParent(aParent), + mUpDegrees(aSrc.upDegrees), + mRightDegrees(aSrc.rightDegrees), + mDownDegrees(aSrc.downDegrees), + mLeftDegrees(aSrc.leftDegrees) {} + +bool VRDisplayCapabilities::HasPosition() const { + return bool(mFlags & gfx::VRDisplayCapabilityFlags::Cap_Position) || + bool(mFlags & gfx::VRDisplayCapabilityFlags::Cap_PositionEmulated); +} + +bool VRDisplayCapabilities::HasOrientation() const { + return bool(mFlags & gfx::VRDisplayCapabilityFlags::Cap_Orientation); +} + +bool VRDisplayCapabilities::HasExternalDisplay() const { + return bool(mFlags & gfx::VRDisplayCapabilityFlags::Cap_External); +} + +bool VRDisplayCapabilities::CanPresent() const { + return bool(mFlags & gfx::VRDisplayCapabilityFlags::Cap_Present); +} + +uint32_t VRDisplayCapabilities::MaxLayers() const { + return CanPresent() ? 1 : 0; +} + +void VRDisplay::UpdateDisplayClient( + already_AddRefed<gfx::VRDisplayClient> aClient) { + mClient = std::move(aClient); +} + +/*static*/ +bool VRDisplay::RefreshVRDisplays(uint64_t aWindowId) { + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + return vm && vm->RefreshVRDisplaysWithCallback(aWindowId); +} + +/*static*/ +void VRDisplay::UpdateVRDisplays(nsTArray<RefPtr<VRDisplay>>& aDisplays, + nsPIDOMWindowInner* aWindow) { + nsTArray<RefPtr<VRDisplay>> displays; + + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + nsTArray<RefPtr<gfx::VRDisplayClient>> updatedDisplays; + if (vm) { + vm->GetVRDisplays(updatedDisplays); + for (size_t i = 0; i < updatedDisplays.Length(); i++) { + RefPtr<gfx::VRDisplayClient> display = updatedDisplays[i]; + bool isNewDisplay = true; + for (size_t j = 0; j < aDisplays.Length(); j++) { + if (aDisplays[j]->GetClient()->GetDisplayInfo().GetDisplayID() == + display->GetDisplayInfo().GetDisplayID()) { + displays.AppendElement(aDisplays[j]); + isNewDisplay = false; + } else { + RefPtr<gfx::VRDisplayClient> ref = display; + aDisplays[j]->UpdateDisplayClient(do_AddRef(display)); + displays.AppendElement(aDisplays[j]); + isNewDisplay = false; + } + } + + if (isNewDisplay) { + displays.AppendElement(new VRDisplay(aWindow, display)); + } + } + } + + aDisplays = std::move(displays); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(VRFieldOfView, mParent) + +JSObject* VRFieldOfView::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRFieldOfView_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS(VREyeParameters, + (mParent, mFOV), + (mOffset)) + +VREyeParameters::VREyeParameters(nsISupports* aParent, + const gfx::Point3D& aEyeTranslation, + const gfx::VRFieldOfView& aFOV, + const gfx::IntSize& aRenderSize) + : mParent(aParent), + mEyeTranslation(aEyeTranslation), + mRenderSize(aRenderSize) { + mFOV = new VRFieldOfView(aParent, aFOV); + mozilla::HoldJSObjects(this); +} + +VREyeParameters::~VREyeParameters() { mozilla::DropJSObjects(this); } + +VRFieldOfView* VREyeParameters::FieldOfView() { return mFOV; } + +void VREyeParameters::GetOffset(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + if (!mOffset) { + // Lazily create the Float32Array + mOffset = + dom::Float32Array::Create(aCx, this, 3, mEyeTranslation.components); + if (!mOffset) { + aRv.NoteJSContextException(aCx); + return; + } + } + aRetval.set(mOffset); +} + +JSObject* VREyeParameters::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VREyeParameters_Binding::Wrap(aCx, this, aGivenProto); +} + +VRStageParameters::VRStageParameters( + nsISupports* aParent, const gfx::Matrix4x4& aSittingToStandingTransform, + const gfx::Size& aSize) + : mParent(aParent), + mSittingToStandingTransform(aSittingToStandingTransform), + mSittingToStandingTransformArray(nullptr), + mSize(aSize) { + mozilla::HoldJSObjects(this); +} + +VRStageParameters::~VRStageParameters() { mozilla::DropJSObjects(this); } + +JSObject* VRStageParameters::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRStageParameters_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(VRStageParameters) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(VRStageParameters) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + tmp->mSittingToStandingTransformArray = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(VRStageParameters) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(VRStageParameters) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK( + mSittingToStandingTransformArray) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +void VRStageParameters::GetSittingToStandingTransform( + JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, ErrorResult& aRv) { + if (!mSittingToStandingTransformArray) { + // Lazily create the Float32Array + mSittingToStandingTransformArray = dom::Float32Array::Create( + aCx, this, 16, mSittingToStandingTransform.components); + if (!mSittingToStandingTransformArray) { + aRv.NoteJSContextException(aCx); + return; + } + } + aRetval.set(mSittingToStandingTransformArray); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(VRDisplayCapabilities, mParent) + +JSObject* VRDisplayCapabilities::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRDisplayCapabilities_Binding::Wrap(aCx, this, aGivenProto); +} + +VRPose::VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState) + : Pose(aParent), mVRState(aState) { + mozilla::HoldJSObjects(this); +} + +VRPose::VRPose(nsISupports* aParent) : Pose(aParent) { + mVRState.inputFrameID = 0; + mVRState.timestamp = 0.0; + mVRState.flags = gfx::VRDisplayCapabilityFlags::Cap_None; + mozilla::HoldJSObjects(this); +} + +VRPose::~VRPose() { mozilla::DropJSObjects(this); } + +void VRPose::GetPosition(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position) || + bool(mVRState.flags & + gfx::VRDisplayCapabilityFlags::Cap_PositionEmulated); + SetFloat32Array(aCx, this, aRetval, mPosition, + valid ? mVRState.pose.position : nullptr, 3, aRv); +} + +void VRPose::GetLinearVelocity(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position) || + bool(mVRState.flags & + gfx::VRDisplayCapabilityFlags::Cap_PositionEmulated); + SetFloat32Array(aCx, this, aRetval, mLinearVelocity, + valid ? mVRState.pose.linearVelocity : nullptr, 3, aRv); +} + +void VRPose::GetLinearAcceleration(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = bool( + mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_LinearAcceleration); + SetFloat32Array(aCx, this, aRetval, mLinearAcceleration, + valid ? mVRState.pose.linearAcceleration : nullptr, 3, aRv); +} + +void VRPose::GetOrientation(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation); + SetFloat32Array(aCx, this, aRetval, mOrientation, + valid ? mVRState.pose.orientation : nullptr, 4, aRv); +} + +void VRPose::GetAngularVelocity(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = + bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation); + SetFloat32Array(aCx, this, aRetval, mAngularVelocity, + valid ? mVRState.pose.angularVelocity : nullptr, 3, aRv); +} + +void VRPose::GetAngularAcceleration(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + const bool valid = bool( + mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_AngularAcceleration); + SetFloat32Array(aCx, this, aRetval, mAngularAcceleration, + valid ? mVRState.pose.angularAcceleration : nullptr, 3, aRv); +} + +void VRPose::Update(const gfx::VRHMDSensorState& aState) { mVRState = aState; } + +JSObject* VRPose::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRPose_Binding::Wrap(aCx, this, aGivenProto); +} + +/* virtual */ +JSObject* VRDisplay::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRDisplay_Binding::Wrap(aCx, this, aGivenProto); +} + +VRDisplay::VRDisplay(nsPIDOMWindowInner* aWindow, gfx::VRDisplayClient* aClient) + : DOMEventTargetHelper(aWindow), + mClient(aClient), + mDepthNear(0.01f) // Default value from WebVR Spec + , + mDepthFar(10000.0f) // Default value from WebVR Spec + , + mVRNavigationEventDepth(0), + mShutdown(false) { + const gfx::VRDisplayInfo& info = aClient->GetDisplayInfo(); + mCapabilities = new VRDisplayCapabilities(aWindow, info.GetCapabilities()); + if (info.GetCapabilities() & + gfx::VRDisplayCapabilityFlags::Cap_StageParameters) { + mStageParameters = new VRStageParameters( + aWindow, info.GetSittingToStandingTransform(), info.GetStageSize()); + } + mozilla::HoldJSObjects(this); + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (MOZ_LIKELY(obs)) { + obs->AddObserver(this, "inner-window-destroyed", false); + } +} + +VRDisplay::~VRDisplay() { + MOZ_ASSERT(mShutdown); + mozilla::DropJSObjects(this); +} + +void VRDisplay::LastRelease() { + // We don't want to wait for the CC to free up the presentation + // for use in other documents, so we do this in LastRelease(). + Shutdown(); +} + +already_AddRefed<VREyeParameters> VRDisplay::GetEyeParameters(VREye aEye) { + gfx::VRDisplayState::Eye eye = aEye == VREye::Left + ? gfx::VRDisplayState::Eye_Left + : gfx::VRDisplayState::Eye_Right; + RefPtr<VREyeParameters> params = new VREyeParameters( + GetParentObject(), mClient->GetDisplayInfo().GetEyeTranslation(eye), + mClient->GetDisplayInfo().GetEyeFOV(eye), + mClient->GetDisplayInfo().SuggestedEyeResolution()); + return params.forget(); +} + +VRDisplayCapabilities* VRDisplay::Capabilities() { return mCapabilities; } + +VRStageParameters* VRDisplay::GetStageParameters() { return mStageParameters; } + +uint32_t VRDisplay::DisplayId() const { + const gfx::VRDisplayInfo& info = mClient->GetDisplayInfo(); + return info.GetDisplayID(); +} + +void VRDisplay::GetDisplayName(nsAString& aDisplayName) const { + const gfx::VRDisplayInfo& info = mClient->GetDisplayInfo(); + CopyUTF8toUTF16(MakeStringSpan(info.GetDisplayName()), aDisplayName); +} + +void VRDisplay::UpdateFrameInfo() { + /** + * The WebVR 1.1 spec Requires that VRDisplay.getPose and + * VRDisplay.getFrameData must return the same values until the next + * VRDisplay.submitFrame. + * + * mFrameInfo is marked dirty at the end of the frame or start of a new + * composition and lazily created here in order to receive mid-frame + * pose-prediction updates while still ensuring conformance to the WebVR spec + * requirements. + * + * If we are not presenting WebVR content, the frame will never end and we + * should return the latest frame data always. + */ + mFrameInfo.Clear(); + + if ((mFrameInfo.IsDirty() && IsPresenting()) || + mClient->GetDisplayInfo().GetPresentingGroups() == 0) { + const gfx::VRHMDSensorState& state = mClient->GetSensorState(); + const gfx::VRDisplayInfo& info = mClient->GetDisplayInfo(); + mFrameInfo.Update(info, state, mDepthNear, mDepthFar); + } +} + +bool VRDisplay::GetFrameData(VRFrameData& aFrameData) { + UpdateFrameInfo(); + if (!(mFrameInfo.mVRState.flags & + gfx::VRDisplayCapabilityFlags::Cap_Orientation)) { + // We must have at minimum Cap_Orientation for a valid pose. + return false; + } + aFrameData.Update(mFrameInfo); + return true; +} + +already_AddRefed<VRPose> VRDisplay::GetPose() { + UpdateFrameInfo(); + RefPtr<VRPose> obj = new VRPose(GetParentObject(), mFrameInfo.mVRState); + + return obj.forget(); +} + +void VRDisplay::ResetPose() { + // ResetPose is deprecated and unimplemented + // We must keep this stub function around as its referenced by + // VRDisplay.webidl. Not asserting here, as that could break existing web + // content. +} + +void VRDisplay::StartVRNavigation() { mClient->StartVRNavigation(); } + +void VRDisplay::StartHandlingVRNavigationEvent() { + mHandlingVRNavigationEventStart = TimeStamp::Now(); + ++mVRNavigationEventDepth; + TimeDuration timeout = + TimeDuration::FromMilliseconds(StaticPrefs::dom_vr_navigation_timeout()); + // A 0 or negative TimeDuration indicates that content may take + // as long as it wishes to respond to the event, as long as + // it happens before the event exits. + if (timeout.ToMilliseconds() > 0) { + mClient->StopVRNavigation(timeout); + } +} + +void VRDisplay::StopHandlingVRNavigationEvent() { + MOZ_ASSERT(mVRNavigationEventDepth > 0); + --mVRNavigationEventDepth; + if (mVRNavigationEventDepth == 0) { + mClient->StopVRNavigation(TimeDuration::FromMilliseconds(0)); + } +} + +bool VRDisplay::IsHandlingVRNavigationEvent() { + if (mVRNavigationEventDepth == 0) { + return false; + } + if (mHandlingVRNavigationEventStart.IsNull()) { + return false; + } + TimeDuration timeout = + TimeDuration::FromMilliseconds(StaticPrefs::dom_vr_navigation_timeout()); + return timeout.ToMilliseconds() <= 0 || + (TimeStamp::Now() - mHandlingVRNavigationEventStart) <= timeout; +} + +void VRDisplay::OnPresentationGenerationChanged() { ExitPresentInternal(); } + +already_AddRefed<Promise> VRDisplay::RequestPresent( + const nsTArray<VRLayer>& aLayers, CallerType aCallerType, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetParentObject(); + if (!global) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + NS_ENSURE_TRUE(!aRv.Failed(), nullptr); + + bool isChromePresentation = aCallerType == CallerType::System; + uint32_t presentationGroup = + isChromePresentation ? gfx::kVRGroupChrome : gfx::kVRGroupContent; + + mClient->SetXRAPIMode(gfx::VRAPIMode::WebVR); + if (!UserActivation::IsHandlingUserInput() && !isChromePresentation && + !IsHandlingVRNavigationEvent() && StaticPrefs::dom_vr_require_gesture() && + !IsPresenting()) { + // The WebVR API states that if called outside of a user gesture, the + // promise must be rejected. We allow VR presentations to start within + // trusted events such as vrdisplayactivate, which triggers in response to + // HMD proximity sensors and when navigating within a VR presentation. + // This user gesture requirement is not enforced for chrome/system code. + promise->MaybeRejectWithUndefined(); + } else if (!IsPresenting() && IsAnyPresenting(presentationGroup)) { + // Only one presentation allowed per VRDisplay on a + // first-come-first-serve basis. + // If this Javascript context is presenting, then we can replace our + // presentation with a new one containing new layers but we should never + // replace the presentation of another context. + // Simultaneous presentations in other groups are allowed in separate + // Javascript contexts to enable browser UI from chrome/system contexts. + // Eventually, this restriction will be loosened to enable multitasking + // use cases. + promise->MaybeRejectWithUndefined(); + } else { + if (mPresentation) { + mPresentation->UpdateLayers(aLayers); + } else { + mPresentation = mClient->BeginPresentation(aLayers, presentationGroup); + } + mFrameInfo.Clear(); + promise->MaybeResolve(JS::UndefinedHandleValue); + } + return promise.forget(); +} + +NS_IMETHODIMP +VRDisplay::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + if (strcmp(aTopic, "inner-window-destroyed") == 0) { + nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + NS_ENSURE_SUCCESS(rv, rv); + + if (!GetOwner() || GetOwner()->WindowID() == innerID) { + Shutdown(); + } + + return NS_OK; + } + + // This should not happen. + return NS_ERROR_FAILURE; +} + +already_AddRefed<Promise> VRDisplay::ExitPresent(ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetParentObject(); + if (!global) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + NS_ENSURE_TRUE(!aRv.Failed(), nullptr); + + if (!IsPresenting()) { + // We can not exit a presentation outside of the context that + // started the presentation. + promise->MaybeRejectWithUndefined(); + } else { + promise->MaybeResolve(JS::UndefinedHandleValue); + ExitPresentInternal(); + } + + return promise.forget(); +} + +void VRDisplay::ExitPresentInternal() { mPresentation = nullptr; } + +void VRDisplay::Shutdown() { + mShutdown = true; + ExitPresentInternal(); + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (MOZ_LIKELY(obs)) { + obs->RemoveObserver(this, "inner-window-destroyed"); + } +} + +void VRDisplay::GetLayers(nsTArray<VRLayer>& result) { + if (mPresentation) { + mPresentation->GetDOMLayers(result); + } else { + result = nsTArray<VRLayer>(); + } +} + +void VRDisplay::SubmitFrame() { + AUTO_PROFILER_TRACING_MARKER("VR", "SubmitFrameAtVRDisplay", OTHER); + + if (mClient && !mClient->IsPresentationGenerationCurrent()) { + mPresentation = nullptr; + mClient->MakePresentationGenerationCurrent(); + } + + if (mPresentation) { + mPresentation->SubmitFrame(); + } + mFrameInfo.Clear(); +} + +int32_t VRDisplay::RequestAnimationFrame(FrameRequestCallback& aCallback, + ErrorResult& aError) { + if (mShutdown) { + return 0; + } + + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + + int32_t handle; + aError = vm->ScheduleFrameRequestCallback(aCallback, &handle); + return handle; +} + +void VRDisplay::CancelAnimationFrame(int32_t aHandle, ErrorResult& aError) { + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->CancelFrameRequestCallback(aHandle); +} + +bool VRDisplay::IsPresenting() const { + // IsPresenting returns true only if this Javascript context is presenting + // and will return false if another context is presenting. + return mPresentation != nullptr; +} + +bool VRDisplay::IsAnyPresenting(uint32_t aGroupMask) const { + // IsAnyPresenting returns true if either this VRDisplay object or any other + // from anther Javascript context is presenting with a group matching + // aGroupMask. + if (mPresentation && (mPresentation->GetGroup() & aGroupMask)) { + return true; + } + if (mClient->GetDisplayInfo().GetPresentingGroups() & aGroupMask) { + return true; + } + return false; +} + +bool VRDisplay::IsConnected() const { return mClient->GetIsConnected(); } + +uint32_t VRDisplay::PresentingGroups() const { + return mClient->GetDisplayInfo().GetPresentingGroups(); +} + +uint32_t VRDisplay::GroupMask() const { + return mClient->GetDisplayInfo().GetGroupMask(); +} + +void VRDisplay::SetGroupMask(const uint32_t& aGroupMask) { + mClient->SetGroupMask(aGroupMask); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(VRDisplay, DOMEventTargetHelper, + mCapabilities, mStageParameters) + +NS_IMPL_ADDREF_INHERITED(VRDisplay, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(VRDisplay, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(VRDisplay) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, EventTarget) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_CLASS(VRFrameData) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(VRFrameData) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent, mPose) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + tmp->mLeftProjectionMatrix = nullptr; + tmp->mLeftViewMatrix = nullptr; + tmp->mRightProjectionMatrix = nullptr; + tmp->mRightViewMatrix = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(VRFrameData) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent, mPose) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(VRFrameData) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLeftProjectionMatrix) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLeftViewMatrix) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mRightProjectionMatrix) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mRightViewMatrix) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +VRFrameData::VRFrameData(nsISupports* aParent) + : mParent(aParent), + mLeftProjectionMatrix(nullptr), + mLeftViewMatrix(nullptr), + mRightProjectionMatrix(nullptr), + mRightViewMatrix(nullptr) { + mozilla::HoldJSObjects(this); + mPose = new VRPose(aParent); +} + +VRFrameData::~VRFrameData() { mozilla::DropJSObjects(this); } + +/* static */ +already_AddRefed<VRFrameData> VRFrameData::Constructor( + const GlobalObject& aGlobal) { + RefPtr<VRFrameData> obj = new VRFrameData(aGlobal.GetAsSupports()); + return obj.forget(); +} + +JSObject* VRFrameData::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRFrameData_Binding::Wrap(aCx, this, aGivenProto); +} + +VRPose* VRFrameData::Pose() { return mPose; } + +double VRFrameData::Timestamp() const { + // Converting from seconds to milliseconds + return mFrameInfo.mVRState.timestamp * 1000.0f; +} + +void VRFrameData::GetLeftProjectionMatrix(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + Pose::SetFloat32Array(aCx, this, aRetval, mLeftProjectionMatrix, + mFrameInfo.mLeftProjection.components, 16, aRv); +} + +void VRFrameData::GetLeftViewMatrix(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + Pose::SetFloat32Array(aCx, this, aRetval, mLeftViewMatrix, + mFrameInfo.mLeftView.components, 16, aRv); +} + +void VRFrameData::GetRightProjectionMatrix(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + Pose::SetFloat32Array(aCx, this, aRetval, mRightProjectionMatrix, + mFrameInfo.mRightProjection.components, 16, aRv); +} + +void VRFrameData::GetRightViewMatrix(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + Pose::SetFloat32Array(aCx, this, aRetval, mRightViewMatrix, + mFrameInfo.mRightView.components, 16, aRv); +} + +void VRFrameData::Update(const VRFrameInfo& aFrameInfo) { + mFrameInfo = aFrameInfo; + mPose->Update(mFrameInfo.mVRState); +} + +void VRFrameInfo::Update(const gfx::VRDisplayInfo& aInfo, + const gfx::VRHMDSensorState& aState, float aDepthNear, + float aDepthFar) { + mVRState = aState; + if (mTimeStampOffset == 0.0f) { + /** + * A mTimeStampOffset value of 0.0f indicates that this is the first + * iteration and an offset has not yet been set. + * + * Generate a value for mTimeStampOffset such that if aState.timestamp is + * monotonically increasing, aState.timestamp + mTimeStampOffset will never + * be a negative number and will start at a pseudo-random offset + * between 1000.0f and 11000.0f seconds. + * + * We use a pseudo random offset rather than 0.0f just to discourage users + * from making the assumption that the timestamp returned in the WebVR API + * has a base of 0, which is not necessarily true in all UA's. + */ + mTimeStampOffset = + float(rand()) / float(RAND_MAX) * 10000.0f + 1000.0f - aState.timestamp; + } + mVRState.timestamp = aState.timestamp + mTimeStampOffset; + + // Avoid division by zero within ConstructProjectionMatrix + const float kEpsilon = 0.00001f; + if (fabs(aDepthFar - aDepthNear) < kEpsilon) { + aDepthFar = aDepthNear + kEpsilon; + } + + const gfx::VRFieldOfView leftFOV = + aInfo.mDisplayState.eyeFOV[gfx::VRDisplayState::Eye_Left]; + mLeftProjection = + leftFOV.ConstructProjectionMatrix(aDepthNear, aDepthFar, true); + const gfx::VRFieldOfView rightFOV = + aInfo.mDisplayState.eyeFOV[gfx::VRDisplayState::Eye_Right]; + mRightProjection = + rightFOV.ConstructProjectionMatrix(aDepthNear, aDepthFar, true); + memcpy(mLeftView.components, aState.leftViewMatrix, + sizeof(aState.leftViewMatrix)); + memcpy(mRightView.components, aState.rightViewMatrix, + sizeof(aState.rightViewMatrix)); +} + +VRFrameInfo::VRFrameInfo() : mTimeStampOffset(0.0f) { + mVRState.inputFrameID = 0; + mVRState.timestamp = 0.0; + mVRState.flags = gfx::VRDisplayCapabilityFlags::Cap_None; +} + +bool VRFrameInfo::IsDirty() { return mVRState.timestamp == 0; } + +void VRFrameInfo::Clear() { mVRState.Clear(); } + +} // namespace mozilla::dom diff --git a/dom/vr/VRDisplay.h b/dom/vr/VRDisplay.h new file mode 100644 index 0000000000..866edc9846 --- /dev/null +++ b/dom/vr/VRDisplay.h @@ -0,0 +1,367 @@ +/* -*- 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_VRDisplay_h_ +#define mozilla_dom_VRDisplay_h_ + +#include <stdint.h> + +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/VRDisplayBinding.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/DOMPoint.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/Pose.h" +#include "mozilla/TimeStamp.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +#include "gfxVR.h" + +namespace mozilla { +class ErrorResult; + +namespace gfx { +class VRDisplayClient; +class VRDisplayPresentation; +struct VRFieldOfView; +enum class VRDisplayCapabilityFlags : uint16_t; +struct VRHMDSensorState; +} // namespace gfx +namespace dom { +class Navigator; + +class VRFieldOfView final : public nsWrapperCache { + public: + VRFieldOfView(nsISupports* aParent, double aUpDegrees, double aRightDegrees, + double aDownDegrees, double aLeftDegrees); + VRFieldOfView(nsISupports* aParent, const gfx::VRFieldOfView& aSrc); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VRFieldOfView) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(VRFieldOfView) + + double UpDegrees() const { return mUpDegrees; } + double RightDegrees() const { return mRightDegrees; } + double DownDegrees() const { return mDownDegrees; } + double LeftDegrees() const { return mLeftDegrees; } + + nsISupports* GetParentObject() const { return mParent; } + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + virtual ~VRFieldOfView() = default; + + nsCOMPtr<nsISupports> mParent; + + double mUpDegrees; + double mRightDegrees; + double mDownDegrees; + double mLeftDegrees; +}; + +class VRDisplayCapabilities final : public nsWrapperCache { + public: + VRDisplayCapabilities(nsISupports* aParent, + const gfx::VRDisplayCapabilityFlags& aFlags) + : mParent(aParent), mFlags(aFlags) {} + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VRDisplayCapabilities) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(VRDisplayCapabilities) + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + bool HasPosition() const; + bool HasOrientation() const; + bool HasExternalDisplay() const; + bool CanPresent() const; + uint32_t MaxLayers() const; + + protected: + ~VRDisplayCapabilities() = default; + nsCOMPtr<nsISupports> mParent; + gfx::VRDisplayCapabilityFlags mFlags; +}; + +class VRPose final : public Pose { + public: + VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState); + explicit VRPose(nsISupports* aParent); + + virtual void GetPosition(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetLinearVelocity(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetLinearAcceleration(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetOrientation(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetAngularVelocity(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + virtual void GetAngularAcceleration(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) override; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void Update(const gfx::VRHMDSensorState& aState); + + protected: + ~VRPose(); + + gfx::VRHMDSensorState mVRState; +}; + +struct VRFrameInfo { + VRFrameInfo(); + + void Update(const gfx::VRDisplayInfo& aInfo, + const gfx::VRHMDSensorState& aState, float aDepthNear, + float aDepthFar); + + void Clear(); + bool IsDirty(); + + gfx::VRHMDSensorState mVRState; + gfx::Matrix4x4 mLeftProjection; + gfx::Matrix4x4 mLeftView; + gfx::Matrix4x4 mRightProjection; + gfx::Matrix4x4 mRightView; + + /** + * In order to avoid leaking information related to the duration of + * the user's VR session, we re-base timestamps. + * mTimeStampOffset is added to the actual timestamp returned by the + * underlying VR platform API when returned through WebVR API's. + */ + double mTimeStampOffset; +}; + +class VRFrameData final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VRFrameData) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(VRFrameData) + + explicit VRFrameData(nsISupports* aParent); + static already_AddRefed<VRFrameData> Constructor(const GlobalObject& aGlobal); + + void Update(const VRFrameInfo& aFrameInfo); + + // WebIDL Members + double Timestamp() const; + void GetLeftProjectionMatrix(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + void GetLeftViewMatrix(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + void GetRightProjectionMatrix(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + void GetRightViewMatrix(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + + VRPose* Pose(); + + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + ~VRFrameData(); + nsCOMPtr<nsISupports> mParent; + + VRFrameInfo mFrameInfo; + RefPtr<VRPose> mPose; + JS::Heap<JSObject*> mLeftProjectionMatrix; + JS::Heap<JSObject*> mLeftViewMatrix; + JS::Heap<JSObject*> mRightProjectionMatrix; + JS::Heap<JSObject*> mRightViewMatrix; + + void LazyCreateMatrix(JS::Heap<JSObject*>& aArray, gfx::Matrix4x4& aMat, + JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); +}; + +class VRStageParameters final : public nsWrapperCache { + public: + VRStageParameters(nsISupports* aParent, + const gfx::Matrix4x4& aSittingToStandingTransform, + const gfx::Size& aSize); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VRStageParameters) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(VRStageParameters) + + void GetSittingToStandingTransform(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + float SizeX() const { return mSize.width; } + float SizeZ() const { return mSize.height; } + + nsISupports* GetParentObject() const { return mParent; } + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + ~VRStageParameters(); + + nsCOMPtr<nsISupports> mParent; + + gfx::Matrix4x4 mSittingToStandingTransform; + JS::Heap<JSObject*> mSittingToStandingTransformArray; + gfx::Size mSize; +}; + +class VREyeParameters final : public nsWrapperCache { + public: + VREyeParameters(nsISupports* aParent, const gfx::Point3D& aEyeTranslation, + const gfx::VRFieldOfView& aFOV, + const gfx::IntSize& aRenderSize); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VREyeParameters) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(VREyeParameters) + + void GetOffset(JSContext* aCx, JS::MutableHandle<JSObject*> aRetVal, + ErrorResult& aRv); + + VRFieldOfView* FieldOfView(); + + uint32_t RenderWidth() const { return mRenderSize.width; } + uint32_t RenderHeight() const { return mRenderSize.height; } + + nsISupports* GetParentObject() const { return mParent; } + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + ~VREyeParameters(); + + nsCOMPtr<nsISupports> mParent; + + gfx::Point3D mEyeTranslation; + gfx::IntSize mRenderSize; + JS::Heap<JSObject*> mOffset; + RefPtr<VRFieldOfView> mFOV; +}; + +class VRDisplay final : public DOMEventTargetHelper, public nsIObserver { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIOBSERVER + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(VRDisplay, DOMEventTargetHelper) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint32_t PresentingGroups() const; + uint32_t GroupMask() const; + void SetGroupMask(const uint32_t& aGroupMask); + bool IsAnyPresenting(uint32_t aGroupMask) const; + bool IsPresenting() const; + bool IsConnected() const; + + VRDisplayCapabilities* Capabilities(); + VRStageParameters* GetStageParameters(); + + uint32_t DisplayId() const; + void GetDisplayName(nsAString& aDisplayName) const; + // Replacing the old VRDisplayClient with the newest one to avoid + // JS needs to reload to recover VRDisplay when VRService is shutdown at the + // backend. + void UpdateDisplayClient(already_AddRefed<gfx::VRDisplayClient> aClient); + + static bool RefreshVRDisplays(uint64_t aWindowId); + static void UpdateVRDisplays(nsTArray<RefPtr<VRDisplay> >& aDisplays, + nsPIDOMWindowInner* aWindow); + + gfx::VRDisplayClient* GetClient() { return mClient; } + + virtual already_AddRefed<VREyeParameters> GetEyeParameters(VREye aEye); + + bool GetFrameData(VRFrameData& aFrameData); + already_AddRefed<VRPose> GetPose(); + void ResetPose(); + + double DepthNear() { return mDepthNear; } + + double DepthFar() { return mDepthFar; } + + void SetDepthNear(double aDepthNear) { + // XXX When we start sending depth buffers to VRLayer's we will want + // to communicate this with the VRDisplayHost + mDepthNear = aDepthNear; + } + + void SetDepthFar(double aDepthFar) { + // XXX When we start sending depth buffers to VRLayer's we will want + // to communicate this with the VRDisplayHost + mDepthFar = aDepthFar; + } + + already_AddRefed<Promise> RequestPresent(const nsTArray<VRLayer>& aLayers, + CallerType aCallerType, + ErrorResult& aRv); + already_AddRefed<Promise> ExitPresent(ErrorResult& aRv); + void GetLayers(nsTArray<VRLayer>& result); + void SubmitFrame(); + + int32_t RequestAnimationFrame(mozilla::dom::FrameRequestCallback& aCallback, + mozilla::ErrorResult& aError); + void CancelAnimationFrame(int32_t aHandle, mozilla::ErrorResult& aError); + void StartVRNavigation(); + void StartHandlingVRNavigationEvent(); + void StopHandlingVRNavigationEvent(); + bool IsHandlingVRNavigationEvent(); + void OnPresentationGenerationChanged(); + + protected: + VRDisplay(nsPIDOMWindowInner* aWindow, gfx::VRDisplayClient* aClient); + virtual ~VRDisplay(); + virtual void LastRelease() override; + + void ExitPresentInternal(); + void Shutdown(); + void UpdateFrameInfo(); + + RefPtr<gfx::VRDisplayClient> mClient; + + RefPtr<VRDisplayCapabilities> mCapabilities; + RefPtr<VRStageParameters> mStageParameters; + + double mDepthNear; + double mDepthFar; + + RefPtr<gfx::VRDisplayPresentation> mPresentation; + + /** + * The WebVR 1.1 spec Requires that VRDisplay.getPose and + * VRDisplay.getFrameData must return the same values until the next + * VRDisplay.submitFrame. mFrameInfo is updated only on the first call to + * either function within one frame. Subsequent calls before the next + * SubmitFrame or ExitPresent call will use these cached values. + */ + VRFrameInfo mFrameInfo; + + // Time at which we began expecting VR navigation. + TimeStamp mHandlingVRNavigationEventStart; + int32_t mVRNavigationEventDepth; + bool mShutdown; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/vr/VRDisplayEvent.cpp b/dom/vr/VRDisplayEvent.cpp new file mode 100644 index 0000000000..4ef355dcb9 --- /dev/null +++ b/dom/vr/VRDisplayEvent.cpp @@ -0,0 +1,74 @@ +/* -*- 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 "VRDisplayEvent.h" +#include "js/RootingAPI.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/PrimitiveConversions.h" + +using namespace mozilla::gfx; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(VRDisplayEvent) + +NS_IMPL_ADDREF_INHERITED(VRDisplayEvent, Event) +NS_IMPL_RELEASE_INHERITED(VRDisplayEvent, Event) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(VRDisplayEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(VRDisplayEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(VRDisplayEvent, Event) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(VRDisplayEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +VRDisplayEvent::VRDisplayEvent(mozilla::dom::EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) {} + +VRDisplay* VRDisplayEvent::Display() { return mDisplay; } + +JSObject* VRDisplayEvent::WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return VRDisplayEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<VRDisplayEvent> VRDisplayEvent::Constructor( + mozilla::dom::EventTarget* aOwner, const nsAString& aType, + const VRDisplayEventInit& aEventInitDict) { + RefPtr<VRDisplayEvent> e = new VRDisplayEvent(aOwner); + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + if (aEventInitDict.mReason.WasPassed()) { + e->mReason = Some(aEventInitDict.mReason.Value()); + } + e->mDisplay = aEventInitDict.mDisplay; + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + return e.forget(); +} + +already_AddRefed<VRDisplayEvent> VRDisplayEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const VRDisplayEventInit& aEventInitDict) { + nsCOMPtr<mozilla::dom::EventTarget> owner = + do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(owner, aType, aEventInitDict); +} + +Nullable<VRDisplayEventReason> VRDisplayEvent::GetReason() const { + if (mReason.isSome()) { + return mReason.value(); + } + + return nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/vr/VRDisplayEvent.h b/dom/vr/VRDisplayEvent.h new file mode 100644 index 0000000000..3439833b66 --- /dev/null +++ b/dom/vr/VRDisplayEvent.h @@ -0,0 +1,65 @@ +/* -*- 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_VRDisplayEvent_h_ +#define mozilla_dom_VRDisplayEvent_h_ + +#include "js/RootingAPI.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/VRDisplay.h" +#include "mozilla/dom/VRDisplayEventBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsStringFwd.h" + +struct JSContext; + +namespace mozilla { +namespace gfx { +class VRDisplay; +} // namespace gfx + +namespace dom { + +class VRDisplayEvent final : public Event { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(VRDisplayEvent, Event) + + VRDisplay* Display(); + Nullable<VRDisplayEventReason> GetReason() const; + + protected: + virtual ~VRDisplayEvent() = default; + explicit VRDisplayEvent(mozilla::dom::EventTarget* aOwner); + VRDisplayEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalClipboardEvent* aEvent); + + Maybe<VRDisplayEventReason> mReason; + RefPtr<VRDisplay> mDisplay; + + public: + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<VRDisplayEvent> Constructor( + mozilla::dom::EventTarget* aOwner, const nsAString& aType, + const VRDisplayEventInit& aEventInitDict); + + static already_AddRefed<VRDisplayEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const VRDisplayEventInit& aEventInitDict); +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/vr/VREventObserver.cpp b/dom/vr/VREventObserver.cpp new file mode 100644 index 0000000000..4455352f90 --- /dev/null +++ b/dom/vr/VREventObserver.cpp @@ -0,0 +1,181 @@ +/* -*- 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 "VREventObserver.h" + +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" + +#include "mozilla/Telemetry.h" + +namespace mozilla::dom { + +using namespace gfx; + +/** + * This class is used by nsGlobalWindow to implement window.onvrdisplayactivate, + * window.onvrdisplaydeactivate, window.onvrdisplayconnected, + * window.onvrdisplaydisconnected, and window.onvrdisplaypresentchange. + */ +VREventObserver::VREventObserver(nsGlobalWindowInner* aGlobalWindow) + : mWindow(aGlobalWindow), + mIs2DView(true), + mHasReset(false), + mStopActivity(false) { + MOZ_ASSERT(aGlobalWindow); + + UpdateSpentTimeIn2DTelemetry(false); + VRManagerChild* vmc = VRManagerChild::Get(); + if (vmc) { + vmc->AddListener(this); + } +} + +VREventObserver::~VREventObserver() { DisconnectFromOwner(); } + +void VREventObserver::DisconnectFromOwner() { + // In the event that nsGlobalWindow is deallocated, VREventObserver may + // still be AddRef'ed elsewhere. Ensure that we don't UAF by + // dereferencing mWindow. + UpdateSpentTimeIn2DTelemetry(true); + mWindow = nullptr; + + // Unregister from VRManagerChild + if (VRManagerChild::IsCreated()) { + VRManagerChild* vmc = VRManagerChild::Get(); + vmc->RemoveListener(this); + } + mStopActivity = true; +} + +void VREventObserver::UpdateSpentTimeIn2DTelemetry(bool aUpdate) { + // mHasReset for avoiding setting the telemetry continuously + // for the telemetry is already been set when it is at the background. + // then, it would be set again when the process is exit and calling + // VREventObserver::DisconnectFromOwner(). + if (mWindow && mIs2DView && aUpdate && mHasReset) { + // The WebVR content is closed, and we will collect the telemetry info + // for the users who view it in 2D view only. + Telemetry::Accumulate(Telemetry::WEBVR_USERS_VIEW_IN, 0); + Telemetry::AccumulateTimeDelta(Telemetry::WEBVR_TIME_SPENT_VIEWING_IN_2D, + mSpendTimeIn2DView); + mHasReset = false; + } else if (!aUpdate) { + mSpendTimeIn2DView = TimeStamp::Now(); + mHasReset = true; + } +} + +void VREventObserver::StartActivity() { + mStopActivity = false; + VRManagerChild* vmc = VRManagerChild::Get(); + vmc->StartActivity(); +} + +void VREventObserver::StopActivity() { + mStopActivity = true; + VRManagerChild* vmc = VRManagerChild::Get(); + vmc->StopActivity(); +} + +bool VREventObserver::GetStopActivityStatus() const { return mStopActivity; } + +void VREventObserver::NotifyAfterLoad() { + if (VRManagerChild::IsCreated()) { + VRManagerChild* vmc = VRManagerChild::Get(); + vmc->FireDOMVRDisplayConnectEventsForLoad(this); + } +} + +void VREventObserver::NotifyVRDisplayMounted(uint32_t aDisplayID) { + if (mWindow && mWindow->IsCurrentInnerWindow() && IsWebVR(aDisplayID)) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->DispatchVRDisplayActivate(aDisplayID, + VRDisplayEventReason::Mounted); + } +} + +void VREventObserver::NotifyVRDisplayNavigation(uint32_t aDisplayID) { + if (mWindow && mWindow->IsCurrentInnerWindow() && IsWebVR(aDisplayID)) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->DispatchVRDisplayActivate(aDisplayID, + VRDisplayEventReason::Navigation); + } +} + +void VREventObserver::NotifyVRDisplayRequested(uint32_t aDisplayID) { + if (mWindow && mWindow->IsCurrentInnerWindow() && IsWebVR(aDisplayID)) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->DispatchVRDisplayActivate(aDisplayID, + VRDisplayEventReason::Requested); + } +} + +void VREventObserver::NotifyVRDisplayUnmounted(uint32_t aDisplayID) { + if (mWindow && mWindow->IsCurrentInnerWindow() && IsWebVR(aDisplayID)) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->DispatchVRDisplayDeactivate(aDisplayID, + VRDisplayEventReason::Unmounted); + } +} + +void VREventObserver::NotifyVRDisplayConnect(uint32_t aDisplayID) { + /** + * We do not call nsGlobalWindow::NotifyActiveVRDisplaysChanged here, as we + * can assume that a newly enumerated display is not presenting WebVR + * content. + */ + if (mWindow && mWindow->IsCurrentInnerWindow() && IsWebVR(aDisplayID)) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->DispatchVRDisplayConnect(aDisplayID); + } +} + +void VREventObserver::NotifyVRDisplayDisconnect(uint32_t aDisplayID) { + if (mWindow && mWindow->IsCurrentInnerWindow() && IsWebVR(aDisplayID)) { + mWindow->NotifyActiveVRDisplaysChanged(); + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->DispatchVRDisplayDisconnect(aDisplayID); + } +} + +void VREventObserver::NotifyVRDisplayPresentChange(uint32_t aDisplayID) { + // When switching to HMD present mode, it is no longer + // to be a 2D view. + mIs2DView = false; + + if (mWindow && mWindow->IsCurrentInnerWindow() && IsWebVR(aDisplayID)) { + mWindow->NotifyActiveVRDisplaysChanged(); + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->DispatchVRDisplayPresentChange(aDisplayID); + } +} + +void VREventObserver::NotifyPresentationGenerationChanged(uint32_t aDisplayID) { + if (mWindow && mWindow->IsCurrentInnerWindow() && IsWebVR(aDisplayID)) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->NotifyPresentationGenerationChanged(aDisplayID); + } +} + +void VREventObserver::NotifyEnumerationCompleted() {} + +void VREventObserver::NotifyDetectRuntimesCompleted() { + if (mWindow && mWindow->IsCurrentInnerWindow()) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mWindow->NotifyDetectXRRuntimesCompleted(); + } +} + +bool VREventObserver::IsWebVR(uint32_t aDisplayID) const { + VRManagerChild* vmc = VRManagerChild::Get(); + if (vmc) { + return vmc->GetVRAPIMode(aDisplayID) == gfx::VRAPIMode::WebVR; + } + return true; +} + +} // namespace mozilla::dom diff --git a/dom/vr/VREventObserver.h b/dom/vr/VREventObserver.h new file mode 100644 index 0000000000..2f61644c79 --- /dev/null +++ b/dom/vr/VREventObserver.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_VREventObserver_h +#define mozilla_dom_VREventObserver_h + +#include "mozilla/dom/VRDisplayEventBinding.h" +#include "nsISupportsImpl.h" // for NS_INLINE_DECL_REFCOUNTING +#include "VRManagerChild.h" + +class nsGlobalWindowInner; + +namespace mozilla::dom { + +class VREventObserver final : public gfx::VRManagerEventObserver { + public: + NS_INLINE_DECL_REFCOUNTING(VREventObserver, override) + explicit VREventObserver(nsGlobalWindowInner* aGlobalWindow); + + void NotifyAfterLoad(); + void NotifyVRDisplayMounted(uint32_t aDisplayID) override; + void NotifyVRDisplayUnmounted(uint32_t aDisplayID) override; + void NotifyVRDisplayNavigation(uint32_t aDisplayID); + void NotifyVRDisplayRequested(uint32_t aDisplayID); + void NotifyVRDisplayConnect(uint32_t aDisplayID) override; + void NotifyVRDisplayDisconnect(uint32_t aDisplayID) override; + void NotifyVRDisplayPresentChange(uint32_t aDisplayID) override; + void NotifyPresentationGenerationChanged(uint32_t aDisplayID) override; + void NotifyEnumerationCompleted() override; + void NotifyDetectRuntimesCompleted() override; + + void DisconnectFromOwner(); + void UpdateSpentTimeIn2DTelemetry(bool aUpdate); + void StartActivity(); + void StopActivity(); + bool GetStopActivityStatus() const override; + + private: + ~VREventObserver(); + + bool IsWebVR(uint32_t aDisplayID) const; + + RefPtr<nsGlobalWindowInner> mWindow; + // For WebVR telemetry for tracking users who view content + // in the 2D view. + TimeStamp mSpendTimeIn2DView; + bool mIs2DView; + bool mHasReset; + bool mStopActivity; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_VREventObserver_h diff --git a/dom/vr/VRServiceTest.cpp b/dom/vr/VRServiceTest.cpp new file mode 100644 index 0000000000..06720328c2 --- /dev/null +++ b/dom/vr/VRServiceTest.cpp @@ -0,0 +1,740 @@ +/* -*- 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/VRServiceTest.h" +#include "mozilla/dom/VRServiceTestBinding.h" +#include "mozilla/dom/GamepadPoseState.h" +#include "mozilla/dom/Promise.h" +#include "VRManagerChild.h" +#include "VRPuppetCommandBuffer.h" +#include <type_traits> + +namespace mozilla { +using namespace gfx; +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(VRMockDisplay, DOMEventTargetHelper, + mVRServiceTest) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(VRMockDisplay, + DOMEventTargetHelper) + +namespace { +template <class T> +bool ReadFloat32Array(T& aDestination, const Float32Array& aSource, + ErrorResult& aRv) { + constexpr size_t length = std::extent<T>::value; + aSource.ComputeState(); + if (aSource.Length() != length) { + aRv.Throw(NS_ERROR_INVALID_ARG); + // We don't want to MOZ_ASSERT here, as that would cause the + // browser to crash, making it difficult to debug the problem + // in JS code calling this API. + return false; + } + for (size_t i = 0; i < length; i++) { + aDestination[i] = aSource.Data()[i]; + } + return true; +} +}; // anonymous namespace + +VRMockDisplay::VRMockDisplay(VRServiceTest* aVRServiceTest) + : DOMEventTargetHelper(aVRServiceTest->GetOwner()), + mVRServiceTest(aVRServiceTest) {} + +JSObject* VRMockDisplay::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRMockDisplay_Binding::Wrap(aCx, this, aGivenProto); +} + +VRHMDSensorState& VRMockDisplay::SensorState() const { + return mVRServiceTest->SystemState().sensorState; +} + +VRDisplayState& VRMockDisplay::DisplayState() const { + return mVRServiceTest->SystemState().displayState; +} + +void VRMockDisplay::Clear() { + VRDisplayState& displayState = DisplayState(); + displayState.Clear(); + VRHMDSensorState& sensorState = SensorState(); + sensorState.Clear(); +} + +void VRMockDisplay::Create() { + Clear(); + VRDisplayState& state = DisplayState(); + + strncpy(state.displayName, "Puppet HMD", kVRDisplayNameMaxLen); + state.eightCC = GFX_VR_EIGHTCC('P', 'u', 'p', 'p', 'e', 't', ' ', ' '); + state.isConnected = true; + state.isMounted = false; + state.capabilityFlags = VRDisplayCapabilityFlags::Cap_None | + VRDisplayCapabilityFlags::Cap_Orientation | + VRDisplayCapabilityFlags::Cap_Position | + VRDisplayCapabilityFlags::Cap_External | + VRDisplayCapabilityFlags::Cap_Present | + VRDisplayCapabilityFlags::Cap_StageParameters | + VRDisplayCapabilityFlags::Cap_MountDetection | + VRDisplayCapabilityFlags::Cap_ImmersiveVR; + state.blendMode = VRDisplayBlendMode::Opaque; + + // 1836 x 2040 resolution is arbitrary and can be overridden. + // This default resolution was chosen to be within range of a + // typical VR eye buffer size. This value is derived by + // scaling a 1080x1200 per-eye panel resolution by the + // commonly used pre-lens-distortion pass scaling factor of 1.7x. + // 1.7x is commonly used in HMD's employing fresnel lenses to ensure + // a sufficient fragment shading rate in the peripheral area of the + // post-warp eye buffers. + state.eyeResolution.width = 1836; // 1080 * 1.7 + state.eyeResolution.height = 2040; // 1200 * 1.7 + + for (uint32_t eye = 0; eye < VRDisplayState::NumEyes; ++eye) { + state.eyeTranslation[eye].x = 0.0f; + state.eyeTranslation[eye].y = 0.0f; + state.eyeTranslation[eye].z = 0.0f; + state.eyeFOV[eye] = gfx::VRFieldOfView(45.0, 45.0, 45.0, 45.0); + } + + // default: 1m x 1m space, 0.75m high in seated position + state.stageSize.width = 1.0f; + state.stageSize.height = 1.0f; + + state.sittingToStandingTransform[0] = 1.0f; + state.sittingToStandingTransform[1] = 0.0f; + state.sittingToStandingTransform[2] = 0.0f; + state.sittingToStandingTransform[3] = 0.0f; + + state.sittingToStandingTransform[4] = 0.0f; + state.sittingToStandingTransform[5] = 1.0f; + state.sittingToStandingTransform[6] = 0.0f; + state.sittingToStandingTransform[7] = 0.0f; + + state.sittingToStandingTransform[8] = 0.0f; + state.sittingToStandingTransform[9] = 0.0f; + state.sittingToStandingTransform[10] = 1.0f; + state.sittingToStandingTransform[11] = 0.0f; + + state.sittingToStandingTransform[12] = 0.0f; + state.sittingToStandingTransform[13] = 0.75f; + state.sittingToStandingTransform[14] = 0.0f; + state.sittingToStandingTransform[15] = 1.0f; + + VRHMDSensorState& sensorState = SensorState(); + gfx::Quaternion rot; + sensorState.flags |= VRDisplayCapabilityFlags::Cap_Orientation; + sensorState.pose.orientation[0] = rot.x; + sensorState.pose.orientation[1] = rot.y; + sensorState.pose.orientation[2] = rot.z; + sensorState.pose.orientation[3] = rot.w; + sensorState.pose.angularVelocity[0] = 0.0f; + sensorState.pose.angularVelocity[1] = 0.0f; + sensorState.pose.angularVelocity[2] = 0.0f; + + sensorState.flags |= VRDisplayCapabilityFlags::Cap_Position; + sensorState.pose.position[0] = 0.0f; + sensorState.pose.position[1] = 0.0f; + sensorState.pose.position[2] = 0.0f; + sensorState.pose.linearVelocity[0] = 0.0f; + sensorState.pose.linearVelocity[1] = 0.0f; + sensorState.pose.linearVelocity[2] = 0.0f; +} + +void VRMockDisplay::SetConnected(bool aConnected) { + DisplayState().isConnected = aConnected; +} +bool VRMockDisplay::Connected() const { return DisplayState().isConnected; } + +void VRMockDisplay::SetMounted(bool aMounted) { + DisplayState().isMounted = aMounted; +} + +bool VRMockDisplay::Mounted() const { return DisplayState().isMounted; } + +void VRMockDisplay::SetCapFlag(VRDisplayCapabilityFlags aFlag, bool aEnabled) { + if (aEnabled) { + DisplayState().capabilityFlags |= aFlag; + } else { + DisplayState().capabilityFlags &= ~aFlag; + } +} +bool VRMockDisplay::GetCapFlag(VRDisplayCapabilityFlags aFlag) const { + return ((DisplayState().capabilityFlags & aFlag) != + VRDisplayCapabilityFlags::Cap_None); +} + +void VRMockDisplay::SetCapPosition(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_Position, aEnabled); +} + +void VRMockDisplay::SetCapOrientation(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_Orientation, aEnabled); +} + +void VRMockDisplay::SetCapPresent(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_Present, aEnabled); +} + +void VRMockDisplay::SetCapExternal(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_External, aEnabled); +} + +void VRMockDisplay::SetCapAngularAcceleration(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_AngularAcceleration, aEnabled); +} + +void VRMockDisplay::SetCapLinearAcceleration(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_LinearAcceleration, aEnabled); +} + +void VRMockDisplay::SetCapStageParameters(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_StageParameters, aEnabled); +} + +void VRMockDisplay::SetCapMountDetection(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_MountDetection, aEnabled); +} + +void VRMockDisplay::SetCapPositionEmulated(bool aEnabled) { + SetCapFlag(VRDisplayCapabilityFlags::Cap_PositionEmulated, aEnabled); +} + +void VRMockDisplay::SetEyeFOV(VREye aEye, double aUpDegree, double aRightDegree, + double aDownDegree, double aLeftDegree) { + gfx::VRDisplayState::Eye eye = aEye == VREye::Left + ? gfx::VRDisplayState::Eye_Left + : gfx::VRDisplayState::Eye_Right; + VRDisplayState& state = DisplayState(); + state.eyeFOV[eye] = + gfx::VRFieldOfView(aUpDegree, aRightDegree, aDownDegree, aLeftDegree); +} + +void VRMockDisplay::SetEyeOffset(VREye aEye, double aOffsetX, double aOffsetY, + double aOffsetZ) { + gfx::VRDisplayState::Eye eye = aEye == VREye::Left + ? gfx::VRDisplayState::Eye_Left + : gfx::VRDisplayState::Eye_Right; + VRDisplayState& state = DisplayState(); + state.eyeTranslation[eye].x = (float)aOffsetX; + state.eyeTranslation[eye].y = (float)aOffsetY; + state.eyeTranslation[eye].z = (float)aOffsetZ; +} + +bool VRMockDisplay::CapPosition() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_Position); +} + +bool VRMockDisplay::CapOrientation() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_Orientation); +} + +bool VRMockDisplay::CapPresent() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_Present); +} + +bool VRMockDisplay::CapExternal() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_External); +} + +bool VRMockDisplay::CapAngularAcceleration() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_AngularAcceleration); +} + +bool VRMockDisplay::CapLinearAcceleration() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_LinearAcceleration); +} + +bool VRMockDisplay::CapStageParameters() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_StageParameters); +} + +bool VRMockDisplay::CapMountDetection() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_MountDetection); +} + +bool VRMockDisplay::CapPositionEmulated() const { + return GetCapFlag(VRDisplayCapabilityFlags::Cap_PositionEmulated); +} + +void VRMockDisplay::SetEyeResolution(uint32_t aRenderWidth, + uint32_t aRenderHeight) { + DisplayState().eyeResolution.width = aRenderWidth; + DisplayState().eyeResolution.height = aRenderHeight; +} + +void VRMockDisplay::SetStageSize(double aWidth, double aHeight) { + VRDisplayState& displayState = DisplayState(); + displayState.stageSize.width = (float)aWidth; + displayState.stageSize.height = (float)aHeight; +} + +void VRMockDisplay::SetSittingToStandingTransform( + const Float32Array& aTransform, ErrorResult& aRv) { + Unused << ReadFloat32Array(DisplayState().sittingToStandingTransform, + aTransform, aRv); +} + +void VRMockDisplay::SetPose(const Nullable<Float32Array>& aPosition, + const Nullable<Float32Array>& aLinearVelocity, + const Nullable<Float32Array>& aLinearAcceleration, + const Nullable<Float32Array>& aOrientation, + const Nullable<Float32Array>& aAngularVelocity, + const Nullable<Float32Array>& aAngularAcceleration, + ErrorResult& aRv) { + VRHMDSensorState& sensorState = mVRServiceTest->SystemState().sensorState; + sensorState.Clear(); + sensorState.flags = VRDisplayCapabilityFlags::Cap_None; + // sensorState.timestamp will be set automatically during + // puppet script execution + + if (!aOrientation.IsNull()) { + if (!ReadFloat32Array(sensorState.pose.orientation, aOrientation.Value(), + aRv)) { + return; + } + sensorState.flags |= VRDisplayCapabilityFlags::Cap_Orientation; + } + if (!aAngularVelocity.IsNull()) { + if (!ReadFloat32Array(sensorState.pose.angularVelocity, + aAngularVelocity.Value(), aRv)) { + return; + } + sensorState.flags |= VRDisplayCapabilityFlags::Cap_AngularAcceleration; + } + if (!aAngularAcceleration.IsNull()) { + if (!ReadFloat32Array(sensorState.pose.angularAcceleration, + aAngularAcceleration.Value(), aRv)) { + return; + } + sensorState.flags |= VRDisplayCapabilityFlags::Cap_AngularAcceleration; + } + if (!aPosition.IsNull()) { + if (!ReadFloat32Array(sensorState.pose.position, aPosition.Value(), aRv)) { + return; + } + sensorState.flags |= VRDisplayCapabilityFlags::Cap_Position; + } + if (!aLinearVelocity.IsNull()) { + if (!ReadFloat32Array(sensorState.pose.linearVelocity, + aLinearVelocity.Value(), aRv)) { + return; + } + sensorState.flags |= VRDisplayCapabilityFlags::Cap_LinearAcceleration; + } + if (!aLinearAcceleration.IsNull()) { + if (!ReadFloat32Array(sensorState.pose.linearAcceleration, + aLinearAcceleration.Value(), aRv)) { + return; + } + sensorState.flags |= VRDisplayCapabilityFlags::Cap_LinearAcceleration; + } +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(VRMockController, DOMEventTargetHelper, + mVRServiceTest) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(VRMockController, + DOMEventTargetHelper) + +VRMockController::VRMockController(VRServiceTest* aVRServiceTest, + uint32_t aControllerIdx) + : DOMEventTargetHelper(aVRServiceTest->GetOwner()), + mVRServiceTest(aVRServiceTest), + mControllerIdx(aControllerIdx) { + MOZ_ASSERT(aControllerIdx < kVRControllerMaxCount); +} + +JSObject* VRMockController::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRMockController_Binding::Wrap(aCx, this, aGivenProto); +} + +VRControllerState& VRMockController::ControllerState() const { + return mVRServiceTest->SystemState().controllerState[mControllerIdx]; +} + +void VRMockController::Create() { + // Initialize with a 6dof, left-handed gamepad with one haptic actuator + // Tests are expected to modify the controller before it is sent to the + // puppet. + Clear(); + VRControllerState& state = ControllerState(); + strncpy(state.controllerName, "Puppet Gamepad", kVRControllerNameMaxLen); + state.hand = GamepadHand::Left; + state.flags = GamepadCapabilityFlags::Cap_Position | + GamepadCapabilityFlags::Cap_Orientation; + state.numButtons = 1; + state.numHaptics = 1; + state.triggerValue[0] = 0.0f; +} + +void VRMockController::Clear() { + mVRServiceTest->ClearController(mControllerIdx); +} + +void VRMockController::SetCapFlag(GamepadCapabilityFlags aFlag, bool aEnabled) { + if (aEnabled) { + ControllerState().flags |= aFlag; + } else { + ControllerState().flags &= ~aFlag; + } +} +bool VRMockController::GetCapFlag(GamepadCapabilityFlags aFlag) const { + return (ControllerState().flags & aFlag) != GamepadCapabilityFlags::Cap_None; +} + +void VRMockController::SetHand(GamepadHand aHand) { + ControllerState().hand = aHand; +} + +GamepadHand VRMockController::Hand() const { return ControllerState().hand; } + +void VRMockController::SetCapPosition(bool aEnabled) { + SetCapFlag(GamepadCapabilityFlags::Cap_Position, aEnabled); +} + +bool VRMockController::CapPosition() const { + return GetCapFlag(GamepadCapabilityFlags::Cap_Position); +} + +void VRMockController::SetCapOrientation(bool aEnabled) { + SetCapFlag(GamepadCapabilityFlags::Cap_Orientation, aEnabled); +} + +bool VRMockController::CapOrientation() const { + return GetCapFlag(GamepadCapabilityFlags::Cap_Orientation); +} + +void VRMockController::SetCapAngularAcceleration(bool aEnabled) { + SetCapFlag(GamepadCapabilityFlags::Cap_AngularAcceleration, aEnabled); +} + +bool VRMockController::CapAngularAcceleration() const { + return GetCapFlag(GamepadCapabilityFlags::Cap_AngularAcceleration); +} + +void VRMockController::SetCapLinearAcceleration(bool aEnabled) { + SetCapFlag(GamepadCapabilityFlags::Cap_LinearAcceleration, aEnabled); +} + +bool VRMockController::CapLinearAcceleration() const { + return GetCapFlag(GamepadCapabilityFlags::Cap_LinearAcceleration); +} + +void VRMockController::SetAxisCount(uint32_t aCount) { + MOZ_ASSERT(aCount <= kVRControllerMaxAxis); + ControllerState().numAxes = aCount; +} + +uint32_t VRMockController::AxisCount() const { + return ControllerState().numAxes; +} + +void VRMockController::SetButtonCount(uint32_t aCount) { + MOZ_ASSERT(aCount <= kVRControllerMaxButtons); + ControllerState().numButtons = aCount; +} + +uint32_t VRMockController::ButtonCount() const { + return ControllerState().numButtons; +} + +void VRMockController::SetHapticCount(uint32_t aCount) { + ControllerState().numHaptics = aCount; +} + +uint32_t VRMockController::HapticCount() const { + return ControllerState().numHaptics; +} + +void VRMockController::SetButtonPressed(uint32_t aButtonIdx, bool aPressed) { + MOZ_ASSERT(aButtonIdx < kVRControllerMaxButtons); + if (aPressed) { + ControllerState().buttonPressed |= (1 << aButtonIdx); + } else { + ControllerState().buttonPressed &= ~(1 << aButtonIdx); + } +} + +void VRMockController::SetButtonTouched(uint32_t aButtonIdx, bool aTouched) { + MOZ_ASSERT(aButtonIdx < kVRControllerMaxButtons); + if (aTouched) { + ControllerState().buttonTouched |= (1 << aButtonIdx); + } else { + ControllerState().buttonTouched &= ~(1 << aButtonIdx); + } +} + +void VRMockController::SetButtonTrigger(uint32_t aButtonIdx, double aTrigger) { + MOZ_ASSERT(aButtonIdx < kVRControllerMaxButtons); + + ControllerState().triggerValue[aButtonIdx] = (float)aTrigger; +} + +void VRMockController::SetAxisValue(uint32_t aAxisIdx, double aValue) { + MOZ_ASSERT(aAxisIdx < kVRControllerMaxAxis); + ControllerState().axisValue[aAxisIdx] = (float)aValue; +} + +void VRMockController::SetPose( + const Nullable<Float32Array>& aPosition, + const Nullable<Float32Array>& aLinearVelocity, + const Nullable<Float32Array>& aLinearAcceleration, + const Nullable<Float32Array>& aOrientation, + const Nullable<Float32Array>& aAngularVelocity, + const Nullable<Float32Array>& aAngularAcceleration, ErrorResult& aRv) { + VRControllerState& controllerState = ControllerState(); + controllerState.flags = GamepadCapabilityFlags::Cap_None; + + if (!aOrientation.IsNull()) { + if (!ReadFloat32Array(controllerState.pose.orientation, + aOrientation.Value(), aRv)) { + return; + } + controllerState.flags |= GamepadCapabilityFlags::Cap_Orientation; + } + if (!aAngularVelocity.IsNull()) { + if (!ReadFloat32Array(controllerState.pose.angularVelocity, + aAngularVelocity.Value(), aRv)) { + return; + } + controllerState.flags |= GamepadCapabilityFlags::Cap_AngularAcceleration; + } + if (!aAngularAcceleration.IsNull()) { + if (!ReadFloat32Array(controllerState.pose.angularAcceleration, + aAngularAcceleration.Value(), aRv)) { + return; + } + controllerState.flags |= GamepadCapabilityFlags::Cap_AngularAcceleration; + } + if (!aPosition.IsNull()) { + if (!ReadFloat32Array(controllerState.pose.position, aPosition.Value(), + aRv)) { + return; + } + controllerState.flags |= GamepadCapabilityFlags::Cap_Position; + } + if (!aLinearVelocity.IsNull()) { + if (!ReadFloat32Array(controllerState.pose.linearVelocity, + aLinearVelocity.Value(), aRv)) { + return; + } + controllerState.flags |= GamepadCapabilityFlags::Cap_LinearAcceleration; + } + if (!aLinearAcceleration.IsNull()) { + if (!ReadFloat32Array(controllerState.pose.linearAcceleration, + aLinearAcceleration.Value(), aRv)) { + return; + } + controllerState.flags |= GamepadCapabilityFlags::Cap_LinearAcceleration; + } +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(VRServiceTest, DOMEventTargetHelper, + mDisplay, mControllers, mWindow) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(VRServiceTest, + DOMEventTargetHelper) + +JSObject* VRServiceTest::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VRServiceTest_Binding::Wrap(aCx, this, aGivenProto); +} + +// static +already_AddRefed<VRServiceTest> VRServiceTest::CreateTestService( + nsPIDOMWindowInner* aWindow) { + MOZ_ASSERT(aWindow); + RefPtr<VRServiceTest> service = new VRServiceTest(aWindow); + return service.forget(); +} + +VRServiceTest::VRServiceTest(nsPIDOMWindowInner* aWindow) + : mWindow(aWindow), mPendingState{}, mEncodedState{}, mShuttingDown(false) { + mDisplay = new VRMockDisplay(this); + for (int i = 0; i < kVRControllerMaxCount; i++) { + mControllers.AppendElement(new VRMockController(this, i)); + } + ClearAll(); +} + +gfx::VRSystemState& VRServiceTest::SystemState() { return mPendingState; } + +VRMockDisplay* VRServiceTest::GetVRDisplay() { return mDisplay; } + +VRMockController* VRServiceTest::GetVRController(uint32_t aControllerIdx, + ErrorResult& aRv) { + if (aControllerIdx >= kVRControllerMaxCount) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return nullptr; + } + return mControllers[aControllerIdx]; +} + +void VRServiceTest::Shutdown() { + MOZ_ASSERT(!mShuttingDown); + mShuttingDown = true; + mWindow = nullptr; +} + +void VRServiceTest::AddCommand(uint64_t aCommand) { + EncodeData(); + mCommandBuffer.AppendElement(aCommand); +} + +void VRServiceTest::End() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_End); +} + +void VRServiceTest::ClearAll() { + memset(&mPendingState, 0, sizeof(VRSystemState)); + memset(&mEncodedState, 0, sizeof(VRSystemState)); + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_ClearAll); +} + +void VRServiceTest::ClearController(uint32_t aControllerIdx) { + MOZ_ASSERT(aControllerIdx < kVRControllerMaxCount); + mPendingState.controllerState[aControllerIdx].Clear(); + mEncodedState.controllerState[aControllerIdx].Clear(); + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_ClearController | + (uint64_t)aControllerIdx); +} + +void VRServiceTest::Timeout(uint32_t aDuration) { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_Timeout | + (uint64_t)aDuration); +} + +void VRServiceTest::Wait(uint32_t aDuration) { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_Wait | (uint64_t)aDuration); +} + +void VRServiceTest::WaitHapticIntensity(uint32_t aControllerIdx, + uint32_t aHapticIdx, double aIntensity, + ErrorResult& aRv) { + if (aControllerIdx >= kVRControllerMaxCount) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + if (aHapticIdx >= kVRHapticsMaxCount) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + // convert to 16.16 fixed point. This must match conversion in + // VRPuppetCommandBuffer::RunCommand + uint64_t iIntensity = round((float)aIntensity * (1 << 16)); + if (iIntensity > 0xffffffff) { + iIntensity = 0xffffffff; + } + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_WaitHapticIntensity | + ((uint64_t)aControllerIdx << 40) | ((uint64_t)aHapticIdx << 32) | + iIntensity); +} + +void VRServiceTest::WaitSubmit() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_WaitSubmit); +} + +void VRServiceTest::WaitPresentationStart() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_WaitPresentationStart); +} +void VRServiceTest::WaitPresentationEnd() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_WaitPresentationEnd); +} + +void VRServiceTest::EncodeData() { + VRPuppetCommandBuffer::EncodeStruct( + mCommandBuffer, (uint8_t*)&mPendingState.displayState, + (uint8_t*)&mEncodedState.displayState, sizeof(VRDisplayState), + VRPuppet_Command::VRPuppet_UpdateDisplay); + VRPuppetCommandBuffer::EncodeStruct( + mCommandBuffer, (uint8_t*)&mPendingState.sensorState, + (uint8_t*)&mEncodedState.sensorState, sizeof(VRHMDSensorState), + VRPuppet_Command::VRPuppet_UpdateSensor); + VRPuppetCommandBuffer::EncodeStruct( + mCommandBuffer, (uint8_t*)&mPendingState.controllerState, + (uint8_t*)&mEncodedState.controllerState, + sizeof(VRControllerState) * kVRControllerMaxCount, + VRPuppet_Command::VRPuppet_UpdateControllers); +} + +void VRServiceTest::CaptureFrame() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_CaptureFrame); +} + +void VRServiceTest::AcknowledgeFrame() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_AcknowledgeFrame); +} + +void VRServiceTest::RejectFrame() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_RejectFrame); +} + +void VRServiceTest::StartTimer() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_StartTimer); +} + +void VRServiceTest::StopTimer() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_StopTimer); +} + +void VRServiceTest::Commit() { + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_Commit); +} + +already_AddRefed<Promise> VRServiceTest::Run(ErrorResult& aRv) { + if (mShuttingDown) { + return nullptr; + } + + AddCommand((uint64_t)VRPuppet_Command::VRPuppet_End); + + RefPtr<dom::Promise> runPuppetPromise = + Promise::Create(mWindow->AsGlobal(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->RunPuppet(mCommandBuffer, runPuppetPromise, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + mCommandBuffer.Clear(); + + return runPuppetPromise.forget(); +} + +already_AddRefed<Promise> VRServiceTest::Reset(ErrorResult& aRv) { + if (mShuttingDown) { + return nullptr; + } + + RefPtr<dom::Promise> resetPuppetPromise = + Promise::Create(mWindow->AsGlobal(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->ResetPuppet(resetPuppetPromise, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + memset(&mPendingState, 0, sizeof(VRSystemState)); + memset(&mEncodedState, 0, sizeof(VRSystemState)); + mCommandBuffer.Clear(); + + return resetPuppetPromise.forget(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/vr/VRServiceTest.h b/dom/vr/VRServiceTest.h new file mode 100644 index 0000000000..b397108f6f --- /dev/null +++ b/dom/vr/VRServiceTest.h @@ -0,0 +1,206 @@ +/* -*- 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_VRServiceTest_h_ +#define mozilla_dom_VRServiceTest_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/VRServiceTestBinding.h" + +#include "gfxVR.h" + +namespace mozilla { +namespace gfx { +enum class VRDisplayCapabilityFlags : uint16_t; +enum class VRPuppet_Command : uint64_t; +} // namespace gfx +namespace dom { +enum class GamepadCapabilityFlags : uint16_t; + +class VRMockDisplay final : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(VRMockDisplay, DOMEventTargetHelper) + + explicit VRMockDisplay(VRServiceTest* aVRServiceTest); + + void Create(); + void Clear(); + + void SetConnected(bool aConnected); + bool Connected() const; + void SetMounted(bool aMounted); + bool Mounted() const; + void SetCapPosition(bool aEnabled); + bool CapPosition() const; + void SetCapOrientation(bool aEnabled); + bool CapOrientation() const; + void SetCapPresent(bool aEnabled); + bool CapPresent() const; + void SetCapExternal(bool aEnabled); + bool CapExternal() const; + void SetCapAngularAcceleration(bool aEnabled); + bool CapAngularAcceleration() const; + void SetCapLinearAcceleration(bool aEnabled); + bool CapLinearAcceleration() const; + void SetCapStageParameters(bool aEnabled); + bool CapStageParameters() const; + void SetCapMountDetection(bool aEnabled); + bool CapMountDetection() const; + void SetCapPositionEmulated(bool aEnabled); + bool CapPositionEmulated() const; + void SetEyeFOV(VREye aEye, double aUpDegree, double aRightDegree, + double aDownDegree, double aLeftDegree); + void SetEyeOffset(VREye aEye, double aOffsetX, double aOffsetY, + double aOffsetZ); + void SetEyeResolution(uint32_t aRenderWidth, uint32_t aRenderHeight); + void SetStageSize(double aWidth, double aHeight); + void SetSittingToStandingTransform(const Float32Array& aTransform, + ErrorResult& aRv); + void SetPose(const Nullable<Float32Array>& aPosition, + const Nullable<Float32Array>& aLinearVelocity, + const Nullable<Float32Array>& aLinearAcceleration, + const Nullable<Float32Array>& aOrientation, + const Nullable<Float32Array>& aAngularVelocity, + const Nullable<Float32Array>& aAngularAcceleration, + ErrorResult& aRv); + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + ~VRMockDisplay() = default; + gfx::VRDisplayState& DisplayState() const; + gfx::VRHMDSensorState& SensorState() const; + void SetCapFlag(gfx::VRDisplayCapabilityFlags aFlag, bool aEnabled); + bool GetCapFlag(gfx::VRDisplayCapabilityFlags aFlag) const; + + RefPtr<VRServiceTest> mVRServiceTest; +}; + +class VRMockController : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(VRMockController, + DOMEventTargetHelper) + + VRMockController(VRServiceTest* aVRServiceTest, uint32_t aControllerIdx); + void Create(); + void Clear(); + void SetHand(GamepadHand aHand); + GamepadHand Hand() const; + void SetCapPosition(bool aEnabled); + bool CapPosition() const; + void SetCapOrientation(bool aEnabled); + bool CapOrientation() const; + void SetCapAngularAcceleration(bool aEnabled); + bool CapAngularAcceleration() const; + void SetCapLinearAcceleration(bool aEnabled); + bool CapLinearAcceleration() const; + void SetAxisCount(uint32_t aCount); + uint32_t AxisCount() const; + void SetButtonCount(uint32_t aCount); + uint32_t ButtonCount() const; + void SetHapticCount(uint32_t aCount); + uint32_t HapticCount() const; + void SetButtonPressed(uint32_t aButtonIdx, bool aPressed); + void SetButtonTouched(uint32_t aButtonIdx, bool aTouched); + void SetButtonTrigger(uint32_t aButtonIdx, double aTrigger); + void SetAxisValue(uint32_t aAxisIdx, double aValue); + void SetPose(const Nullable<Float32Array>& aPosition, + const Nullable<Float32Array>& aLinearVelocity, + const Nullable<Float32Array>& aLinearAcceleration, + const Nullable<Float32Array>& aOrientation, + const Nullable<Float32Array>& aAngularVelocity, + const Nullable<Float32Array>& aAngularAcceleration, + ErrorResult& aRv); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + ~VRMockController() = default; + gfx::VRControllerState& ControllerState() const; + void SetCapFlag(GamepadCapabilityFlags aFlag, bool aEnabled); + bool GetCapFlag(GamepadCapabilityFlags aFlag) const; + RefPtr<VRServiceTest> mVRServiceTest; + uint32_t mControllerIdx; +}; + +class VRServiceTest final : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(VRServiceTest, DOMEventTargetHelper) + + // WebIDL interface + + void ClearAll(); + void ClearController(uint32_t aControllerIdx); + void Commit(); + void End(); + already_AddRefed<Promise> Run(ErrorResult& aRv); + already_AddRefed<Promise> Reset(ErrorResult& aRv); + VRMockDisplay* GetVRDisplay(); + VRMockController* GetVRController(uint32_t aControllerIdx, ErrorResult& aRv); + void Timeout(uint32_t aDuration); + void Wait(uint32_t aDuration); + void WaitSubmit(); + void WaitPresentationStart(); + void WaitPresentationEnd(); + void WaitHapticIntensity(uint32_t aControllerIdx, uint32_t aHapticIdx, + double aIntensity, ErrorResult& aRv); + void CaptureFrame(); + void AcknowledgeFrame(); + void RejectFrame(); + void StartTimer(); + void StopTimer(); + + // Implementation + void Shutdown(); + void AddCommand(uint64_t aCommand); + static already_AddRefed<VRServiceTest> CreateTestService( + nsPIDOMWindowInner* aWindow); + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + gfx::VRSystemState& SystemState(); + + private: + explicit VRServiceTest(nsPIDOMWindowInner* aWindow); + ~VRServiceTest() = default; + void EncodeData(); + + RefPtr<VRMockDisplay> mDisplay; + nsTArray<RefPtr<VRMockController>> mControllers; + nsCOMPtr<nsPIDOMWindowInner> mWindow; + // mPendingState records the state of the emulated VR hardware, including + // changes that have not yet been committed to the command buffer. + gfx::VRSystemState mPendingState; + // mEncodedState records the state of the emulate VR hardware at the end + // of the last committed transaction, submitted with VRServiceTest::Commit(). + // mPendingState represents the resulting state if all of the commands in + // mCommandBuffer have been played back. + gfx::VRSystemState mEncodedState; + // mCommandBuffer encodes a sequence of steps to be executed asynchronously by + // the simulated VR hardware. The steps are encoded as a stream of uint64's, + // using the format described in gfx/vr/VRPuppetCommandBuffer.h + // mCommandBuffer includes only complete transactions, which will be played + // back such that multiple values in VRSystemState will be updated atomically. + // When the command buffer is submitted to the PuppetSession, with + // VRServiceTest::Run(), it is cleared to ensure that the commands are not + // sent redundantly in subsequent VRServicetest::Run() calls. + // VRServiceTest::Commit() will perform a binary comparison of mPendingState + // and mEncodedState to determine what instructions need to be appended to + // mCommandBuffer. + // VRServiceTest::Reset() will effectively cancel all transactions and clear + // mCommandBuffer before submitting the reset request to the PuppetSession. + nsTArray<uint64_t> mCommandBuffer; + bool mShuttingDown; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_VRServiceTest_h_ diff --git a/dom/vr/XRBoundedReferenceSpace.cpp b/dom/vr/XRBoundedReferenceSpace.cpp new file mode 100644 index 0000000000..8d6eb25fdf --- /dev/null +++ b/dom/vr/XRBoundedReferenceSpace.cpp @@ -0,0 +1,73 @@ +/* -*- 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/XRBoundedReferenceSpace.h" +#include "mozilla/dom/XRRigidTransform.h" +#include "mozilla/dom/DOMPoint.h" +#include "VRDisplayClient.h" + +namespace mozilla::dom { + +XRBoundedReferenceSpace::XRBoundedReferenceSpace(nsIGlobalObject* aParent, + XRSession* aSession, + XRNativeOrigin* aNativeOrigin) + : XRReferenceSpace(aParent, aSession, aNativeOrigin, + XRReferenceSpaceType::Bounded_floor) {} + +JSObject* XRBoundedReferenceSpace::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return XRBoundedReferenceSpace_Binding::Wrap(aCx, this, aGivenProto); +} + +void XRBoundedReferenceSpace::GetBoundsGeometry( + nsTArray<RefPtr<DOMPointReadOnly>>& result) { + const auto size = + mSession->GetDisplayClient()->GetDisplayInfo().GetStageSize(); + if (size.width == 0 || size.height == 0) { + return; + } + + // https://immersive-web.github.io/webxr/#dom-xrboundedreferencespace-boundsgeometry + // bounds geometry must be premultiplied by the inverse of the origin offset. + gfx::PointDouble3D offset = mNativeOrigin->GetPosition(); + + const auto addPoint = [&](const double x, const double z) { + RefPtr<DOMPointReadOnly> obj = new DOMPointReadOnly( + GetParentObject(), x - offset.x, 0.0f, z - offset.z, 1.0f); + result.EmplaceBack(obj); + }; + + addPoint(-size.width * 0.5f, size.height * 0.5f); + addPoint(size.width * 0.5f, size.height * 0.5f); + addPoint(size.width * 0.5f, -size.height * 0.5f); + addPoint(-size.width * 0.5f, -size.height * 0.5f); + + // TODO (Bug 1611526): Support WebXR bounded reference spaces +} + +already_AddRefed<XRReferenceSpace> +XRBoundedReferenceSpace::GetOffsetReferenceSpace( + const XRRigidTransform& aOriginOffset) { + RefPtr<XRBoundedReferenceSpace> offsetReferenceSpace = + new XRBoundedReferenceSpace(GetParentObject(), mSession, mNativeOrigin); + + // https://immersive-web.github.io/webxr/#multiply-transforms + // An XRRigidTransform is essentially a rotation followed by a translation + gfx::QuaternionDouble otherOrientation = aOriginOffset.RawOrientation(); + // The resulting rotation is the two combined + offsetReferenceSpace->mOriginOffsetOrientation = + mOriginOffsetOrientation * otherOrientation; + // We first apply the rotation of aOriginOffset to + // mOriginOffsetPosition offset, then translate by the offset of + // aOriginOffset + offsetReferenceSpace->mOriginOffsetPosition = + otherOrientation.RotatePoint(mOriginOffsetPosition) + + aOriginOffset.RawPosition(); + + return offsetReferenceSpace.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRBoundedReferenceSpace.h b/dom/vr/XRBoundedReferenceSpace.h new file mode 100644 index 0000000000..3748ff47e5 --- /dev/null +++ b/dom/vr/XRBoundedReferenceSpace.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XRBoundedReferenceSpace_h_ +#define mozilla_dom_XRBoundedReferenceSpace_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" +#include "mozilla/dom/XRReferenceSpace.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +class DOMPointReadOnly; +class XRSession; + +class XRBoundedReferenceSpace final : public XRReferenceSpace { + public: + explicit XRBoundedReferenceSpace(nsIGlobalObject* aParent, + XRSession* aSession, + XRNativeOrigin* aNativeOrigin); + + // WebIDL Boilerplate + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + void GetBoundsGeometry(nsTArray<RefPtr<DOMPointReadOnly>>& result); + already_AddRefed<XRReferenceSpace> GetOffsetReferenceSpace( + const XRRigidTransform& aOriginOffset) override; + + protected: + virtual ~XRBoundedReferenceSpace() = default; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRBoundedReferenceSpace_h_ diff --git a/dom/vr/XRFrame.cpp b/dom/vr/XRFrame.cpp new file mode 100644 index 0000000000..639bb2b019 --- /dev/null +++ b/dom/vr/XRFrame.cpp @@ -0,0 +1,202 @@ +/* -*- 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/XRFrame.h" +#include "mozilla/dom/XRRenderState.h" +#include "mozilla/dom/XRRigidTransform.h" +#include "mozilla/dom/XRViewerPose.h" +#include "mozilla/dom/XRView.h" +#include "mozilla/dom/XRReferenceSpace.h" +#include "VRDisplayClient.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(XRFrame, mParent, mSession) + +XRFrame::XRFrame(nsISupports* aParent, XRSession* aXRSession) + : mParent(aParent), + mSession(aXRSession), + mActive(false), + mAnimationFrame(false) {} + +JSObject* XRFrame::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRFrame_Binding::Wrap(aCx, this, aGivenProto); +} + +XRSession* XRFrame::Session() { return mSession; } + +already_AddRefed<XRViewerPose> XRFrame::GetViewerPose( + const XRReferenceSpace& aReferenceSpace, ErrorResult& aRv) { + if (!mActive || !mAnimationFrame) { + aRv.ThrowInvalidStateError( + "GetViewerPose can only be called on an XRFrame during an " + "XRSession.requestAnimationFrame callback."); + return nullptr; + } + + if (aReferenceSpace.GetSession() != mSession) { + aRv.ThrowInvalidStateError( + "The XRReferenceSpace passed to GetViewerPose must belong to the " + "XRSession that GetViewerPose is called on."); + return nullptr; + } + + if (!mSession->CanReportPoses()) { + aRv.ThrowSecurityError( + "The visibilityState of the XRSpace's XRSession " + "that is passed to GetViewerPose must be 'visible'."); + return nullptr; + } + + // TODO (Bug 1616393) - Check if poses must be limited: + // https://immersive-web.github.io/webxr/#poses-must-be-limited + + bool emulatedPosition = aReferenceSpace.IsPositionEmulated(); + + XRRenderState* renderState = mSession->GetActiveRenderState(); + float depthNear = (float)renderState->DepthNear(); + float depthFar = (float)renderState->DepthFar(); + + RefPtr<XRViewerPose> viewerPose; + + gfx::VRDisplayClient* display = mSession->GetDisplayClient(); + if (display) { + // Have a VRDisplayClient + const gfx::VRDisplayInfo& displayInfo = + mSession->GetDisplayClient()->GetDisplayInfo(); + const gfx::VRHMDSensorState& sensorState = display->GetSensorState(); + + gfx::PointDouble3D viewerPosition = gfx::PointDouble3D( + sensorState.pose.position[0], sensorState.pose.position[1], + sensorState.pose.position[2]); + gfx::QuaternionDouble viewerOrientation = gfx::QuaternionDouble( + sensorState.pose.orientation[0], sensorState.pose.orientation[1], + sensorState.pose.orientation[2], sensorState.pose.orientation[3]); + + gfx::Matrix4x4Double headTransform; + headTransform.SetRotationFromQuaternion(viewerOrientation); + headTransform.PostTranslate(viewerPosition); + + gfx::Matrix4x4Double originTransform; + originTransform.SetRotationFromQuaternion( + aReferenceSpace.GetEffectiveOriginOrientation().Inverse()); + originTransform.PreTranslate(-aReferenceSpace.GetEffectiveOriginPosition()); + + headTransform *= originTransform; + + viewerPose = mSession->PooledViewerPose(headTransform, emulatedPosition); + + auto updateEye = [&](int32_t viewIndex, gfx::VRDisplayState::Eye eye) { + auto offset = displayInfo.GetEyeTranslation(eye); + auto eyeFromHead = gfx::Matrix4x4Double::Translation( + gfx::PointDouble3D(offset.x, offset.y, offset.z)); + auto eyeTransform = eyeFromHead * headTransform; + gfx::PointDouble3D eyePosition; + gfx::QuaternionDouble eyeRotation; + gfx::PointDouble3D eyeScale; + eyeTransform.Decompose(eyePosition, eyeRotation, eyeScale); + + const gfx::VRFieldOfView fov = displayInfo.mDisplayState.eyeFOV[eye]; + gfx::Matrix4x4 projection = + fov.ConstructProjectionMatrix(depthNear, depthFar, true); + viewerPose->GetEye(viewIndex)->Update(eyePosition, eyeRotation, + projection); + }; + + updateEye(0, gfx::VRDisplayState::Eye_Left); + updateEye(1, gfx::VRDisplayState::Eye_Right); + } else { + auto inlineVerticalFov = renderState->GetInlineVerticalFieldOfView(); + const double fov = + inlineVerticalFov.IsNull() ? M_PI * 0.5f : inlineVerticalFov.Value(); + HTMLCanvasElement* canvas = renderState->GetOutputCanvas(); + float aspect = 1.0f; + if (canvas) { + aspect = (float)canvas->Width() / (float)canvas->Height(); + } + gfx::Matrix4x4 projection = + ConstructInlineProjection((float)fov, aspect, depthNear, depthFar); + + viewerPose = + mSession->PooledViewerPose(gfx::Matrix4x4Double(), emulatedPosition); + viewerPose->GetEye(0)->Update(gfx::PointDouble3D(), gfx::QuaternionDouble(), + projection); + } + + return viewerPose.forget(); +} + +already_AddRefed<XRPose> XRFrame::GetPose(const XRSpace& aSpace, + const XRSpace& aBaseSpace, + ErrorResult& aRv) { + if (!mActive) { + aRv.ThrowInvalidStateError( + "GetPose can not be called on an XRFrame that is not active."); + return nullptr; + } + + if (aSpace.GetSession() != mSession || aBaseSpace.GetSession() != mSession) { + aRv.ThrowInvalidStateError( + "The XRSpace passed to GetPose must belong to the " + "XRSession that GetPose is called on."); + return nullptr; + } + + if (!mSession->CanReportPoses()) { + aRv.ThrowSecurityError( + "The visibilityState of the XRSpace's XRSession " + "that is passed to GetPose must be 'visible'."); + return nullptr; + } + + // TODO (Bug 1616393) - Check if poses must be limited: + // https://immersive-web.github.io/webxr/#poses-must-be-limited + + const bool emulatedPosition = aSpace.IsPositionEmulated(); + gfx::Matrix4x4Double base; + base.SetRotationFromQuaternion( + aBaseSpace.GetEffectiveOriginOrientation().Inverse()); + base.PreTranslate(-aBaseSpace.GetEffectiveOriginPosition()); + + gfx::Matrix4x4Double matrix = aSpace.GetEffectiveOriginTransform() * base; + + RefPtr<XRRigidTransform> transform = new XRRigidTransform(mParent, matrix); + RefPtr<XRPose> pose = new XRPose(mParent, transform, emulatedPosition); + + return pose.forget(); +} + +void XRFrame::StartAnimationFrame() { + mActive = true; + mAnimationFrame = true; +} + +void XRFrame::EndAnimationFrame() { mActive = false; } + +void XRFrame::StartInputSourceEvent() { mActive = true; } + +void XRFrame::EndInputSourceEvent() { mActive = false; } + +gfx::Matrix4x4 XRFrame::ConstructInlineProjection(float aFov, float aAspect, + float aNear, float aFar) { + gfx::Matrix4x4 m; + const float depth = aFar - aNear; + const float invDepth = 1 / depth; + if (aFov == 0) { + aFov = 0.5f * M_PI; + } + + m._22 = 1.0f / tan(0.5f * aFov); + m._11 = -m._22 / aAspect; + m._33 = depth * invDepth; + m._43 = (-aFar * aNear) * invDepth; + m._34 = 1.0f; + + return m; +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRFrame.h b/dom/vr/XRFrame.h new file mode 100644 index 0000000000..26bd601f3a --- /dev/null +++ b/dom/vr/XRFrame.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XRFrame_h_ +#define mozilla_dom_XRFrame_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +class XRFrameOfReference; +class XRInputPose; +class XRInputSource; +class XRPose; +class XRReferenceSpace; +class XRSession; +class XRSpace; +class XRViewerPose; + +class XRFrame final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(XRFrame) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(XRFrame) + + explicit XRFrame(nsISupports* aParent, XRSession* aXRSession); + + void StartAnimationFrame(); + void EndAnimationFrame(); + void StartInputSourceEvent(); + void EndInputSourceEvent(); + + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + XRSession* Session(); + already_AddRefed<XRViewerPose> GetViewerPose( + const XRReferenceSpace& aReferenceSpace, ErrorResult& aRv); + already_AddRefed<XRPose> GetPose(const XRSpace& aSpace, + const XRSpace& aBaseSpace, ErrorResult& aRv); + gfx::Matrix4x4 ConstructInlineProjection(float aFov, float aAspect, + float aNear, float aFar); + + protected: + virtual ~XRFrame() = default; + + nsCOMPtr<nsISupports> mParent; + RefPtr<XRSession> mSession; + bool mActive; + bool mAnimationFrame; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRFrame_h_ diff --git a/dom/vr/XRInputSource.cpp b/dom/vr/XRInputSource.cpp new file mode 100644 index 0000000000..8cf2849d1c --- /dev/null +++ b/dom/vr/XRInputSource.cpp @@ -0,0 +1,400 @@ +/* -*- 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/XRInputSource.h" +#include "mozilla/dom/XRInputSourceEvent.h" +#include "XRNativeOriginViewer.h" +#include "XRNativeOriginTracker.h" +#include "XRInputSpace.h" +#include "VRDisplayClient.h" + +#include "mozilla/dom/Gamepad.h" +#include "mozilla/dom/GamepadManager.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(XRInputSource, mParent, mTargetRaySpace, + mGripSpace, mGamepad) + +// Follow the controller profile ids from +// https://github.com/immersive-web/webxr-input-profiles. +nsTArray<nsString> GetInputSourceProfile(gfx::VRControllerType aType) { + nsTArray<nsString> profile; + nsString id; + + switch (aType) { + case gfx::VRControllerType::HTCVive: + id.AssignLiteral("htc-vive"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-touchpad"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::HTCViveCosmos: + id.AssignLiteral("htc-vive-cosmos"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-thumbstick"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::HTCViveFocus: + id.AssignLiteral("htc-vive-focus"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-touchpad"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::HTCViveFocusPlus: + id.AssignLiteral("htc-vive-focus-plus"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-touchpad"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::MSMR: + id.AssignLiteral("microsoft-mixed-reality"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-touchpad-thumbstick"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::ValveIndex: + id.AssignLiteral("valve-index"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-touchpad-thumbstick"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::OculusGo: + id.AssignLiteral("oculus-go"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-touchpad"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::OculusTouch: + id.AssignLiteral("oculus-touch"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-thumbstick"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::OculusTouch2: + id.AssignLiteral("oculus-touch-v2"); + profile.AppendElement(id); + id.AssignLiteral("oculus-touch"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-thumbstick"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::OculusTouch3: + id.AssignLiteral("oculus-touch-v3"); + profile.AppendElement(id); + id.AssignLiteral("oculus-touch-v2"); + profile.AppendElement(id); + id.AssignLiteral("oculus-touch"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-thumbstick"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::PicoGaze: + id.AssignLiteral("pico-gaze"); + profile.AppendElement(id); + id.AssignLiteral("generic-button"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::PicoG2: + id.AssignLiteral("pico-g2"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-touchpad"); + profile.AppendElement(id); + break; + case gfx::VRControllerType::PicoNeo2: + id.AssignLiteral("pico-neo2"); + profile.AppendElement(id); + id.AssignLiteral("generic-trigger-squeeze-thumbstick"); + profile.AppendElement(id); + break; + default: + NS_WARNING("Unsupported XR input source profile.\n"); + break; + } + return profile; +} + +XRInputSource::XRInputSource(nsISupports* aParent) + : mParent(aParent), + mGamepad(nullptr), + mIndex(-1), + mSelectAction(ActionState::ActionState_Released), + mSqueezeAction(ActionState::ActionState_Released) {} + +XRInputSource::~XRInputSource() { + mTargetRaySpace = nullptr; + mGripSpace = nullptr; + mGamepad = nullptr; +} + +JSObject* XRInputSource::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRInputSource_Binding::Wrap(aCx, this, aGivenProto); +} + +XRHandedness XRInputSource::Handedness() { return mHandedness; } + +XRTargetRayMode XRInputSource::TargetRayMode() { return mTargetRayMode; } + +XRSpace* XRInputSource::TargetRaySpace() { return mTargetRaySpace; } + +XRSpace* XRInputSource::GetGripSpace() { return mGripSpace; } + +void XRInputSource::GetProfiles(nsTArray<nsString>& aResult) { + aResult = mProfiles.Clone(); +} + +Gamepad* XRInputSource::GetGamepad() { return mGamepad; } + +void XRInputSource::Setup(XRSession* aSession, uint32_t aIndex) { + MOZ_ASSERT(aSession); + gfx::VRDisplayClient* displayClient = aSession->GetDisplayClient(); + if (!displayClient) { + MOZ_ASSERT(displayClient); + return; + } + const gfx::VRDisplayInfo& displayInfo = displayClient->GetDisplayInfo(); + const gfx::VRControllerState& controllerState = + displayInfo.mControllerState[aIndex]; + MOZ_ASSERT(controllerState.controllerName[0] != '\0'); + + mProfiles = GetInputSourceProfile(controllerState.type); + mHandedness = XRHandedness::None; + switch (controllerState.hand) { + case GamepadHand::_empty: + mHandedness = XRHandedness::None; + break; + case GamepadHand::Left: + mHandedness = XRHandedness::Left; + break; + case GamepadHand::Right: + mHandedness = XRHandedness::Right; + break; + default: + MOZ_ASSERT(false && "Unknown GamepadHand type."); + break; + } + + RefPtr<XRNativeOrigin> nativeOriginTargetRay = nullptr; + mTargetRayMode = XRTargetRayMode::Tracked_pointer; + switch (controllerState.targetRayMode) { + case gfx::TargetRayMode::Gaze: + mTargetRayMode = XRTargetRayMode::Gaze; + nativeOriginTargetRay = new XRNativeOriginViewer(displayClient); + break; + case gfx::TargetRayMode::TrackedPointer: + mTargetRayMode = XRTargetRayMode::Tracked_pointer; + // We use weak pointers of poses in XRNativeOriginTracker to sync their + // data internally. + nativeOriginTargetRay = + new XRNativeOriginTracker(&controllerState.targetRayPose); + break; + case gfx::TargetRayMode::Screen: + mTargetRayMode = XRTargetRayMode::Screen; + break; + default: + MOZ_ASSERT(false && "Undefined TargetRayMode type."); + break; + } + + mTargetRaySpace = new XRInputSpace(aSession->GetParentObject(), aSession, + nativeOriginTargetRay, aIndex); + + const uint32_t gamepadHandleValue = + displayInfo.mDisplayID * gfx::kVRControllerMaxCount + aIndex; + + const GamepadHandle gamepadHandle{gamepadHandleValue, GamepadHandleKind::VR}; + + mGamepad = + new Gamepad(mParent, NS_ConvertASCIItoUTF16(""), -1, gamepadHandle, + GamepadMappingType::Xr_standard, controllerState.hand, + displayInfo.mDisplayID, controllerState.numButtons, + controllerState.numAxes, controllerState.numHaptics, 0, 0); + mIndex = aIndex; + + if (!mGripSpace) { + CreateGripSpace(aSession, controllerState); + } +} + +void XRInputSource::SetGamepadIsConnected(bool aConnected, + XRSession* aSession) { + mGamepad->SetConnected(aConnected); + MOZ_ASSERT(aSession); + + if (!aConnected) { + if (mSelectAction != ActionState::ActionState_Released) { + DispatchEvent(u"selectend"_ns, aSession); + mSelectAction = ActionState::ActionState_Released; + } + if (mSqueezeAction != ActionState::ActionState_Released) { + DispatchEvent(u"squeezeend"_ns, aSession); + mSqueezeAction = ActionState::ActionState_Released; + } + } +} + +void XRInputSource::Update(XRSession* aSession) { + MOZ_ASSERT(aSession && mIndex >= 0 && mGamepad); + + gfx::VRDisplayClient* displayClient = aSession->GetDisplayClient(); + if (!displayClient) { + MOZ_ASSERT(displayClient); + return; + } + const gfx::VRDisplayInfo& displayInfo = displayClient->GetDisplayInfo(); + const gfx::VRControllerState& controllerState = + displayInfo.mControllerState[mIndex]; + MOZ_ASSERT(controllerState.controllerName[0] != '\0'); + + // OculusVR and OpenVR controllers need to wait until + // update functions to assign GamepadCapabilityFlags::Cap_GripSpacePosition + // flag. + if (!mGripSpace) { + CreateGripSpace(aSession, controllerState); + } + + // Update button values. + nsTArray<RefPtr<GamepadButton>> buttons; + mGamepad->GetButtons(buttons); + for (uint32_t i = 0; i < buttons.Length(); ++i) { + const bool pressed = controllerState.buttonPressed & (1ULL << i); + const bool touched = controllerState.buttonTouched & (1ULL << i); + + if (buttons[i]->Pressed() != pressed || buttons[i]->Touched() != touched || + buttons[i]->Value() != controllerState.triggerValue[i]) { + mGamepad->SetButton(i, pressed, touched, controllerState.triggerValue[i]); + } + } + // Update axis values. + nsTArray<double> axes; + mGamepad->GetAxes(axes); + for (uint32_t i = 0; i < axes.Length(); ++i) { + if (axes[i] != controllerState.axisValue[i]) { + mGamepad->SetAxis(i, controllerState.axisValue[i]); + } + } + + // We define 0.85f and 0.15f based on our current finding + // for better experience, we can adjust these values if we need. + const float completeThreshold = 0.90f; + const float startThreshold = 0.85f; + const float endThreshold = 0.15f; + const uint32_t selectIndex = 0; + const uint32_t squeezeIndex = 1; + + // Checking selectstart, select, selectend + if (buttons.Length() > selectIndex) { + if (controllerState.selectActionStartFrameId > + controllerState.selectActionStopFrameId) { + if (mSelectAction == ActionState::ActionState_Released && + controllerState.triggerValue[selectIndex] > endThreshold) { + DispatchEvent(u"selectstart"_ns, aSession); + mSelectAction = ActionState::ActionState_Pressing; + } else if (mSelectAction == ActionState::ActionState_Pressing && + controllerState.triggerValue[selectIndex] > + completeThreshold) { + mSelectAction = ActionState::ActionState_Pressed; + } else if (mSelectAction == ActionState::ActionState_Pressed && + controllerState.triggerValue[selectIndex] < startThreshold) { + DispatchEvent(u"select"_ns, aSession); + mSelectAction = ActionState::ActionState_Releasing; + } else if (mSelectAction <= ActionState::ActionState_Releasing && + controllerState.triggerValue[selectIndex] < endThreshold) { + // For a select btn which only has pressed and unpressed status. + if (mSelectAction == ActionState::ActionState_Pressed) { + DispatchEvent(u"select"_ns, aSession); + } + DispatchEvent(u"selectend"_ns, aSession); + mSelectAction = ActionState::ActionState_Released; + } + } else if (mSelectAction <= ActionState::ActionState_Releasing) { + // For a select btn which only has pressed and unpressed status. + if (mSelectAction == ActionState::ActionState_Pressed) { + DispatchEvent(u"select"_ns, aSession); + } + DispatchEvent(u"selectend"_ns, aSession); + mSelectAction = ActionState::ActionState_Released; + } + } + + // Checking squeezestart, squeeze, squeezeend + if (buttons.Length() > squeezeIndex) { + if (controllerState.squeezeActionStartFrameId > + controllerState.squeezeActionStopFrameId) { + if (mSqueezeAction == ActionState::ActionState_Released && + controllerState.triggerValue[squeezeIndex] > endThreshold) { + DispatchEvent(u"squeezestart"_ns, aSession); + mSqueezeAction = ActionState::ActionState_Pressing; + } else if (mSqueezeAction == ActionState::ActionState_Pressing && + controllerState.triggerValue[squeezeIndex] > + completeThreshold) { + mSqueezeAction = ActionState::ActionState_Pressed; + } else if (mSqueezeAction == ActionState::ActionState_Pressed && + controllerState.triggerValue[squeezeIndex] < startThreshold) { + DispatchEvent(u"squeeze"_ns, aSession); + mSqueezeAction = ActionState::ActionState_Releasing; + } else if (mSqueezeAction <= ActionState::ActionState_Releasing && + controllerState.triggerValue[squeezeIndex] < endThreshold) { + // For a squeeze btn which only has pressed and unpressed status. + if (mSqueezeAction == ActionState::ActionState_Pressed) { + DispatchEvent(u"squeeze"_ns, aSession); + } + DispatchEvent(u"squeezeend"_ns, aSession); + mSqueezeAction = ActionState::ActionState_Released; + } + } else if (mSqueezeAction <= ActionState::ActionState_Releasing) { + // For a squeeze btn which only has pressed and unpressed status. + if (mSqueezeAction == ActionState::ActionState_Pressed) { + DispatchEvent(u"squeeze"_ns, aSession); + } + DispatchEvent(u"squeezeend"_ns, aSession); + mSqueezeAction = ActionState::ActionState_Released; + } + } +} + +int32_t XRInputSource::GetIndex() { return mIndex; } + +void XRInputSource::DispatchEvent(const nsAString& aEvent, + XRSession* aSession) { + if (!GetParentObject() || !aSession) { + return; + } + // Create a XRFrame for its callbacks + RefPtr<XRFrame> frame = new XRFrame(GetParentObject(), aSession); + frame->StartInputSourceEvent(); + + XRInputSourceEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mFrame = frame; + init.mInputSource = this; + + RefPtr<XRInputSourceEvent> event = + XRInputSourceEvent::Constructor(aSession, aEvent, init); + + event->SetTrusted(true); + aSession->DispatchEvent(*event); + frame->EndInputSourceEvent(); +} + +void XRInputSource::CreateGripSpace( + XRSession* aSession, const gfx::VRControllerState& controllerState) { + MOZ_ASSERT(!mGripSpace); + MOZ_ASSERT(aSession && mIndex >= 0 && mGamepad); + if (mTargetRayMode == XRTargetRayMode::Tracked_pointer && + controllerState.flags & GamepadCapabilityFlags::Cap_GripSpacePosition) { + RefPtr<XRNativeOrigin> nativeOriginGrip = nullptr; + nativeOriginGrip = new XRNativeOriginTracker(&controllerState.pose); + mGripSpace = new XRInputSpace(aSession->GetParentObject(), aSession, + nativeOriginGrip, mIndex); + } else { + mGripSpace = nullptr; + } +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRInputSource.h b/dom/vr/XRInputSource.h new file mode 100644 index 0000000000..2f9405c18e --- /dev/null +++ b/dom/vr/XRInputSource.h @@ -0,0 +1,83 @@ +/* -*- 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_XRInputSource_h_ +#define mozilla_dom_XRInputSource_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" + +#include "gfxVR.h" + +namespace mozilla { +namespace gfx { +class VRDisplayClient; +} // namespace gfx +namespace dom { +class Gamepad; +class XRSpace; +class XRSession; +class XRNativeOrigin; +enum class XRHandedness : uint8_t; +enum class XRTargetRayMode : uint8_t; + +class XRInputSource final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(XRInputSource) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(XRInputSource) + + explicit XRInputSource(nsISupports* aParent); + + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + XRHandedness Handedness(); + XRTargetRayMode TargetRayMode(); + XRSpace* TargetRaySpace(); + XRSpace* GetGripSpace(); + void GetProfiles(nsTArray<nsString>& aResult); + Gamepad* GetGamepad(); + void Setup(XRSession* aSession, uint32_t aIndex); + void SetGamepadIsConnected(bool aConnected, XRSession* aSession); + void Update(XRSession* aSession); + int32_t GetIndex(); + + protected: + virtual ~XRInputSource(); + + nsCOMPtr<nsISupports> mParent; + + private: + enum class ActionState : uint8_t { + ActionState_Pressing = 0, + ActionState_Pressed = 1, + ActionState_Releasing = 2, + ActionState_Released = 3 + }; + + void CreateGripSpace(XRSession* aSession, + const gfx::VRControllerState& controllerState); + void DispatchEvent(const nsAString& aEvent, XRSession* aSession); + + nsTArray<nsString> mProfiles; + XRHandedness mHandedness; + XRTargetRayMode mTargetRayMode; + + RefPtr<XRSpace> mTargetRaySpace; + RefPtr<XRSpace> mGripSpace; + RefPtr<Gamepad> mGamepad; + int32_t mIndex; + ActionState mSelectAction; + ActionState mSqueezeAction; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XRInputSource_h_ diff --git a/dom/vr/XRInputSourceArray.cpp b/dom/vr/XRInputSourceArray.cpp new file mode 100644 index 0000000000..2cc2c4537a --- /dev/null +++ b/dom/vr/XRInputSourceArray.cpp @@ -0,0 +1,167 @@ +/* -*- 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/XRInputSourceArray.h" +#include "mozilla/dom/XRSession.h" +#include "mozilla/dom/XRInputSourcesChangeEvent.h" +#include "VRDisplayClient.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(XRInputSourceArray, mParent, + mInputSources) +NS_IMPL_CYCLE_COLLECTING_ADDREF(XRInputSourceArray) +NS_IMPL_CYCLE_COLLECTING_RELEASE(XRInputSourceArray) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XRInputSourceArray) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +XRInputSourceArray::XRInputSourceArray(nsISupports* aParent) + : mParent(aParent) {} + +JSObject* XRInputSourceArray::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRInputSourceArray_Binding::Wrap(aCx, this, aGivenProto); +} + +void XRInputSourceArray::Update(XRSession* aSession) { + MOZ_ASSERT(aSession); + + gfx::VRDisplayClient* displayClient = aSession->GetDisplayClient(); + if (!displayClient) { + return; + } + + XRInputSourcesChangeEventInit addInit; + nsTArray<RefPtr<XRInputSource>> removedInputs; + if (NS_WARN_IF(!addInit.mAdded.SetCapacity(gfx::kVRControllerMaxCount, + mozilla::fallible))) { + MOZ_ASSERT(false, + "'add' sequence in XRInputSourcesChangeEventInit SetCapacity() " + "failed."); + return; + } + + for (int32_t i = 0; i < gfx::kVRControllerMaxCount; ++i) { + const gfx::VRControllerState& controllerState = + displayClient->GetDisplayInfo().mControllerState[i]; + if (controllerState.controllerName[0] == '\0') { + // Checking if exising controllers need to be removed. + for (auto& input : mInputSources) { + if (input->GetIndex() == i) { + removedInputs.AppendElement(input); + break; + } + } + continue; + } + + bool found = false; + RefPtr<XRInputSource> inputSource = nullptr; + for (auto& input : mInputSources) { + if (input->GetIndex() == i) { + found = true; + inputSource = input; + break; + } + } + // Checking if it is added before. + if (!found && + (controllerState.numButtons > 0 || controllerState.numAxes > 0)) { + inputSource = new XRInputSource(mParent); + inputSource->Setup(aSession, i); + mInputSources.AppendElement(inputSource); + + addInit.mBubbles = false; + addInit.mCancelable = false; + addInit.mSession = aSession; + if (!addInit.mAdded.AppendElement(*inputSource, mozilla::fallible)) { + MOZ_ASSERT(false, + "'add' sequence in XRInputSourcesChangeEventInit " + "AppendElement() failed, it might be due to the" + "wrong size when SetCapacity()."); + } + } + // If added, updating the current controller states. + if (inputSource) { + inputSource->Update(aSession); + } + } + + // Send `inputsourceschange` for new controllers. + if (addInit.mAdded.Length()) { + RefPtr<XRInputSourcesChangeEvent> event = + XRInputSourcesChangeEvent::Constructor( + aSession, u"inputsourceschange"_ns, addInit); + + event->SetTrusted(true); + aSession->DispatchEvent(*event); + } + + // If there's a controller is removed, dispatch `inputsourceschange`. + if (removedInputs.Length()) { + DispatchInputSourceRemovedEvent(removedInputs, aSession); + } + for (auto& input : removedInputs) { + mInputSources.RemoveElement(input); + } +} + +void XRInputSourceArray::DispatchInputSourceRemovedEvent( + const nsTArray<RefPtr<XRInputSource>>& aInputs, XRSession* aSession) { + if (!aSession) { + return; + } + + XRInputSourcesChangeEventInit init; + if (NS_WARN_IF( + !init.mRemoved.SetCapacity(aInputs.Length(), mozilla::fallible))) { + MOZ_ASSERT(false, + "'removed' sequence in XRInputSourcesChangeEventInit " + "SetCapacity() failed."); + return; + } + for (const auto& input : aInputs) { + input->SetGamepadIsConnected(false, aSession); + init.mBubbles = false; + init.mCancelable = false; + init.mSession = aSession; + if (!init.mRemoved.AppendElement(*input, mozilla::fallible)) { + MOZ_ASSERT(false, + "'removed' sequence in XRInputSourcesChangeEventInit " + "AppendElement() failed, it might be due to the" + "wrong size when SetCapacity()."); + } + } + + if (init.mRemoved.Length()) { + RefPtr<XRInputSourcesChangeEvent> event = + XRInputSourcesChangeEvent::Constructor(aSession, + u"inputsourceschange"_ns, init); + + event->SetTrusted(true); + aSession->DispatchEvent(*event); + } +} + +void XRInputSourceArray::Clear(XRSession* aSession) { + DispatchInputSourceRemovedEvent(mInputSources, aSession); + mInputSources.Clear(); +} + +uint32_t XRInputSourceArray::Length() { return mInputSources.Length(); } + +XRInputSource* XRInputSourceArray::IndexedGetter(uint32_t aIndex, + bool& aFound) { + aFound = aIndex < mInputSources.Length(); + if (!aFound) { + return nullptr; + } + return mInputSources[aIndex]; +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRInputSourceArray.h b/dom/vr/XRInputSourceArray.h new file mode 100644 index 0000000000..424e006295 --- /dev/null +++ b/dom/vr/XRInputSourceArray.h @@ -0,0 +1,55 @@ +/* -*- 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_XRInputSourceArray_h_ +#define mozilla_dom_XRInputSourceArray_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" + +#include "gfxVR.h" + +namespace mozilla { +namespace gfx { +struct VRControllerState; +} +namespace dom { +class XRInputSource; + +class XRInputSourceArray final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(XRInputSourceArray) + + explicit XRInputSourceArray(nsISupports* aParent); + + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + XRInputSource* IndexedGetter(uint32_t aIndex, bool& aFound); + uint32_t Length(); + void Setup(XRSession* aSession, RefPtr<gfx::VRDisplayClient> aDisplayClient); + void Update(XRSession* aSession); + void Clear(XRSession* aSession); + + protected: + virtual ~XRInputSourceArray() = default; + + private: + void DispatchInputSourceRemovedEvent( + const nsTArray<RefPtr<XRInputSource>>& aInputs, XRSession* aSession); + + nsCOMPtr<nsISupports> mParent; + nsTArray<RefPtr<XRInputSource>> mInputSources; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XRInputSourceArray_h_ diff --git a/dom/vr/XRInputSpace.cpp b/dom/vr/XRInputSpace.cpp new file mode 100644 index 0000000000..34ccd37607 --- /dev/null +++ b/dom/vr/XRInputSpace.cpp @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XRInputSpace.h" +#include "VRDisplayClient.h" +#include "mozilla/dom/GamepadPoseState.h" + +namespace mozilla::dom { + +XRInputSpace::XRInputSpace(nsIGlobalObject* aParent, XRSession* aSession, + XRNativeOrigin* aNativeOrigin, + int32_t aControllerIndex) + : XRSpace(aParent, aSession, aNativeOrigin), mIndex(aControllerIndex) {} + +bool XRInputSpace::IsPositionEmulated() const { + gfx::VRDisplayClient* display = mSession->GetDisplayClient(); + if (!display) { + // When there are no sensors, the position is considered emulated. + return true; + } + const gfx::VRDisplayInfo& displayInfo = display->GetDisplayInfo(); + const gfx::VRControllerState& controllerState = + displayInfo.mControllerState[mIndex]; + MOZ_ASSERT(controllerState.controllerName[0] != '\0'); + + return ( + (controllerState.flags & GamepadCapabilityFlags::Cap_PositionEmulated) != + GamepadCapabilityFlags::Cap_None); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRInputSpace.h b/dom/vr/XRInputSpace.h new file mode 100644 index 0000000000..82bc494776 --- /dev/null +++ b/dom/vr/XRInputSpace.h @@ -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/. */ + +#ifndef mozilla_dom_XRInputSpace_h_ +#define mozilla_dom_XRInputSpace_h_ + +#include "XRInputSpace.h" +#include "mozilla/dom/XRSpace.h" + +namespace mozilla::dom { + +class XRInputSpace : public XRSpace { + public: + explicit XRInputSpace(nsIGlobalObject* aParent, XRSession* aSession, + XRNativeOrigin* aNativeOrigin, + int32_t aControllerIndex); + + virtual bool IsPositionEmulated() const override; + + protected: + virtual ~XRInputSpace() = default; + + private: + int32_t mIndex; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRInputSpace_h_ diff --git a/dom/vr/XRNativeOrigin.h b/dom/vr/XRNativeOrigin.h new file mode 100644 index 0000000000..034b20551c --- /dev/null +++ b/dom/vr/XRNativeOrigin.h @@ -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/. */ + +#ifndef mozilla_dom_XRNativeOrigin_h_ +#define mozilla_dom_XRNativeOrigin_h_ + +#include "gfxVR.h" +#include "mozilla/RefPtr.h" + +namespace mozilla::dom { + +class XRNativeOrigin { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + XRNativeOrigin() = default; + + virtual gfx::PointDouble3D GetPosition() = 0; + virtual gfx::QuaternionDouble GetOrientation() { + static const gfx::QuaternionDouble orientation; + return orientation; + } + + protected: + virtual ~XRNativeOrigin() = default; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRNativeOrigin_h_ diff --git a/dom/vr/XRNativeOriginFixed.cpp b/dom/vr/XRNativeOriginFixed.cpp new file mode 100644 index 0000000000..42de321fd1 --- /dev/null +++ b/dom/vr/XRNativeOriginFixed.cpp @@ -0,0 +1,16 @@ +/* -*- 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 "XRNativeOriginFixed.h" + +namespace mozilla::dom { + +XRNativeOriginFixed::XRNativeOriginFixed(const gfx::PointDouble3D& aPosition) + : mPosition(aPosition) {} + +gfx::PointDouble3D XRNativeOriginFixed::GetPosition() { return mPosition; } + +} // namespace mozilla::dom diff --git a/dom/vr/XRNativeOriginFixed.h b/dom/vr/XRNativeOriginFixed.h new file mode 100644 index 0000000000..c1c2317a07 --- /dev/null +++ b/dom/vr/XRNativeOriginFixed.h @@ -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/. */ + +#ifndef mozilla_dom_XRNativeOriginFixed_h_ +#define mozilla_dom_XRNativeOriginFixed_h_ + +#include "gfxVR.h" +#include "XRNativeOrigin.h" + +namespace mozilla::dom { + +class XRNativeOriginFixed : public XRNativeOrigin { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XRNativeOriginFixed, override) + explicit XRNativeOriginFixed(const gfx::PointDouble3D& aPosition); + + gfx::PointDouble3D GetPosition() override; + + private: + ~XRNativeOriginFixed() = default; + + gfx::PointDouble3D mPosition; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRNativeOriginFixed_h_ diff --git a/dom/vr/XRNativeOriginLocal.cpp b/dom/vr/XRNativeOriginLocal.cpp new file mode 100644 index 0000000000..fd4590cf9e --- /dev/null +++ b/dom/vr/XRNativeOriginLocal.cpp @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XRNativeOriginLocal.h" +#include "VRDisplayClient.h" + +namespace mozilla::dom { + +XRNativeOriginLocal::XRNativeOriginLocal(gfx::VRDisplayClient* aDisplay) + : mDisplay(aDisplay), mInitialPositionValid(false) { + MOZ_ASSERT(aDisplay); +} + +gfx::PointDouble3D XRNativeOriginLocal::GetPosition() { + // Keep returning {0,0,0} until a position can be found + if (!mInitialPositionValid) { + const gfx::VRHMDSensorState& sensorState = mDisplay->GetSensorState(); + gfx::PointDouble3D origin; + if (sensorState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position || + sensorState.flags & + gfx::VRDisplayCapabilityFlags::Cap_PositionEmulated) { + mInitialPosition.x = sensorState.pose.position[0]; + mInitialPosition.y = sensorState.pose.position[1]; + mInitialPosition.z = sensorState.pose.position[2]; + mInitialPositionValid = true; + } + } + return mInitialPosition; +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRNativeOriginLocal.h b/dom/vr/XRNativeOriginLocal.h new file mode 100644 index 0000000000..882f02c96d --- /dev/null +++ b/dom/vr/XRNativeOriginLocal.h @@ -0,0 +1,36 @@ +/* -*- 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_XRNativeOriginLocal_h_ +#define mozilla_dom_XRNativeOriginLocal_h_ + +#include "gfxVR.h" +#include "XRNativeOrigin.h" + +namespace mozilla { +namespace gfx { +class VRDisplayClient; +} +namespace dom { + +class XRNativeOriginLocal : public XRNativeOrigin { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XRNativeOriginLocal, override) + explicit XRNativeOriginLocal(gfx::VRDisplayClient* aDisplay); + + gfx::PointDouble3D GetPosition() override; + + private: + ~XRNativeOriginLocal() = default; + RefPtr<gfx::VRDisplayClient> mDisplay; + gfx::PointDouble3D mInitialPosition; + bool mInitialPositionValid; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XRNativeOriginLocal_h_ diff --git a/dom/vr/XRNativeOriginLocalFloor.cpp b/dom/vr/XRNativeOriginLocalFloor.cpp new file mode 100644 index 0000000000..2cd146d925 --- /dev/null +++ b/dom/vr/XRNativeOriginLocalFloor.cpp @@ -0,0 +1,41 @@ +/* -*- 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/StaticPrefs_dom.h" +#include "XRNativeOriginLocalFloor.h" +#include "VRDisplayClient.h" + +namespace mozilla::dom { + +XRNativeOriginLocalFloor::XRNativeOriginLocalFloor( + gfx::VRDisplayClient* aDisplay) + : mDisplay(aDisplay), mInitialPositionValid(false) { + MOZ_ASSERT(aDisplay); + + // To avoid fingerprinting, we offset the floor height. + // This should result in the floor being higher than the + // real floor in order to avoid breaking content that expects + // you to pick objects up off the floor. + const double kFloorFuzz = StaticPrefs::dom_vr_webxr_quantization(); // Meters + mFloorRandom = double(rand()) / double(RAND_MAX) * kFloorFuzz; +} + +gfx::PointDouble3D XRNativeOriginLocalFloor::GetPosition() { + // Keep returning {0,-fuzz,0} until a position can be found + const auto standing = + mDisplay->GetDisplayInfo().GetSittingToStandingTransform(); + if (!mInitialPositionValid || standing != mStandingTransform) { + const gfx::VRHMDSensorState& sensorState = mDisplay->GetSensorState(); + mInitialPosition.x = sensorState.pose.position[0]; + mInitialPosition.y = -mFloorRandom - standing._42; + mInitialPosition.z = sensorState.pose.position[2]; + mInitialPositionValid = true; + mStandingTransform = standing; + } + return mInitialPosition; +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRNativeOriginLocalFloor.h b/dom/vr/XRNativeOriginLocalFloor.h new file mode 100644 index 0000000000..77b9ce28f3 --- /dev/null +++ b/dom/vr/XRNativeOriginLocalFloor.h @@ -0,0 +1,38 @@ +/* -*- 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_XRNativeOriginLocalFloor_h_ +#define mozilla_dom_XRNativeOriginLocalFloor_h_ + +#include "gfxVR.h" +#include "XRNativeOrigin.h" + +namespace mozilla { +namespace gfx { +class VRDisplayClient; +} +namespace dom { + +class XRNativeOriginLocalFloor : public XRNativeOrigin { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XRNativeOriginLocalFloor, override) + explicit XRNativeOriginLocalFloor(gfx::VRDisplayClient* aDisplay); + + gfx::PointDouble3D GetPosition() override; + + private: + ~XRNativeOriginLocalFloor() = default; + RefPtr<gfx::VRDisplayClient> mDisplay; + gfx::PointDouble3D mInitialPosition; + gfx::Matrix4x4 mStandingTransform; + bool mInitialPositionValid; + double mFloorRandom; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XRNativeOriginLocalFloor_h_ diff --git a/dom/vr/XRNativeOriginTracker.cpp b/dom/vr/XRNativeOriginTracker.cpp new file mode 100644 index 0000000000..a86898a719 --- /dev/null +++ b/dom/vr/XRNativeOriginTracker.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 "XRNativeOriginTracker.h" + +namespace mozilla::dom { + +XRNativeOriginTracker::XRNativeOriginTracker(const gfx::VRPose* aPose) + : mPose(aPose) { + MOZ_ASSERT(aPose); +} + +gfx::PointDouble3D XRNativeOriginTracker::GetPosition() { + MOZ_ASSERT(mPose); + return gfx::PointDouble3D(mPose->position[0], mPose->position[1], + mPose->position[2]); +} + +gfx::QuaternionDouble XRNativeOriginTracker::GetOrientation() { + MOZ_ASSERT(mPose); + gfx::QuaternionDouble orientation( + mPose->orientation[0], mPose->orientation[1], mPose->orientation[2], + mPose->orientation[3]); + return orientation; +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRNativeOriginTracker.h b/dom/vr/XRNativeOriginTracker.h new file mode 100644 index 0000000000..fdc5bb8d08 --- /dev/null +++ b/dom/vr/XRNativeOriginTracker.h @@ -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/. */ + +#ifndef mozilla_dom_XRNativeOriginTracker_h_ +#define mozilla_dom_XRNativeOriginTracker_h_ + +#include "gfxVR.h" +#include "XRNativeOrigin.h" + +namespace mozilla::dom { + +class XRNativeOriginTracker : public XRNativeOrigin { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XRNativeOriginTracker, override) + explicit XRNativeOriginTracker(const gfx::VRPose* aPose); + + gfx::PointDouble3D GetPosition() override; + gfx::QuaternionDouble GetOrientation() override; + + private: + ~XRNativeOriginTracker() = default; + const gfx::VRPose* mPose; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRNativeOriginTracker_h_ diff --git a/dom/vr/XRNativeOriginViewer.cpp b/dom/vr/XRNativeOriginViewer.cpp new file mode 100644 index 0000000000..a385d2d6e2 --- /dev/null +++ b/dom/vr/XRNativeOriginViewer.cpp @@ -0,0 +1,31 @@ +/* -*- 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 "XRNativeOriginViewer.h" +#include "VRDisplayClient.h" + +namespace mozilla::dom { + +XRNativeOriginViewer::XRNativeOriginViewer(gfx::VRDisplayClient* aDisplay) + : mDisplay(aDisplay) { + MOZ_ASSERT(aDisplay); +} + +gfx::PointDouble3D XRNativeOriginViewer::GetPosition() { + const gfx::VRHMDSensorState& sensorState = mDisplay->GetSensorState(); + return gfx::PointDouble3D(sensorState.pose.position[0], + sensorState.pose.position[1], + sensorState.pose.position[2]); +} + +gfx::QuaternionDouble XRNativeOriginViewer::GetOrientation() { + const gfx::VRHMDSensorState& sensorState = mDisplay->GetSensorState(); + return gfx::QuaternionDouble( + sensorState.pose.orientation[0], sensorState.pose.orientation[1], + sensorState.pose.orientation[2], sensorState.pose.orientation[3]); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRNativeOriginViewer.h b/dom/vr/XRNativeOriginViewer.h new file mode 100644 index 0000000000..6178ae66ea --- /dev/null +++ b/dom/vr/XRNativeOriginViewer.h @@ -0,0 +1,35 @@ +/* -*- 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_XRNativeOriginViewer_h_ +#define mozilla_dom_XRNativeOriginViewer_h_ + +#include "gfxVR.h" +#include "XRNativeOrigin.h" + +namespace mozilla { +namespace gfx { +class VRDisplayClient; +} +namespace dom { + +class XRNativeOriginViewer : public XRNativeOrigin { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XRNativeOriginViewer, override) + explicit XRNativeOriginViewer(gfx::VRDisplayClient* aDisplay); + + gfx::PointDouble3D GetPosition() override; + gfx::QuaternionDouble GetOrientation() override; + + private: + ~XRNativeOriginViewer() = default; + RefPtr<gfx::VRDisplayClient> mDisplay; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XRNativeOriginViewer_h_ diff --git a/dom/vr/XRPermissionRequest.cpp b/dom/vr/XRPermissionRequest.cpp new file mode 100644 index 0000000000..2661f246cd --- /dev/null +++ b/dom/vr/XRPermissionRequest.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "XRPermissionRequest.h" +#include "nsGlobalWindowInner.h" +#include "mozilla/dom/Document.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" + +namespace mozilla::dom { + +//------------------------------------------------- +// XR Permission Requests +//------------------------------------------------- + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XRPermissionRequest, + ContentPermissionRequestBase) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(XRPermissionRequest, + ContentPermissionRequestBase) + +XRPermissionRequest::XRPermissionRequest(nsPIDOMWindowInner* aWindow, + uint64_t aWindowId) + : ContentPermissionRequestBase(aWindow->GetDoc()->NodePrincipal(), aWindow, + "dom.vr"_ns, "xr"_ns), + mWindowId(aWindowId) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->GetDoc()); + mPrincipal = aWindow->GetDoc()->NodePrincipal(); + MOZ_ASSERT(mPrincipal); +} + +NS_IMETHODIMP +XRPermissionRequest::Cancel() { + nsGlobalWindowInner* window = + nsGlobalWindowInner::GetInnerWindowWithId(mWindowId); + if (!window) { + return NS_OK; + } + window->OnXRPermissionRequestCancel(); + return NS_OK; +} + +NS_IMETHODIMP +XRPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) { + MOZ_ASSERT(aChoices.isUndefined()); + nsGlobalWindowInner* window = + nsGlobalWindowInner::GetInnerWindowWithId(mWindowId); + if (!window) { + return NS_OK; + } + window->OnXRPermissionRequestAllow(); + return NS_OK; +} + +nsresult XRPermissionRequest::Start() { + MOZ_ASSERT(NS_IsMainThread()); + if (!CheckPermissionDelegate()) { + return Cancel(); + } + PromptResult pr = CheckPromptPrefs(); + if (pr == PromptResult::Granted) { + return Allow(JS::UndefinedHandleValue); + } + if (pr == PromptResult::Denied) { + return Cancel(); + } + + return nsContentPermissionUtils::AskPermission(this, mWindow); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRPermissionRequest.h b/dom/vr/XRPermissionRequest.h new file mode 100644 index 0000000000..93a6c310b3 --- /dev/null +++ b/dom/vr/XRPermissionRequest.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_XRPermissionRequest_h_ +#define mozilla_dom_XRPermissionRequest_h_ + +#include "mozilla/dom/Promise.h" +#include "nsContentPermissionHelper.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +/** + * Handles permission dialog management when requesting XR device access. + */ +class XRPermissionRequest final : public ContentPermissionRequestBase { + public: + XRPermissionRequest(nsPIDOMWindowInner* aWindow, uint64_t aWindowId); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XRPermissionRequest, + ContentPermissionRequestBase) + // nsIContentPermissionRequest + NS_IMETHOD Cancel(void) override; + NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override; + nsresult Start(); + + private: + ~XRPermissionRequest() = default; + + uint64_t mWindowId; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XR_h_ diff --git a/dom/vr/XRPose.cpp b/dom/vr/XRPose.cpp new file mode 100644 index 0000000000..4516d7b8b8 --- /dev/null +++ b/dom/vr/XRPose.cpp @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/XRPose.h" +#include "mozilla/dom/XRRigidTransform.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(XRPose, mParent, mTransform) +NS_IMPL_CYCLE_COLLECTING_ADDREF(XRPose) +NS_IMPL_CYCLE_COLLECTING_RELEASE(XRPose) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XRPose) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +XRPose::XRPose(nsISupports* aParent, XRRigidTransform* aTransform, + bool aEmulatedPosition) + : mParent(aParent), + mTransform(aTransform), + mEmulatedPosition(aEmulatedPosition) {} + +JSObject* XRPose::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRPose_Binding::Wrap(aCx, this, aGivenProto); +} + +void XRPose::SetEmulatedPosition(bool aEmulated) { + mEmulatedPosition = aEmulated; +} + +XRRigidTransform* XRPose::Transform() { return mTransform; } + +bool XRPose::EmulatedPosition() const { return mEmulatedPosition; } + +} // namespace mozilla::dom diff --git a/dom/vr/XRPose.h b/dom/vr/XRPose.h new file mode 100644 index 0000000000..560581a257 --- /dev/null +++ b/dom/vr/XRPose.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XRPose_h_ +#define mozilla_dom_XRPose_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" +#include "mozilla/dom/XRRigidTransform.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +class XRRigidTransform; +class XRView; + +class XRPose : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(XRPose) + + explicit XRPose(nsISupports* aParent, XRRigidTransform* aTransform, + bool aEmulatedPosition); + void SetEmulatedPosition(bool aEmulated); + + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + XRRigidTransform* Transform(); + bool EmulatedPosition() const; + + protected: + virtual ~XRPose() = default; + nsCOMPtr<nsISupports> mParent; + RefPtr<XRRigidTransform> mTransform; + bool mEmulatedPosition; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRPose_h_ diff --git a/dom/vr/XRReferenceSpace.cpp b/dom/vr/XRReferenceSpace.cpp new file mode 100644 index 0000000000..9ed73003cd --- /dev/null +++ b/dom/vr/XRReferenceSpace.cpp @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/XRReferenceSpace.h" +#include "mozilla/dom/XRRigidTransform.h" +#include "VRDisplayClient.h" + +namespace mozilla::dom { + +XRReferenceSpace::XRReferenceSpace(nsIGlobalObject* aParent, + XRSession* aSession, + XRNativeOrigin* aNativeOrigin, + XRReferenceSpaceType aType) + : XRSpace(aParent, aSession, aNativeOrigin), mType(aType) {} + +already_AddRefed<XRReferenceSpace> XRReferenceSpace::GetOffsetReferenceSpace( + const XRRigidTransform& aOriginOffset) { + RefPtr<XRReferenceSpace> offsetReferenceSpace = + new XRReferenceSpace(GetParentObject(), mSession, mNativeOrigin, mType); + + // https://immersive-web.github.io/webxr/#multiply-transforms + // An XRRigidTransform is essentially a rotation followed by a translation + gfx::QuaternionDouble otherOrientation = aOriginOffset.RawOrientation(); + // The resulting rotation is the two combined + offsetReferenceSpace->mOriginOffsetOrientation = + mOriginOffsetOrientation * otherOrientation; + // We first apply the rotation of aOriginOffset to + // mOriginOffsetPosition offset, then translate by the offset of + // aOriginOffset + offsetReferenceSpace->mOriginOffsetPosition = + otherOrientation.RotatePoint(mOriginOffsetPosition) + + aOriginOffset.RawPosition(); + + return offsetReferenceSpace.forget(); +} + +JSObject* XRReferenceSpace::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRReferenceSpace_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRReferenceSpace.h b/dom/vr/XRReferenceSpace.h new file mode 100644 index 0000000000..7a6f9a54bb --- /dev/null +++ b/dom/vr/XRReferenceSpace.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XRReferenceSpace_h_ +#define mozilla_dom_XRReferenceSpace_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" +#include "mozilla/dom/XRSpace.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +enum class XRReferenceSpaceType : uint8_t; +class XRRigidTransform; +class XRSession; + +class XRReferenceSpace : public XRSpace { + public: + explicit XRReferenceSpace(nsIGlobalObject* aParent, XRSession* aSession, + XRNativeOrigin* aNativeOrigin, + XRReferenceSpaceType aType); + + // WebIDL Boilerplate + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + virtual already_AddRefed<XRReferenceSpace> GetOffsetReferenceSpace( + const XRRigidTransform& aOriginOffset); + + // TODO (Bug 1611309): Implement XRReferenceSpace reset events + // https://immersive-web.github.io/webxr/#eventdef-xrreferencespace-reset + IMPL_EVENT_HANDLER(reset); + + protected: + virtual ~XRReferenceSpace() = default; + XRReferenceSpaceType mType; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRReferenceSpace_h_ diff --git a/dom/vr/XRRenderState.cpp b/dom/vr/XRRenderState.cpp new file mode 100644 index 0000000000..ef5c4ba0e7 --- /dev/null +++ b/dom/vr/XRRenderState.cpp @@ -0,0 +1,90 @@ +/* -*- 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/XRRenderState.h" +#include "VRLayerChild.h" +#include "nsIObserverService.h" +#include "nsISupportsPrimitives.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(XRRenderState, mParent, mSession, + mBaseLayer, mOutputCanvas) + +XRRenderState::XRRenderState(nsISupports* aParent, XRSession* aSession) + : mParent(aParent), + mSession(aSession), + mDepthNear(0.1f), + mDepthFar(1000.0f), + mCompositionDisabled(false) { + if (!mSession->IsImmersive()) { + mInlineVerticalFieldOfView.SetValue(M_PI * 0.5f); + } +} + +XRRenderState::XRRenderState(const XRRenderState& aOther) + : mParent(aOther.mParent), + mSession(aOther.mSession), + mBaseLayer(aOther.mBaseLayer), + mDepthNear(aOther.mDepthNear), + mDepthFar(aOther.mDepthFar), + mInlineVerticalFieldOfView(aOther.mInlineVerticalFieldOfView), + mOutputCanvas(aOther.mOutputCanvas), + mCompositionDisabled(aOther.mCompositionDisabled) {} + +JSObject* XRRenderState::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRRenderState_Binding::Wrap(aCx, this, aGivenProto); +} + +double XRRenderState::DepthNear() { return mDepthNear; } + +double XRRenderState::DepthFar() { return mDepthFar; } + +Nullable<double> XRRenderState::GetInlineVerticalFieldOfView() { + return mInlineVerticalFieldOfView; +} + +void XRRenderState::SetDepthNear(double aDepthNear) { mDepthNear = aDepthNear; } + +void XRRenderState::SetDepthFar(double aDepthFar) { mDepthFar = aDepthFar; } + +void XRRenderState::SetInlineVerticalFieldOfView( + double aInlineVerticalFieldOfView) { + mInlineVerticalFieldOfView.SetValue(aInlineVerticalFieldOfView); +} + +XRWebGLLayer* XRRenderState::GetBaseLayer() { return mBaseLayer; } + +void XRRenderState::SetBaseLayer(XRWebGLLayer* aBaseLayer) { + mBaseLayer = aBaseLayer; +} + +void XRRenderState::SetOutputCanvas(HTMLCanvasElement* aCanvas) { + mOutputCanvas = aCanvas; +} + +HTMLCanvasElement* XRRenderState::GetOutputCanvas() const { + return mOutputCanvas; +} + +void XRRenderState::SetCompositionDisabled(bool aCompositionDisabled) { + mCompositionDisabled = aCompositionDisabled; +} + +bool XRRenderState::IsCompositionDisabled() const { + return mCompositionDisabled; +} + +void XRRenderState::SessionEnded() { + if (mBaseLayer) { + mBaseLayer->SessionEnded(); + mBaseLayer = nullptr; + } + mOutputCanvas = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRRenderState.h b/dom/vr/XRRenderState.h new file mode 100644 index 0000000000..6004949500 --- /dev/null +++ b/dom/vr/XRRenderState.h @@ -0,0 +1,65 @@ +/* -*- 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_XRRenderState_h_ +#define mozilla_dom_XRRenderState_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" + +#include "gfxVR.h" + +namespace mozilla::dom { +class XRWebGLLayer; + +class XRRenderState final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(XRRenderState) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(XRRenderState) + + explicit XRRenderState(nsISupports* aParent, XRSession* aSession); + explicit XRRenderState(const XRRenderState& aOther); + + void SetDepthNear(double aDepthNear); + void SetDepthFar(double aDepthFar); + void SetInlineVerticalFieldOfView(double aInlineVerticalFieldOfView); + void SetBaseLayer(XRWebGLLayer* aBaseLayer); + + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + double DepthNear(); + double DepthFar(); + Nullable<double> GetInlineVerticalFieldOfView(); + XRWebGLLayer* GetBaseLayer(); + + // Non-WebIDL Members + void SetOutputCanvas(HTMLCanvasElement* aCanvas); + HTMLCanvasElement* GetOutputCanvas() const; + void SetCompositionDisabled(bool aCompositionDisabled); + bool IsCompositionDisabled() const; + void SessionEnded(); + + protected: + virtual ~XRRenderState() = default; + nsCOMPtr<nsISupports> mParent; + RefPtr<XRSession> mSession; + RefPtr<XRWebGLLayer> mBaseLayer; + double mDepthNear; + double mDepthFar; + Nullable<double> mInlineVerticalFieldOfView; + // https://immersive-web.github.io/webxr/#xrrenderstate-output-canvas + RefPtr<HTMLCanvasElement> mOutputCanvas; + // https://immersive-web.github.io/webxr/#xrrenderstate-composition-disabled + bool mCompositionDisabled; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRRenderState_h_ diff --git a/dom/vr/XRRigidTransform.cpp b/dom/vr/XRRigidTransform.cpp new file mode 100644 index 0000000000..d29937c56a --- /dev/null +++ b/dom/vr/XRRigidTransform.cpp @@ -0,0 +1,171 @@ +/* -*- 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/XRRigidTransform.h" +#include "mozilla/dom/DOMPoint.h" +#include "mozilla/dom/Pose.h" +#include "mozilla/dom/DOMPointBinding.h" +#include "mozilla/HoldDropJSObjects.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS(XRRigidTransform, + (mParent, mPosition, + mOrientation, mInverse), + (mMatrixArray)) + +XRRigidTransform::XRRigidTransform(nsISupports* aParent, + const gfx::PointDouble3D& aPosition, + const gfx::QuaternionDouble& aOrientation) + : mParent(aParent), + mMatrixArray(nullptr), + mPosition(nullptr), + mOrientation(nullptr), + mInverse(nullptr), + mRawPosition(aPosition), + mRawOrientation(aOrientation), + mNeedsUpdate(true) { + mozilla::HoldJSObjects(this); + mRawTransformMatrix.SetRotationFromQuaternion(mRawOrientation); + mRawTransformMatrix.PostTranslate(mRawPosition); +} + +XRRigidTransform::XRRigidTransform(nsISupports* aParent, + const gfx::Matrix4x4Double& aTransform) + : mParent(aParent), + mMatrixArray(nullptr), + mPosition(nullptr), + mOrientation(nullptr), + mInverse(nullptr), + mNeedsUpdate(true) { + mozilla::HoldJSObjects(this); + gfx::PointDouble3D scale; + mRawTransformMatrix = aTransform; + mRawTransformMatrix.Decompose(mRawPosition, mRawOrientation, scale); +} + +XRRigidTransform::~XRRigidTransform() { mozilla::DropJSObjects(this); } + +/* static */ already_AddRefed<XRRigidTransform> XRRigidTransform::Constructor( + const GlobalObject& aGlobal, const DOMPointInit& aOrigin, + const DOMPointInit& aDirection, ErrorResult& aRv) { + gfx::PointDouble3D position(aOrigin.mX, aOrigin.mY, aOrigin.mZ); + gfx::QuaternionDouble orientation(aDirection.mX, aDirection.mY, aDirection.mZ, + aDirection.mW); + orientation.Normalize(); + RefPtr<XRRigidTransform> obj = + new XRRigidTransform(aGlobal.GetAsSupports(), position, orientation); + return obj.forget(); +} + +JSObject* XRRigidTransform::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRRigidTransform_Binding::Wrap(aCx, this, aGivenProto); +} + +DOMPoint* XRRigidTransform::Position() { + if (!mPosition) { + mPosition = new DOMPoint(mParent, mRawPosition.x, mRawPosition.y, + mRawPosition.z, 1.0f); + } + return mPosition; +} + +DOMPoint* XRRigidTransform::Orientation() { + if (!mOrientation) { + mOrientation = new DOMPoint(mParent, mRawOrientation.x, mRawOrientation.y, + mRawOrientation.z, mRawOrientation.w); + } + return mOrientation; +} + +XRRigidTransform& XRRigidTransform::operator=(const XRRigidTransform& aOther) { + Update(aOther.mRawPosition, aOther.mRawOrientation); + return *this; +} + +gfx::QuaternionDouble XRRigidTransform::RawOrientation() const { + return mRawOrientation; +} +gfx::PointDouble3D XRRigidTransform::RawPosition() const { + return mRawPosition; +} + +void XRRigidTransform::Update(const gfx::PointDouble3D& aPosition, + const gfx::QuaternionDouble& aOrientation) { + mNeedsUpdate = true; + mRawPosition = aPosition; + mRawOrientation = aOrientation; + mRawTransformMatrix.SetRotationFromQuaternion(mRawOrientation); + mRawTransformMatrix.PostTranslate(mRawPosition); + UpdateInternal(); +} + +void XRRigidTransform::Update(const gfx::Matrix4x4Double& aTransform) { + mNeedsUpdate = true; + mRawTransformMatrix = aTransform; + gfx::PointDouble3D scale; + mRawTransformMatrix.Decompose(mRawPosition, mRawOrientation, scale); + UpdateInternal(); +} + +void XRRigidTransform::UpdateInternal() { + if (mPosition) { + mPosition->SetX(mRawPosition.x); + mPosition->SetY(mRawPosition.y); + mPosition->SetZ(mRawPosition.z); + } + if (mOrientation) { + mOrientation->SetX(mRawOrientation.x); + mOrientation->SetY(mRawOrientation.y); + mOrientation->SetZ(mRawOrientation.z); + mOrientation->SetW(mRawOrientation.w); + } + if (mInverse) { + gfx::Matrix4x4Double inverseMatrix = mRawTransformMatrix; + Unused << inverseMatrix.Invert(); + mInverse->Update(inverseMatrix); + } +} + +void XRRigidTransform::GetMatrix(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + if (!mMatrixArray || mNeedsUpdate) { + mNeedsUpdate = false; + + const uint32_t size = 16; + float components[size] = {}; + // In order to avoid some platforms which only copy + // the first or last two bytes of a Float64 to a Float32. + for (uint32_t i = 0; i < size; ++i) { + components[i] = mRawTransformMatrix.components[i]; + } + Pose::SetFloat32Array(aCx, this, aRetval, mMatrixArray, components, 16, + aRv); + if (!mMatrixArray) { + return; + } + } + if (mMatrixArray) { + JS::ExposeObjectToActiveJS(mMatrixArray); + } + aRetval.set(mMatrixArray); +} + +already_AddRefed<XRRigidTransform> XRRigidTransform::Inverse() { + if (!mInverse) { + gfx::Matrix4x4Double inverseMatrix = mRawTransformMatrix; + Unused << inverseMatrix.Invert(); + mInverse = new XRRigidTransform(mParent, inverseMatrix); + } + + RefPtr<XRRigidTransform> inverse = mInverse; + return inverse.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRRigidTransform.h b/dom/vr/XRRigidTransform.h new file mode 100644 index 0000000000..defeebe13d --- /dev/null +++ b/dom/vr/XRRigidTransform.h @@ -0,0 +1,67 @@ +/* -*- 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_XRRigidTransform_h_ +#define mozilla_dom_XRRigidTransform_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +class VRFrameData; + +class XRRigidTransform final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(XRRigidTransform) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(XRRigidTransform) + + explicit XRRigidTransform(nsISupports* aParent, + const gfx::PointDouble3D& aPosition, + const gfx::QuaternionDouble& aOrientation); + explicit XRRigidTransform(nsISupports* aParent, + const gfx::Matrix4x4Double& aTransform); + static already_AddRefed<XRRigidTransform> Constructor( + const GlobalObject& aGlobal, const DOMPointInit& aOrigin, + const DOMPointInit& aDirection, ErrorResult& aRv); + XRRigidTransform& operator=(const XRRigidTransform& aOther); + gfx::QuaternionDouble RawOrientation() const; + gfx::PointDouble3D RawPosition() const; + void Update(const gfx::PointDouble3D& aPosition, + const gfx::QuaternionDouble& aOrientation); + void Update(const gfx::Matrix4x4Double& aTransform); + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + DOMPoint* Position(); + DOMPoint* Orientation(); + void GetMatrix(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + already_AddRefed<XRRigidTransform> Inverse(); + + protected: + void UpdateInternal(); + virtual ~XRRigidTransform(); + + nsCOMPtr<nsISupports> mParent; + JS::Heap<JSObject*> mMatrixArray; + RefPtr<DOMPoint> mPosition; + RefPtr<DOMPoint> mOrientation; + RefPtr<XRRigidTransform> mInverse; + gfx::Matrix4x4Double mRawTransformMatrix; + gfx::PointDouble3D mRawPosition; + gfx::QuaternionDouble mRawOrientation; + bool mNeedsUpdate; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRRigidTransform_h_ diff --git a/dom/vr/XRSession.cpp b/dom/vr/XRSession.cpp new file mode 100644 index 0000000000..a3b659634a --- /dev/null +++ b/dom/vr/XRSession.cpp @@ -0,0 +1,563 @@ +/* -*- 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/XRSession.h" + +#include "mozilla/dom/XRSessionEvent.h" +#include "mozilla/dom/XRInputSourceEvent.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Promise.h" +#include "XRSystem.h" +#include "XRRenderState.h" +#include "XRBoundedReferenceSpace.h" +#include "XRFrame.h" +#include "XRNativeOrigin.h" +#include "XRNativeOriginFixed.h" +#include "XRNativeOriginViewer.h" +#include "XRNativeOriginLocal.h" +#include "XRNativeOriginLocalFloor.h" +#include "XRView.h" +#include "XRViewerPose.h" +#include "VRLayerChild.h" +#include "XRInputSourceArray.h" +#include "nsGlobalWindow.h" +#include "nsIObserverService.h" +#include "nsISupportsPrimitives.h" +#include "nsRefreshDriver.h" +#include "VRDisplayClient.h" +#include "VRDisplayPresentation.h" + +/** + * Maximum instances of XRFrame and XRViewerPose objects + * created in the pool. + */ +const uint32_t kMaxPoolSize = 16; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(XRSession) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XRSession, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mXRSystem) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActiveRenderState) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingRenderState) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputSources) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mViewerPosePool) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFramePool) + + for (uint32_t i = 0; i < tmp->mFrameRequestCallbacks.Length(); ++i) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mFrameRequestCallbacks[i]"); + cb.NoteXPCOMChild(tmp->mFrameRequestCallbacks[i].mCallback); + } + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XRSession, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mXRSystem) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mActiveRenderState) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingRenderState) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputSources) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mViewerPosePool) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFramePool) + + tmp->mFrameRequestCallbacks.Clear(); + + // Don't need NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER because + // DOMEventTargetHelper does it for us. +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XRSession, DOMEventTargetHelper) + +already_AddRefed<XRSession> XRSession::CreateInlineSession( + nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, + const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) { + nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(aWindow); + if (!win) { + return nullptr; + } + Document* doc = aWindow->GetExtantDoc(); + if (!doc) { + return nullptr; + } + nsPresContext* context = doc->GetPresContext(); + if (!context) { + return nullptr; + } + nsRefreshDriver* driver = context->RefreshDriver(); + if (!driver) { + return nullptr; + } + + RefPtr<XRSession> session = + new XRSession(aWindow, aXRSystem, driver, nullptr, gfx::kVRGroupContent, + aEnabledReferenceSpaceTypes); + driver->AddRefreshObserver(session, FlushType::Display, "XR Session"); + return session.forget(); +} + +already_AddRefed<XRSession> XRSession::CreateImmersiveSession( + nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, + gfx::VRDisplayClient* aClient, uint32_t aPresentationGroup, + const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) { + RefPtr<XRSession> session = + new XRSession(aWindow, aXRSystem, nullptr, aClient, aPresentationGroup, + aEnabledReferenceSpaceTypes); + return session.forget(); +} + +XRSession::XRSession( + nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, + nsRefreshDriver* aRefreshDriver, gfx::VRDisplayClient* aClient, + uint32_t aPresentationGroup, + const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) + : DOMEventTargetHelper(aWindow), + mXRSystem(aXRSystem), + mShutdown(false), + mEnded(false), + mRefreshDriver(aRefreshDriver), + mDisplayClient(aClient), + mFrameRequestCallbackCounter(0), + mEnabledReferenceSpaceTypes(aEnabledReferenceSpaceTypes.Clone()), + mViewerPosePoolIndex(0), + mFramePoolIndex(0) { + if (aClient) { + aClient->SessionStarted(this); + } + mActiveRenderState = new XRRenderState(aWindow, this); + mStartTimeStamp = TimeStamp::Now(); + if (IsImmersive()) { + mDisplayPresentation = + mDisplayClient->BeginPresentation({}, aPresentationGroup); + } + if (mDisplayClient) { + mDisplayClient->SetXRAPIMode(gfx::VRAPIMode::WebXR); + } + // TODO: Handle XR input sources are no longer available cases. + // https://immersive-web.github.io/webxr/#dom-xrsession-inputsources + mInputSources = new XRInputSourceArray(aWindow); +} + +XRSession::~XRSession() { MOZ_ASSERT(mShutdown); } + +gfx::VRDisplayClient* XRSession::GetDisplayClient() const { + return mDisplayClient; +} + +JSObject* XRSession::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRSession_Binding::Wrap(aCx, this, aGivenProto); +} + +bool XRSession::IsEnded() const { return mEnded; } + +already_AddRefed<Promise> XRSession::End(ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetParentObject(); + NS_ENSURE_TRUE(global, nullptr); + + ExitPresentInternal(); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + NS_ENSURE_TRUE(!aRv.Failed(), nullptr); + + promise->MaybeResolve(JS::UndefinedHandleValue); + + return promise.forget(); +} + +bool XRSession::IsImmersive() const { + // Only immersive sessions have a VRDisplayClient + return mDisplayClient != nullptr; +} + +XRVisibilityState XRSession::VisibilityState() const { + return XRVisibilityState::Visible; + // TODO (Bug 1609771): Implement changing visibility state +} + +// https://immersive-web.github.io/webxr/#poses-may-be-reported +// Given that an XRSession cannot be requested without explicit consent +// by the user, the only necessary check is whether the XRSession's +// visiblityState is 'visible'. +bool XRSession::CanReportPoses() const { + return VisibilityState() == XRVisibilityState::Visible; +} + +// https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate +void XRSession::UpdateRenderState(const XRRenderStateInit& aNewState, + ErrorResult& aRv) { + if (mEnded) { + aRv.ThrowInvalidStateError( + "UpdateRenderState can not be called on an XRSession that has ended."); + return; + } + if (aNewState.mBaseLayer.WasPassed() && + aNewState.mBaseLayer.Value()->mSession != this) { + aRv.ThrowInvalidStateError( + "The baseLayer passed in to UpdateRenderState must " + "belong to the XRSession that UpdateRenderState is " + "being called on."); + return; + } + if (aNewState.mInlineVerticalFieldOfView.WasPassed() && IsImmersive()) { + aRv.ThrowInvalidStateError( + "The inlineVerticalFieldOfView can not be set on an " + "XRRenderState for an immersive XRSession."); + return; + } + if (mPendingRenderState == nullptr) { + mPendingRenderState = new XRRenderState(*mActiveRenderState); + } + if (aNewState.mDepthNear.WasPassed()) { + mPendingRenderState->SetDepthNear(aNewState.mDepthNear.Value()); + } + if (aNewState.mDepthFar.WasPassed()) { + mPendingRenderState->SetDepthFar(aNewState.mDepthFar.Value()); + } + if (aNewState.mInlineVerticalFieldOfView.WasPassed()) { + mPendingRenderState->SetInlineVerticalFieldOfView( + aNewState.mInlineVerticalFieldOfView.Value()); + } + if (aNewState.mBaseLayer.WasPassed()) { + mPendingRenderState->SetBaseLayer(aNewState.mBaseLayer.Value()); + } +} + +XRRenderState* XRSession::RenderState() { return mActiveRenderState; } + +XRInputSourceArray* XRSession::InputSources() { return mInputSources; } + +Nullable<float> XRSession::GetFrameRate() { return {}; } + +void XRSession::GetSupportedFrameRates(JSContext*, + JS::MutableHandle<JSObject*> aRetVal) { + aRetVal.set(nullptr); +} + +// https://immersive-web.github.io/webxr/#apply-the-pending-render-state +void XRSession::ApplyPendingRenderState() { + if (mPendingRenderState == nullptr) { + return; + } + mActiveRenderState = mPendingRenderState; + mPendingRenderState = nullptr; + + // https://immersive-web.github.io/webxr/#minimum-inline-field-of-view + const double kMinimumInlineVerticalFieldOfView = 0.0f; + + // https://immersive-web.github.io/webxr/#maximum-inline-field-of-view + const double kMaximumInlineVerticalFieldOfView = M_PI; + + if (!mActiveRenderState->GetInlineVerticalFieldOfView().IsNull()) { + double verticalFOV = + mActiveRenderState->GetInlineVerticalFieldOfView().Value(); + if (verticalFOV < kMinimumInlineVerticalFieldOfView) { + verticalFOV = kMinimumInlineVerticalFieldOfView; + } + if (verticalFOV > kMaximumInlineVerticalFieldOfView) { + verticalFOV = kMaximumInlineVerticalFieldOfView; + } + mActiveRenderState->SetInlineVerticalFieldOfView(verticalFOV); + } + + // Our minimum near plane value is set to a small value close but not equal to + // zero (kEpsilon) The maximum far plane is infinite. + const float kEpsilon = 0.00001f; + double depthNear = mActiveRenderState->DepthNear(); + double depthFar = mActiveRenderState->DepthFar(); + if (depthNear < 0.0f) { + depthNear = 0.0f; + } + if (depthFar < 0.0f) { + depthFar = 0.0f; + } + // Ensure at least a small distance between the near and far planes + if (fabs(depthFar - depthNear) < kEpsilon) { + depthFar = depthNear + kEpsilon; + } + mActiveRenderState->SetDepthNear(depthNear); + mActiveRenderState->SetDepthFar(depthFar); + + XRWebGLLayer* baseLayer = mActiveRenderState->GetBaseLayer(); + if (baseLayer) { + if (!IsImmersive() && baseLayer->mCompositionDisabled) { + mActiveRenderState->SetCompositionDisabled(true); + mActiveRenderState->SetOutputCanvas(baseLayer->GetCanvas()); + } else { + mActiveRenderState->SetCompositionDisabled(false); + mActiveRenderState->SetOutputCanvas(nullptr); + mDisplayPresentation->UpdateXRWebGLLayer(baseLayer); + } + } // if (baseLayer) +} + +void XRSession::WillRefresh(mozilla::TimeStamp aTime) { + // Inline sessions are driven by nsRefreshDriver directly, + // unlike immersive sessions, which are driven VRDisplayClient. + if (!IsImmersive() && !mXRSystem->HasActiveImmersiveSession()) { + nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(GetOwner()); + if (win) { + if (JSObject* obj = win->AsGlobal()->GetGlobalJSObject()) { + js::NotifyAnimationActivity(obj); + } + } + StartFrame(); + } +} + +void XRSession::StartFrame() { + if (mShutdown || mEnded) { + return; + } + ApplyPendingRenderState(); + + XRWebGLLayer* baseLayer = mActiveRenderState->GetBaseLayer(); + if (!baseLayer) { + return; + } + + if (!IsImmersive() && mActiveRenderState->GetOutputCanvas() == nullptr) { + return; + } + + // Determine timestamp for the callbacks + TimeStamp nowTime = TimeStamp::Now(); + mozilla::TimeDuration duration = nowTime - mStartTimeStamp; + DOMHighResTimeStamp timeStamp = duration.ToMilliseconds(); + + // Create an XRFrame for the callbacks + RefPtr<XRFrame> frame = PooledFrame(); + frame->StartAnimationFrame(); + + baseLayer->StartAnimationFrame(); + nsTArray<XRFrameRequest> callbacks; + callbacks.AppendElements(mFrameRequestCallbacks); + mFrameRequestCallbacks.Clear(); + for (auto& callback : callbacks) { + callback.Call(timeStamp, *frame); + } + + baseLayer->EndAnimationFrame(); + frame->EndAnimationFrame(); + if (mDisplayPresentation) { + mDisplayPresentation->SubmitFrame(); + } +} + +void XRSession::ExitPresent() { ExitPresentInternal(); } + +already_AddRefed<Promise> XRSession::RequestReferenceSpace( + const XRReferenceSpaceType& aReferenceSpaceType, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetParentObject(); + NS_ENSURE_TRUE(global, nullptr); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + NS_ENSURE_TRUE(!aRv.Failed(), nullptr); + + if (!mEnabledReferenceSpaceTypes.Contains(aReferenceSpaceType)) { + promise->MaybeRejectWithNotSupportedError(nsLiteralCString( + "Requested XRReferenceSpaceType not available for the XRSession.")); + return promise.forget(); + } + RefPtr<XRReferenceSpace> space; + RefPtr<XRNativeOrigin> nativeOrigin; + if (mDisplayClient) { + switch (aReferenceSpaceType) { + case XRReferenceSpaceType::Viewer: + nativeOrigin = new XRNativeOriginViewer(mDisplayClient); + break; + case XRReferenceSpaceType::Local: + nativeOrigin = new XRNativeOriginLocal(mDisplayClient); + break; + case XRReferenceSpaceType::Local_floor: + case XRReferenceSpaceType::Bounded_floor: + nativeOrigin = new XRNativeOriginLocalFloor(mDisplayClient); + break; + default: + nativeOrigin = new XRNativeOriginFixed(gfx::PointDouble3D()); + break; + } + } else { + // We currently only support XRReferenceSpaceType::Viewer when + // there is no XR hardware. In this case, the native origin + // will always be at {0, 0, 0} which will always be the same + // as the 'tracked' position of the non-existant pose. + MOZ_ASSERT(aReferenceSpaceType == XRReferenceSpaceType::Viewer); + nativeOrigin = new XRNativeOriginFixed(gfx::PointDouble3D()); + } + if (aReferenceSpaceType == XRReferenceSpaceType::Bounded_floor) { + space = new XRBoundedReferenceSpace(GetParentObject(), this, nativeOrigin); + } else { + space = new XRReferenceSpace(GetParentObject(), this, nativeOrigin, + aReferenceSpaceType); + } + + promise->MaybeResolve(space); + return promise.forget(); +} + +already_AddRefed<Promise> XRSession::UpdateTargetFrameRate(float aRate, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetParentObject(); + NS_ENSURE_TRUE(global, nullptr); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + NS_ENSURE_TRUE(!aRv.Failed(), nullptr); + + if (mEnded) { + promise->MaybeRejectWithInvalidStateError( + "UpdateTargetFrameRate can not be called on an XRSession that has " + "ended."); + return promise.forget(); + } + + // https://immersive-web.github.io/webxr/#dom-xrsession-updatetargetframerate + // TODO: Validate the rate with the frame rates supported from the device. + // We add a no op for now to avoid JS exceptions related to undefined method. + // The spec states that user agent MAY use rate to calculate a new display + // frame rate, so it's fine to let the default frame rate for now. + + promise->MaybeResolve(JS::UndefinedHandleValue); + return promise.forget(); +} + +XRRenderState* XRSession::GetActiveRenderState() const { + return mActiveRenderState; +} + +void XRSession::XRFrameRequest::Call(const DOMHighResTimeStamp& aTimeStamp, + XRFrame& aFrame) { + RefPtr<mozilla::dom::XRFrameRequestCallback> callback = mCallback; + callback->Call(aTimeStamp, aFrame); +} + +int32_t XRSession::RequestAnimationFrame(XRFrameRequestCallback& aCallback, + ErrorResult& aError) { + if (mShutdown) { + return 0; + } + + int32_t handle = ++mFrameRequestCallbackCounter; + + mFrameRequestCallbacks.AppendElement(XRFrameRequest(aCallback, handle)); + + return handle; +} + +void XRSession::CancelAnimationFrame(int32_t aHandle, ErrorResult& aError) { + mFrameRequestCallbacks.RemoveElementSorted(aHandle); +} + +void XRSession::Shutdown() { + mShutdown = true; + ExitPresentInternal(); + mViewerPosePool.Clear(); + mViewerPosePoolIndex = 0; + mFramePool.Clear(); + mFramePoolIndex = 0; + mActiveRenderState = nullptr; + mPendingRenderState = nullptr; + mFrameRequestCallbacks.Clear(); + + // Unregister from nsRefreshObserver + if (mRefreshDriver) { + mRefreshDriver->RemoveRefreshObserver(this, FlushType::Display); + mRefreshDriver = nullptr; + } +} + +void XRSession::ExitPresentInternal() { + if (mInputSources) { + mInputSources->Clear(this); + } + if (mDisplayClient) { + mDisplayClient->SessionEnded(this); + } + + if (mXRSystem) { + mXRSystem->SessionEnded(this); + } + + if (mActiveRenderState) { + mActiveRenderState->SessionEnded(); + } + + if (mPendingRenderState) { + mPendingRenderState->SessionEnded(); + } + + mDisplayPresentation = nullptr; + if (!mEnded) { + mEnded = true; + + XRSessionEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mSession = this; + RefPtr<XRSessionEvent> event = + XRSessionEvent::Constructor(this, u"end"_ns, init); + + event->SetTrusted(true); + this->DispatchEvent(*event); + } +} + +void XRSession::DisconnectFromOwner() { + MOZ_ASSERT(NS_IsMainThread()); + Shutdown(); + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void XRSession::LastRelease() { + // We don't want to wait for the GC to free up the presentation + // for use in other documents, so we do this in LastRelease(). + Shutdown(); +} + +RefPtr<XRViewerPose> XRSession::PooledViewerPose( + const gfx::Matrix4x4Double& aTransform, bool aEmulatedPosition) { + RefPtr<XRViewerPose> pose; + if (mViewerPosePool.Length() > mViewerPosePoolIndex) { + pose = mViewerPosePool.ElementAt(mViewerPosePoolIndex); + pose->Transform()->Update(aTransform); + pose->SetEmulatedPosition(aEmulatedPosition); + } else { + RefPtr<XRRigidTransform> transform = + new XRRigidTransform(static_cast<EventTarget*>(this), aTransform); + nsTArray<RefPtr<XRView>> views; + if (IsImmersive()) { + views.AppendElement(new XRView(GetParentObject(), XREye::Left)); + views.AppendElement(new XRView(GetParentObject(), XREye::Right)); + } else { + views.AppendElement(new XRView(GetParentObject(), XREye::None)); + } + pose = new XRViewerPose(static_cast<EventTarget*>(this), transform, + aEmulatedPosition, views); + mViewerPosePool.AppendElement(pose); + } + + mViewerPosePoolIndex++; + if (mViewerPosePoolIndex >= kMaxPoolSize) { + mViewerPosePoolIndex = 0; + } + + return pose; +} + +RefPtr<XRFrame> XRSession::PooledFrame() { + RefPtr<XRFrame> frame; + if (mFramePool.Length() > mFramePoolIndex) { + frame = mFramePool.ElementAt(mFramePoolIndex); + } else { + frame = new XRFrame(GetParentObject(), this); + mFramePool.AppendElement(frame); + } + + return frame; +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRSession.h b/dom/vr/XRSession.h new file mode 100644 index 0000000000..9215058a1b --- /dev/null +++ b/dom/vr/XRSession.h @@ -0,0 +1,159 @@ +/* -*- 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_XRSession_h_ +#define mozilla_dom_XRSession_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" +#include "nsRefreshObservers.h" + +#include "gfxVR.h" + +class nsRefreshDriver; + +namespace mozilla { +namespace gfx { +class VRDisplayClient; +class VRDisplayPresentation; +} // namespace gfx +namespace dom { + +class XRSystem; +enum class XREye : uint8_t; +enum class XRReferenceSpaceType : uint8_t; +enum class XRSessionMode : uint8_t; +enum class XRVisibilityState : uint8_t; +class XRFrame; +class XRFrameRequestCallback; +class XRInputSource; +class XRInputSourceArray; +class XRLayer; +struct XRReferenceSpaceOptions; +class XRRenderState; +struct XRRenderStateInit; +class XRSpace; +class XRViewerPose; + +class XRSession final : public DOMEventTargetHelper, public nsARefreshObserver { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XRSession, DOMEventTargetHelper) + + private: + explicit XRSession( + nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, + nsRefreshDriver* aRefreshDriver, gfx::VRDisplayClient* aClient, + uint32_t aPresentationGroup, + const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes); + + public: + static already_AddRefed<XRSession> CreateInlineSession( + nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, + const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes); + static already_AddRefed<XRSession> CreateImmersiveSession( + nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, + gfx::VRDisplayClient* aClient, uint32_t aPresentationGroup, + const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes); + + // WebIDL Boilerplate + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Attributes + XRVisibilityState VisibilityState() const; + XRRenderState* RenderState(); + XRInputSourceArray* InputSources(); + Nullable<float> GetFrameRate(); + void GetSupportedFrameRates(JSContext* aJSContext, + JS::MutableHandle<JSObject*> aRetval); + + // WebIDL Methods + void UpdateRenderState(const XRRenderStateInit& aNewState, ErrorResult& aRv); + already_AddRefed<Promise> RequestReferenceSpace( + const XRReferenceSpaceType& aReferenceSpaceType, ErrorResult& aRv); + int32_t RequestAnimationFrame(XRFrameRequestCallback& aCallback, + mozilla::ErrorResult& aError); + void CancelAnimationFrame(int32_t aHandle, mozilla::ErrorResult& aError); + already_AddRefed<Promise> End(ErrorResult& aRv); + already_AddRefed<Promise> UpdateTargetFrameRate(float aRate, + ErrorResult& aRv); + + // WebIDL Events + IMPL_EVENT_HANDLER(end); + IMPL_EVENT_HANDLER(inputsourceschange); + IMPL_EVENT_HANDLER(select); + IMPL_EVENT_HANDLER(selectstart); + IMPL_EVENT_HANDLER(selectend); + IMPL_EVENT_HANDLER(squeeze); + IMPL_EVENT_HANDLER(squeezestart); + IMPL_EVENT_HANDLER(squeezeend); + IMPL_EVENT_HANDLER(visibilitychange); + + // Non WebIDL Members + gfx::VRDisplayClient* GetDisplayClient() const; + XRRenderState* GetActiveRenderState() const; + bool IsEnded() const; + bool IsImmersive() const; + MOZ_CAN_RUN_SCRIPT + void StartFrame(); + void ExitPresent(); + RefPtr<XRViewerPose> PooledViewerPose(const gfx::Matrix4x4Double& aTransform, + bool aEmulatedPosition); + bool CanReportPoses() const; + + // nsARefreshObserver + MOZ_CAN_RUN_SCRIPT + void WillRefresh(mozilla::TimeStamp aTime) override; + + protected: + virtual ~XRSession(); + void LastRelease() override; + void DisconnectFromOwner() override; + void Shutdown(); + void ExitPresentInternal(); + void ApplyPendingRenderState(); + RefPtr<XRFrame> PooledFrame(); + RefPtr<XRSystem> mXRSystem; + bool mShutdown; + bool mEnded; + RefPtr<nsRefreshDriver> mRefreshDriver; + RefPtr<gfx::VRDisplayClient> mDisplayClient; + RefPtr<gfx::VRDisplayPresentation> mDisplayPresentation; + RefPtr<XRRenderState> mActiveRenderState; + RefPtr<XRRenderState> mPendingRenderState; + RefPtr<XRInputSourceArray> mInputSources; + + struct XRFrameRequest { + XRFrameRequest(mozilla::dom::XRFrameRequestCallback& aCallback, + int32_t aHandle) + : mCallback(&aCallback), mHandle(aHandle) {} + MOZ_CAN_RUN_SCRIPT + void Call(const DOMHighResTimeStamp& aTimeStamp, XRFrame& aFrame); + + // Comparator operators to allow RemoveElementSorted with an + // integer argument on arrays of XRFrameRequest + bool operator==(int32_t aHandle) const { return mHandle == aHandle; } + bool operator<(int32_t aHandle) const { return mHandle < aHandle; } + + RefPtr<mozilla::dom::XRFrameRequestCallback> mCallback; + int32_t mHandle; + }; + + int32_t mFrameRequestCallbackCounter; + nsTArray<XRFrameRequest> mFrameRequestCallbacks; + mozilla::TimeStamp mStartTimeStamp; + nsTArray<XRReferenceSpaceType> mEnabledReferenceSpaceTypes; + nsTArray<RefPtr<XRViewerPose>> mViewerPosePool; + uint32_t mViewerPosePoolIndex; + nsTArray<RefPtr<XRFrame>> mFramePool; + uint32_t mFramePoolIndex; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XRSession_h_ diff --git a/dom/vr/XRSpace.cpp b/dom/vr/XRSpace.cpp new file mode 100644 index 0000000000..1f6449e870 --- /dev/null +++ b/dom/vr/XRSpace.cpp @@ -0,0 +1,81 @@ +/* -*- 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/XRSpace.h" +#include "mozilla/dom/XRRigidTransform.h" +#include "VRDisplayClient.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(XRSpace) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XRSpace, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSession) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XRSpace, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSession) + // Don't need NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER because + // DOMEventTargetHelper does it for us. +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XRSpace, DOMEventTargetHelper) + +XRSpace::XRSpace(nsIGlobalObject* aParent, XRSession* aSession, + XRNativeOrigin* aNativeOrigin) + : DOMEventTargetHelper(aParent), + mSession(aSession), + mNativeOrigin(aNativeOrigin), + mOriginOffsetPosition(0.0f, 0.0f, 0.0f), + mOriginOffsetOrientation(0.0f, 0.0f, 0.0f, 1.0f) {} + +JSObject* XRSpace::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRSpace_Binding::Wrap(aCx, this, aGivenProto); +} + +XRSession* XRSpace::GetSession() const { return mSession; } + +gfx::QuaternionDouble XRSpace::GetEffectiveOriginOrientation() const { + gfx::QuaternionDouble orientation = + mNativeOrigin->GetOrientation() * mOriginOffsetOrientation; + return orientation; +} + +gfx::PointDouble3D XRSpace::GetEffectiveOriginPosition() const { + gfx::PointDouble3D position; + position = mNativeOrigin->GetPosition(); + position = mOriginOffsetOrientation.RotatePoint(position); + position += mOriginOffsetPosition; + return position; +} + +gfx::Matrix4x4Double XRSpace::GetEffectiveOriginTransform() const { + gfx::Matrix4x4Double transform; + transform.SetRotationFromQuaternion(GetEffectiveOriginOrientation()); + transform.PostTranslate(GetEffectiveOriginPosition()); + return transform; +} + +bool XRSpace::IsPositionEmulated() const { + gfx::VRDisplayClient* display = mSession->GetDisplayClient(); + if (!display) { + // When there are no sensors, the position is considered emulated. + return true; + } + const gfx::VRDisplayInfo& displayInfo = display->GetDisplayInfo(); + if (displayInfo.GetCapabilities() & + gfx::VRDisplayCapabilityFlags::Cap_PositionEmulated) { + // Cap_PositionEmulated indicates that the position is always emulated. + return true; + } + const gfx::VRHMDSensorState& sensorState = display->GetSensorState(); + // When positional tracking is lost, the position is considered emulated. + return ((sensorState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position) == + gfx::VRDisplayCapabilityFlags::Cap_None); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRSpace.h b/dom/vr/XRSpace.h new file mode 100644 index 0000000000..a87480857e --- /dev/null +++ b/dom/vr/XRSpace.h @@ -0,0 +1,56 @@ +/* -*- 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_XRSpace_h_ +#define mozilla_dom_XRSpace_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" +#include "XRNativeOrigin.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +class XRRigidTransform; +class XRSession; + +class XRSpace : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XRSpace, DOMEventTargetHelper) + + explicit XRSpace(nsIGlobalObject* aParent, XRSession* aSession, + XRNativeOrigin* aNativeOrigin); + + // WebIDL Boilerplate + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + XRSession* GetSession() const; + XRNativeOrigin* GetNativeOrigin() const; + + // Non webIDL Members + gfx::QuaternionDouble GetEffectiveOriginOrientation() const; + gfx::PointDouble3D GetEffectiveOriginPosition() const; + gfx::Matrix4x4Double GetEffectiveOriginTransform() const; + virtual bool IsPositionEmulated() const; + + protected: + virtual ~XRSpace() = default; + + RefPtr<XRSession> mSession; + RefPtr<XRNativeOrigin> mNativeOrigin; + + // https://immersive-web.github.io/webxr/#xrspace-origin-offset + // Origin Offset, represented as a rigid transform + gfx::PointDouble3D mOriginOffsetPosition; + gfx::QuaternionDouble mOriginOffsetOrientation; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRSpace_h_ diff --git a/dom/vr/XRSystem.cpp b/dom/vr/XRSystem.cpp new file mode 100644 index 0000000000..97795ac7cd --- /dev/null +++ b/dom/vr/XRSystem.cpp @@ -0,0 +1,703 @@ +/* -*- 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/XRSystem.h" + +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/XRPermissionRequest.h" +#include "mozilla/dom/XRSession.h" +#include "mozilla/dom/BindingCallContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "nsGlobalWindow.h" +#include "nsThreadUtils.h" +#include "gfxVR.h" +#include "VRDisplayClient.h" +#include "VRManagerChild.h" + +namespace mozilla::dom { + +using namespace gfx; + +//////////////////////////////////////////////////////////////////////////////// +// XRSystem cycle collection +NS_IMPL_CYCLE_COLLECTION_CLASS(XRSystem) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XRSystem, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActiveImmersiveSession) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineSessions) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIsSessionSupportedRequests) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE( + mRequestSessionRequestsWaitingForRuntimeDetection) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRequestSessionRequestsWithoutHardware) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE( + mRequestSessionRequestsWaitingForEnumeration) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XRSystem, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mActiveImmersiveSession) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInlineSessions) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIsSessionSupportedRequests) + NS_IMPL_CYCLE_COLLECTION_UNLINK( + mRequestSessionRequestsWaitingForRuntimeDetection) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRequestSessionRequestsWithoutHardware) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRequestSessionRequestsWaitingForEnumeration) + // Don't need NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER because + // DOMEventTargetHelper does it for us. +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XRSystem, DOMEventTargetHelper) + +JSObject* XRSystem::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRSystem_Binding::Wrap(aCx, this, aGivenProto); +} + +// static +already_AddRefed<XRSystem> XRSystem::Create(nsPIDOMWindowInner* aWindow) { + MOZ_ASSERT(aWindow); + + RefPtr<XRSystem> service = new XRSystem(aWindow); + return service.forget(); +} + +XRSystem::XRSystem(nsPIDOMWindowInner* aWindow) + : DOMEventTargetHelper(aWindow), + mShuttingDown(false), + mPendingImmersiveSession(false), + mEnumerationInFlight(false) { + // Unregister with VRManagerChild + VRManagerChild* vmc = VRManagerChild::Get(); + if (vmc) { + vmc->AddListener(this); + } +} + +void XRSystem::Shutdown() { + MOZ_ASSERT(!mShuttingDown); + mShuttingDown = true; + + // Unregister from VRManagerChild + if (VRManagerChild::IsCreated()) { + VRManagerChild* vmc = VRManagerChild::Get(); + vmc->RemoveListener(this); + } +} + +void XRSystem::SessionEnded(XRSession* aSession) { + if (mActiveImmersiveSession == aSession) { + mActiveImmersiveSession = nullptr; + } + mInlineSessions.RemoveElement(aSession); +} + +already_AddRefed<Promise> XRSystem::IsSessionSupported(XRSessionMode aMode, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetParentObject(); + NS_ENSURE_TRUE(global, nullptr); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + NS_ENSURE_TRUE(!aRv.Failed(), nullptr); + + if (aMode == XRSessionMode::Inline) { + promise->MaybeResolve(true); + return promise.forget(); + } + + if (mIsSessionSupportedRequests.IsEmpty()) { + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->DetectRuntimes(); + } + + RefPtr<IsSessionSupportedRequest> request = + new IsSessionSupportedRequest(aMode, promise); + mIsSessionSupportedRequests.AppendElement(request); + return promise.forget(); +} + +already_AddRefed<Promise> XRSystem::RequestSession( + JSContext* aCx, XRSessionMode aMode, const XRSessionInit& aOptions, + CallerType aCallerType, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetParentObject(); + NS_ENSURE_TRUE(global, nullptr); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + NS_ENSURE_TRUE(!aRv.Failed(), nullptr); + + bool immersive = (aMode == XRSessionMode::Immersive_vr || + aMode == XRSessionMode::Immersive_ar); + + // The document must be a responsible document, active and focused. + nsCOMPtr<Document> responsibleDocument = GetDocumentIfCurrent(); + if (!responsibleDocument) { + // The document is not trustworthy + promise->MaybeRejectWithSecurityError("This document is not responsible."); + return promise.forget(); + } + + if (immersive || aOptions.mRequiredFeatures.WasPassed() || + aOptions.mOptionalFeatures.WasPassed()) { + if (!responsibleDocument->HasValidTransientUserGestureActivation() && + aCallerType != CallerType::System && + StaticPrefs::dom_vr_require_gesture()) { + // A user gesture is required. + promise->MaybeRejectWithSecurityError("A user gesture is required."); + return promise.forget(); + } + } + + nsTArray<XRReferenceSpaceType> requiredReferenceSpaceTypes; + nsTArray<XRReferenceSpaceType> optionalReferenceSpaceTypes; + + /** + * By default, all sessions will require XRReferenceSpaceType::Viewer + * and immersive sessions will require XRReferenceSpaceType::Local. + * + * https://www.w3.org/TR/webxr/#default-features + */ + requiredReferenceSpaceTypes.AppendElement(XRReferenceSpaceType::Viewer); + if (immersive) { + requiredReferenceSpaceTypes.AppendElement(XRReferenceSpaceType::Local); + } + + BindingCallContext callCx(aCx, "XRSystem.requestSession"); + + if (aOptions.mRequiredFeatures.WasPassed()) { + const Sequence<JS::Value>& arr = (aOptions.mRequiredFeatures.Value()); + for (const JS::Value& val : arr) { + if (!val.isNull() && !val.isUndefined()) { + bool bFound = false; + JS::Rooted<JS::Value> v(aCx, val); + int index = 0; + if (FindEnumStringIndex<false>( + callCx, v, XRReferenceSpaceTypeValues::strings, + "XRReferenceSpaceType", "Argument 2 of XR.requestSession", + &index)) { + if (index >= 0) { + requiredReferenceSpaceTypes.AppendElement( + static_cast<XRReferenceSpaceType>(index)); + bFound = true; + } + } + if (!bFound) { + promise->MaybeRejectWithNotSupportedError( + "A required feature for the XRSession is not available."); + return promise.forget(); + } + } + } + } + + if (aOptions.mOptionalFeatures.WasPassed()) { + const Sequence<JS::Value>& arr = (aOptions.mOptionalFeatures.Value()); + for (const JS::Value& val : arr) { + if (!val.isNull() && !val.isUndefined()) { + JS::Rooted<JS::Value> v(aCx, val); + int index = 0; + if (FindEnumStringIndex<false>( + callCx, v, XRReferenceSpaceTypeValues::strings, + "XRReferenceSpaceType", "Argument 2 of XR.requestSession", + &index)) { + if (index >= 0) { + optionalReferenceSpaceTypes.AppendElement( + static_cast<XRReferenceSpaceType>(index)); + } + } + } + } + } + + if (immersive) { + if (mPendingImmersiveSession || mActiveImmersiveSession) { + promise->MaybeRejectWithInvalidStateError( + "There can only be one immersive XRSession."); + return promise.forget(); + } + mPendingImmersiveSession = true; + } + + bool isChromeSession = aCallerType == CallerType::System; + uint32_t presentationGroup = + isChromeSession ? gfx::kVRGroupChrome : gfx::kVRGroupContent; + RefPtr<RequestSessionRequest> request = new RequestSessionRequest( + aMode, presentationGroup, promise, requiredReferenceSpaceTypes, + optionalReferenceSpaceTypes); + if (request->WantsHardware()) { + QueueSessionRequestWithEnumeration(request); + } else { + QueueSessionRequestWithoutEnumeration(request); + } + + return promise.forget(); +} + +void XRSystem::QueueSessionRequestWithEnumeration( + RequestSessionRequest* aRequest) { + MOZ_ASSERT(aRequest->WantsHardware()); + mRequestSessionRequestsWaitingForRuntimeDetection.AppendElement(aRequest); + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->DetectRuntimes(); +} + +void XRSystem::QueueSessionRequestWithoutEnumeration( + RequestSessionRequest* aRequest) { + MOZ_ASSERT(!aRequest->NeedsHardware()); + mRequestSessionRequestsWithoutHardware.AppendElement(aRequest); + + ResolveSessionRequestsWithoutHardware(); +} + +bool XRSystem::CancelHardwareRequest(RequestSessionRequest* aRequest) { + if (!aRequest->NeedsHardware()) { + // If hardware access was an optional requirement and the user + // opted not to provide access, queue the request + // to be resolved without hardware. + QueueSessionRequestWithoutEnumeration(aRequest); + return false; + } + + if (aRequest->IsImmersive()) { + mPendingImmersiveSession = false; + } + return true; +} + +bool XRSystem::OnXRPermissionRequestAllow() { + if (!gfx::VRManagerChild::IsCreated()) { + // It's possible that this callback returns after + // we have already started shutting down. + return false; + } + if (!mEnumerationInFlight) { + mEnumerationInFlight = true; + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + Unused << vm->EnumerateVRDisplays(); + } + return mEnumerationInFlight || + !mRequestSessionRequestsWaitingForEnumeration.IsEmpty(); +} + +void XRSystem::OnXRPermissionRequestCancel() { + nsTArray<RefPtr<RequestSessionRequest>> requestSessionRequests( + std::move(mRequestSessionRequestsWaitingForEnumeration)); + for (RefPtr<RequestSessionRequest>& request : requestSessionRequests) { + if (CancelHardwareRequest(request)) { + request->mPromise->MaybeRejectWithSecurityError( + "A device supporting the requested session " + "configuration could not be found."); + } + } +} + +bool XRSystem::FeaturePolicyBlocked() const { + nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(GetOwner()); + if (!win) { + return true; + } + RefPtr<XRPermissionRequest> request = + new XRPermissionRequest(win, win->WindowID()); + return !(request->CheckPermissionDelegate()); +} + +bool XRSystem::HasActiveImmersiveSession() const { + return mActiveImmersiveSession; +} + +void XRSystem::ResolveSessionRequestsWithoutHardware() { + // Resolve promises returned by RequestSession + nsTArray<RefPtr<gfx::VRDisplayClient>> displays; + // Try resolving support without a device, for inline sessions. + displays.AppendElement(nullptr); + + nsTArray<RefPtr<RequestSessionRequest>> requestSessionRequests( + std::move(mRequestSessionRequestsWithoutHardware)); + + ResolveSessionRequests(requestSessionRequests, displays); +} + +void XRSystem::NotifyEnumerationCompleted() { + // Enumeration has completed. + mEnumerationInFlight = false; + + if (!gfx::VRManagerChild::IsCreated()) { + // It's possible that this callback returns after + // we have already started shutting down. + return; + } + + // Resolve promises returned by RequestSession + nsTArray<RefPtr<gfx::VRDisplayClient>> displays; + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + vm->GetVRDisplays(displays); + + nsTArray<RefPtr<RequestSessionRequest>> requestSessionRequests( + std::move(mRequestSessionRequestsWaitingForEnumeration)); + + ResolveSessionRequests(requestSessionRequests, displays); +} + +void XRSystem::ResolveSessionRequests( + nsTArray<RefPtr<RequestSessionRequest>>& aRequests, + const nsTArray<RefPtr<gfx::VRDisplayClient>>& aDisplays) { + for (RefPtr<RequestSessionRequest>& request : aRequests) { + RefPtr<XRSession> session; + if (request->IsImmersive()) { + mPendingImmersiveSession = false; + } + // Select an XR device + for (const RefPtr<gfx::VRDisplayClient>& display : aDisplays) { + nsTArray<XRReferenceSpaceType> enabledReferenceSpaceTypes; + if (request->ResolveSupport(display, enabledReferenceSpaceTypes)) { + if (request->IsImmersive()) { + session = XRSession::CreateImmersiveSession( + GetOwner(), this, display, request->GetPresentationGroup(), + enabledReferenceSpaceTypes); + mActiveImmersiveSession = session; + } else { + session = XRSession::CreateInlineSession(GetOwner(), this, + enabledReferenceSpaceTypes); + mInlineSessions.AppendElement(session); + } + request->mPromise->MaybeResolve(session); + break; + } + } + if (!session) { + request->mPromise->MaybeRejectWithNotSupportedError( + "A device supporting the required XRSession configuration " + "could not be found."); + } + } +} + +void XRSystem::NotifyDetectRuntimesCompleted() { + ResolveIsSessionSupportedRequests(); + if (!mRequestSessionRequestsWaitingForRuntimeDetection.IsEmpty()) { + ProcessSessionRequestsWaitingForRuntimeDetection(); + } +} + +void XRSystem::ResolveIsSessionSupportedRequests() { + // Resolve promises returned by IsSessionSupported + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + nsTArray<RefPtr<IsSessionSupportedRequest>> isSessionSupportedRequests( + std::move(mIsSessionSupportedRequests)); + bool featurePolicyBlocked = FeaturePolicyBlocked(); + + for (RefPtr<IsSessionSupportedRequest>& request : + isSessionSupportedRequests) { + if (featurePolicyBlocked) { + request->mPromise->MaybeRejectWithSecurityError( + "The xr-spatial-tracking feature policy is required."); + continue; + } + + bool supported = false; + switch (request->GetSessionMode()) { + case XRSessionMode::Immersive_vr: + supported = vm->RuntimeSupportsVR(); + break; + case XRSessionMode::Immersive_ar: + supported = vm->RuntimeSupportsAR(); + break; + default: + break; + } + request->mPromise->MaybeResolve(supported); + } +} + +void XRSystem::ProcessSessionRequestsWaitingForRuntimeDetection() { + bool alreadyRequestedPermission = + !mRequestSessionRequestsWaitingForEnumeration.IsEmpty(); + bool featurePolicyBlocked = FeaturePolicyBlocked(); + gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); + + nsTArray<RefPtr<RequestSessionRequest>> sessionRequests( + std::move(mRequestSessionRequestsWaitingForRuntimeDetection)); + + for (RefPtr<RequestSessionRequest>& request : sessionRequests) { + bool compatibleRuntime = false; + switch (request->GetSessionMode()) { + case XRSessionMode::Immersive_vr: + compatibleRuntime = vm->RuntimeSupportsVR(); + break; + case XRSessionMode::Immersive_ar: + compatibleRuntime = vm->RuntimeSupportsAR(); + break; + case XRSessionMode::Inline: + compatibleRuntime = vm->RuntimeSupportsInline(); + break; + default: + break; + } + if (!compatibleRuntime) { + // If none of the requested sessions are supported by a + // runtime, early exit without showing a permission prompt. + if (CancelHardwareRequest(request)) { + request->mPromise->MaybeRejectWithNotSupportedError( + "A device supporting the required XRSession configuration " + "could not be found."); + } + continue; + } + if (featurePolicyBlocked) { + // Don't show a permission prompt if blocked by feature policy. + if (CancelHardwareRequest(request)) { + request->mPromise->MaybeRejectWithSecurityError( + "The xr-spatial-tracking feature policy is required."); + } + continue; + } + // To continue evaluating this request, it must wait for hardware + // enumeration and permission request. + mRequestSessionRequestsWaitingForEnumeration.AppendElement(request); + } + + if (!mRequestSessionRequestsWaitingForEnumeration.IsEmpty() && + !alreadyRequestedPermission) { + /** + * Inline sessions will require only a user gesture + * and should not trigger XR permission UI. + * This is not a problem currently, as the only platforms + * allowing xr-spatial-tracking for inline sessions do not + * present a modal XR permission UI. (eg. Android Firefox Reality) + */ + nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(GetOwner()); + win->RequestXRPermission(); + } +} + +void XRSystem::NotifyVRDisplayMounted(uint32_t aDisplayID) {} +void XRSystem::NotifyVRDisplayUnmounted(uint32_t aDisplayID) {} + +void XRSystem::NotifyVRDisplayConnect(uint32_t aDisplayID) { + DispatchTrustedEvent(u"devicechange"_ns); +} + +void XRSystem::NotifyVRDisplayDisconnect(uint32_t aDisplayID) { + DispatchTrustedEvent(u"devicechange"_ns); +} + +void XRSystem::NotifyVRDisplayPresentChange(uint32_t aDisplayID) {} +void XRSystem::NotifyPresentationGenerationChanged(uint32_t aDisplayID) { + if (mActiveImmersiveSession) { + mActiveImmersiveSession->ExitPresent(); + } +} +bool XRSystem::GetStopActivityStatus() const { return true; } + +RequestSessionRequest::RequestSessionRequest( + XRSessionMode aSessionMode, uint32_t aPresentationGroup, Promise* aPromise, + const nsTArray<XRReferenceSpaceType>& aRequiredReferenceSpaceTypes, + const nsTArray<XRReferenceSpaceType>& aOptionalReferenceSpaceTypes) + : mPromise(aPromise), + mSessionMode(aSessionMode), + mPresentationGroup(aPresentationGroup), + mRequiredReferenceSpaceTypes(aRequiredReferenceSpaceTypes.Clone()), + mOptionalReferenceSpaceTypes(aOptionalReferenceSpaceTypes.Clone()) {} + +bool RequestSessionRequest::ResolveSupport( + const gfx::VRDisplayClient* aDisplay, + nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) const { + if (aDisplay) { + if (!aDisplay->GetIsConnected()) { + return false; + } + if ((aDisplay->GetDisplayInfo().GetPresentingGroups() & + mPresentationGroup) != 0) { + return false; + } + + const gfx::VRDisplayInfo& info = aDisplay->GetDisplayInfo(); + switch (mSessionMode) { + case XRSessionMode::Inline: + if (!bool(info.mDisplayState.capabilityFlags & + gfx::VRDisplayCapabilityFlags::Cap_Inline)) { + return false; + } + break; + case XRSessionMode::Immersive_vr: + if (!bool(info.mDisplayState.capabilityFlags & + gfx::VRDisplayCapabilityFlags::Cap_ImmersiveVR)) { + return false; + } + break; + case XRSessionMode::Immersive_ar: + if (!bool(info.mDisplayState.capabilityFlags & + gfx::VRDisplayCapabilityFlags::Cap_ImmersiveAR)) { + return false; + } + break; + default: + break; + } + } else if (mSessionMode != XRSessionMode::Inline) { + // If we don't have a device, we can only support inline sessions + return false; + } + + // All sessions support XRReferenceSpaceType::Viewer by default + aEnabledReferenceSpaceTypes.AppendElement(XRReferenceSpaceType::Viewer); + + // Immersive sessions support XRReferenceSpaceType::Local by default + if (IsImmersive()) { + aEnabledReferenceSpaceTypes.AppendElement(XRReferenceSpaceType::Local); + } + + for (XRReferenceSpaceType type : mRequiredReferenceSpaceTypes) { + if (aDisplay) { + if (!aDisplay->IsReferenceSpaceTypeSupported(type)) { + return false; + } + } else if (type != XRReferenceSpaceType::Viewer) { + // If we don't have a device, We only support + // XRReferenceSpaceType::Viewer + return false; + } + if (!aEnabledReferenceSpaceTypes.Contains(type)) { + aEnabledReferenceSpaceTypes.AppendElement(type); + } + } + if (aDisplay) { + for (XRReferenceSpaceType type : mOptionalReferenceSpaceTypes) { + if (aDisplay->IsReferenceSpaceTypeSupported(type) && + !aEnabledReferenceSpaceTypes.Contains(type)) { + aEnabledReferenceSpaceTypes.AppendElement(type); + } + } + } + return true; +} + +bool RequestSessionRequest::IsImmersive() const { + return (mSessionMode == XRSessionMode::Immersive_vr || + mSessionMode == XRSessionMode::Immersive_ar); +} + +bool RequestSessionRequest::WantsHardware() const { + for (XRReferenceSpaceType type : mOptionalReferenceSpaceTypes) { + // Any XRReferenceSpaceType other than Viewer requires hardware + if (type != XRReferenceSpaceType::Viewer) { + return true; + } + } + return NeedsHardware(); +} + +bool RequestSessionRequest::NeedsHardware() const { + for (XRReferenceSpaceType type : mRequiredReferenceSpaceTypes) { + // Any XRReferenceSpaceType other than Viewer requires hardware + if (type != XRReferenceSpaceType::Viewer) { + return true; + } + } + return false; +} + +XRSessionMode RequestSessionRequest::GetSessionMode() const { + return mSessionMode; +} + +uint32_t RequestSessionRequest::GetPresentationGroup() const { + return mPresentationGroup; +} + +//////////////////////////////////////////////////////////////////////////////// +// IsSessionSupportedRequest cycle collection +NS_IMPL_CYCLE_COLLECTION(IsSessionSupportedRequest, mPromise) + +XRSessionMode IsSessionSupportedRequest::GetSessionMode() const { + return mSessionMode; +} + +//////////////////////////////////////////////////////////////////////////////// +// XRRequestSessionPermissionRequest cycle collection +NS_IMPL_CYCLE_COLLECTION_INHERITED(XRRequestSessionPermissionRequest, + ContentPermissionRequestBase) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0( + XRRequestSessionPermissionRequest, ContentPermissionRequestBase) + +XRRequestSessionPermissionRequest::XRRequestSessionPermissionRequest( + nsPIDOMWindowInner* aWindow, nsIPrincipal* aNodePrincipal, + AllowCallback&& aAllowCallback, + AllowAnySiteCallback&& aAllowAnySiteCallback, + CancelCallback&& aCancelCallback) + : ContentPermissionRequestBase(aNodePrincipal, aWindow, "dom.xr"_ns, + "xr"_ns), + mAllowCallback(std::move(aAllowCallback)), + mAllowAnySiteCallback(std::move(aAllowAnySiteCallback)), + mCancelCallback(std::move(aCancelCallback)), + mCallbackCalled(false) { + mPermissionRequests.AppendElement( + PermissionRequest(mType, nsTArray<nsString>())); +} + +XRRequestSessionPermissionRequest::~XRRequestSessionPermissionRequest() { + Cancel(); +} + +NS_IMETHODIMP +XRRequestSessionPermissionRequest::Cancel() { + if (!mCallbackCalled) { + mCallbackCalled = true; + mCancelCallback(); + } + return NS_OK; +} + +NS_IMETHODIMP +XRRequestSessionPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) { + nsTArray<PermissionChoice> choices; + nsresult rv = TranslateChoices(aChoices, mPermissionRequests, choices); + if (NS_FAILED(rv)) { + return rv; + } + + // There is no support to allow grants automatically from the prompting code + // path. + + if (!mCallbackCalled) { + mCallbackCalled = true; + if (choices.Length() == 1 && + choices[0].choice().EqualsLiteral("allow-on-any-site")) { + mAllowAnySiteCallback(); + } else if (choices.Length() == 1 && + choices[0].choice().EqualsLiteral("allow")) { + mAllowCallback(); + } + } + return NS_OK; +} + +already_AddRefed<XRRequestSessionPermissionRequest> +XRRequestSessionPermissionRequest::Create( + nsPIDOMWindowInner* aWindow, AllowCallback&& aAllowCallback, + AllowAnySiteCallback&& aAllowAnySiteCallback, + CancelCallback&& aCancelCallback) { + if (!aWindow) { + return nullptr; + } + nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(aWindow); + if (!win->GetPrincipal()) { + return nullptr; + } + RefPtr<XRRequestSessionPermissionRequest> request = + new XRRequestSessionPermissionRequest( + aWindow, win->GetPrincipal(), std::move(aAllowCallback), + std::move(aAllowAnySiteCallback), std::move(aCancelCallback)); + return request.forget(); +} + +//////////////////////////////////////////////////////////////////////////////// +// RequestSessionRequest cycle collection +NS_IMPL_CYCLE_COLLECTION(RequestSessionRequest, mPromise) + +} // namespace mozilla::dom diff --git a/dom/vr/XRSystem.h b/dom/vr/XRSystem.h new file mode 100644 index 0000000000..2a49dfb68f --- /dev/null +++ b/dom/vr/XRSystem.h @@ -0,0 +1,174 @@ +/* -*- 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_XRsystem_h_ +#define mozilla_dom_XRsystem_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" +#include "nsContentPermissionHelper.h" +#include "VRManagerChild.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +struct XRSessionCreationOptions; + +class IsSessionSupportedRequest { + public: + IsSessionSupportedRequest(XRSessionMode aSessionMode, Promise* aPromise) + : mPromise(aPromise), mSessionMode(aSessionMode) {} + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(IsSessionSupportedRequest) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(IsSessionSupportedRequest) + + RefPtr<Promise> mPromise; + XRSessionMode GetSessionMode() const; + + private: + ~IsSessionSupportedRequest() = default; + XRSessionMode mSessionMode; +}; + +class RequestSessionRequest { + public: + RequestSessionRequest( + XRSessionMode aSessionMode, uint32_t aPresentationGroup, + Promise* aPromise, + const nsTArray<XRReferenceSpaceType>& aRequiredReferenceSpaceTypes, + const nsTArray<XRReferenceSpaceType>& aOptionalReferenceSpaceTypes); + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RequestSessionRequest) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RequestSessionRequest) + RefPtr<Promise> mPromise; + + bool ResolveSupport( + const gfx::VRDisplayClient* aDisplay, + nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) const; + bool IsImmersive() const; + bool WantsHardware() const; + bool NeedsHardware() const; + XRSessionMode GetSessionMode() const; + uint32_t GetPresentationGroup() const; + + private: + ~RequestSessionRequest() = default; + XRSessionMode mSessionMode; + uint32_t mPresentationGroup; + nsTArray<XRReferenceSpaceType> mRequiredReferenceSpaceTypes; + nsTArray<XRReferenceSpaceType> mOptionalReferenceSpaceTypes; +}; + +class XRRequestSessionPermissionRequest final + : public ContentPermissionRequestBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XRRequestSessionPermissionRequest, + ContentPermissionRequestBase) + + // nsIContentPermissionRequest + NS_IMETHOD Cancel(void) override; + NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override; + + using AllowCallback = std::function<void()>; + using AllowAnySiteCallback = std::function<void()>; + using CancelCallback = std::function<void()>; + + static already_AddRefed<XRRequestSessionPermissionRequest> Create( + nsPIDOMWindowInner* aWindow, AllowCallback&& aAllowCallback, + AllowAnySiteCallback&& aAllowAnySiteCallback, + CancelCallback&& aCancelCallback); + + using AutoGrantDelayPromise = MozPromise<bool, bool, true>; + RefPtr<AutoGrantDelayPromise> MaybeDelayAutomaticGrants(); + + private: + XRRequestSessionPermissionRequest( + nsPIDOMWindowInner* aWindow, nsIPrincipal* aNodePrincipal, + AllowCallback&& aAllowCallback, + AllowAnySiteCallback&& aAllowAnySiteCallback, + CancelCallback&& aCancelCallback); + ~XRRequestSessionPermissionRequest(); + + AllowCallback mAllowCallback; + AllowAnySiteCallback mAllowAnySiteCallback; + CancelCallback mCancelCallback; + nsTArray<PermissionRequest> mPermissionRequests; + bool mCallbackCalled; +}; + +class XRSystem final : public DOMEventTargetHelper, + public gfx::VRManagerEventObserver { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XRSystem, DOMEventTargetHelper) + + void Shutdown(); + void SessionEnded(XRSession* aSession); + bool FeaturePolicyBlocked() const; + bool OnXRPermissionRequestAllow(); + void OnXRPermissionRequestCancel(); + bool HasActiveImmersiveSession() const; + + // WebIDL Boilerplate + static already_AddRefed<XRSystem> Create(nsPIDOMWindowInner* aWindow); + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + already_AddRefed<Promise> IsSessionSupported(XRSessionMode aMode, + ErrorResult& aRv); + already_AddRefed<Promise> RequestSession(JSContext* aCx, XRSessionMode aMode, + const XRSessionInit& aOptions, + CallerType aCallerType, + ErrorResult& aRv); + IMPL_EVENT_HANDLER(devicechange); + + // VRManagerEventObserver interface + void NotifyVRDisplayMounted(uint32_t aDisplayID) override; + void NotifyVRDisplayUnmounted(uint32_t aDisplayID) override; + void NotifyVRDisplayConnect(uint32_t aDisplayID) override; + void NotifyVRDisplayDisconnect(uint32_t aDisplayID) override; + void NotifyVRDisplayPresentChange(uint32_t aDisplayID) override; + void NotifyPresentationGenerationChanged(uint32_t aDisplayID) override; + void NotifyEnumerationCompleted() override; + void NotifyDetectRuntimesCompleted() override; + bool GetStopActivityStatus() const override; + + private: + explicit XRSystem(nsPIDOMWindowInner* aWindow); + virtual ~XRSystem() = default; + void ResolveIsSessionSupportedRequests(); + void ProcessSessionRequestsWaitingForRuntimeDetection(); + bool CancelHardwareRequest(RequestSessionRequest* aRequest); + void QueueSessionRequestWithEnumeration(RequestSessionRequest* aRequest); + void QueueSessionRequestWithoutEnumeration(RequestSessionRequest* aRequest); + void ResolveSessionRequestsWithoutHardware(); + void ResolveSessionRequests( + nsTArray<RefPtr<RequestSessionRequest>>& aRequests, + const nsTArray<RefPtr<gfx::VRDisplayClient>>& aDisplays); + + bool mShuttingDown; + // https://immersive-web.github.io/webxr/#pending-immersive-session + bool mPendingImmersiveSession; + // https://immersive-web.github.io/webxr/#active-immersive-session + RefPtr<XRSession> mActiveImmersiveSession; + // https://immersive-web.github.io/webxr/#list-of-inline-sessions + nsTArray<RefPtr<XRSession>> mInlineSessions; + + bool mEnumerationInFlight; + + nsTArray<RefPtr<IsSessionSupportedRequest>> mIsSessionSupportedRequests; + nsTArray<RefPtr<RequestSessionRequest>> + mRequestSessionRequestsWithoutHardware; + nsTArray<RefPtr<RequestSessionRequest>> + mRequestSessionRequestsWaitingForRuntimeDetection; + nsTArray<RefPtr<RequestSessionRequest>> + mRequestSessionRequestsWaitingForEnumeration; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRsystem_h_ diff --git a/dom/vr/XRView.cpp b/dom/vr/XRView.cpp new file mode 100644 index 0000000000..07c3b81c9f --- /dev/null +++ b/dom/vr/XRView.cpp @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/XRView.h" + +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/XRRigidTransform.h" +#include "mozilla/dom/Pose.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS(XRView, + (mParent, mTransform), + (mJSProjectionMatrix)) + +XRView::XRView(nsISupports* aParent, const XREye& aEye) + : mParent(aParent), + mEye(aEye), + mPosition(gfx::PointDouble3D()), + mOrientation(gfx::QuaternionDouble()), + mJSProjectionMatrix(nullptr) { + mozilla::HoldJSObjects(this); +} + +XRView::~XRView() { mozilla::DropJSObjects(this); } + +JSObject* XRView::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRView_Binding::Wrap(aCx, this, aGivenProto); +} + +void XRView::Update(const gfx::PointDouble3D& aPosition, + const gfx::QuaternionDouble& aOrientation, + const gfx::Matrix4x4& aProjectionMatrix) { + mPosition = aPosition; + mOrientation = aOrientation; + mProjectionMatrix = aProjectionMatrix; + if (mTransform) { + mTransform->Update(aPosition, aOrientation); + } + if (aProjectionMatrix != mProjectionMatrix) { + mProjectionNeedsUpdate = true; + mProjectionMatrix = aProjectionMatrix; + } +} + +XREye XRView::Eye() const { return mEye; } + +void XRView::GetProjectionMatrix(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + if (!mJSProjectionMatrix || mProjectionNeedsUpdate) { + mProjectionNeedsUpdate = false; + gfx::Matrix4x4 mat; + + Pose::SetFloat32Array(aCx, this, aRetval, mJSProjectionMatrix, + mProjectionMatrix.components, 16, aRv); + if (!mJSProjectionMatrix) { + return; + } + } + if (mJSProjectionMatrix) { + JS::ExposeObjectToActiveJS(mJSProjectionMatrix); + } + aRetval.set(mJSProjectionMatrix); +} + +already_AddRefed<XRRigidTransform> XRView::GetTransform(ErrorResult& aRv) { + if (!mTransform) { + mTransform = new XRRigidTransform(mParent, mPosition, mOrientation); + } + RefPtr<XRRigidTransform> transform = mTransform; + return transform.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRView.h b/dom/vr/XRView.h new file mode 100644 index 0000000000..157e489022 --- /dev/null +++ b/dom/vr/XRView.h @@ -0,0 +1,56 @@ +/* -*- 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_XRView_h_ +#define mozilla_dom_XRView_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +enum class XREye : uint8_t; +class XRRigidTransform; + +class XRView final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(XRView) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(XRView) + + explicit XRView(nsISupports* aParent, const XREye& aEye); + + void Update(const gfx::PointDouble3D& aPosition, + const gfx::QuaternionDouble& aOrientation, + const gfx::Matrix4x4& aProjectionMatrix); + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + XREye Eye() const; + void GetProjectionMatrix(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + already_AddRefed<XRRigidTransform> GetTransform(ErrorResult& aRv); + + protected: + virtual ~XRView(); + + nsCOMPtr<nsISupports> mParent; + XREye mEye; + gfx::PointDouble3D mPosition; + gfx::QuaternionDouble mOrientation; + gfx::Matrix4x4 mProjectionMatrix; + JS::Heap<JSObject*> mJSProjectionMatrix; + bool mProjectionNeedsUpdate = true; + RefPtr<XRRigidTransform> mTransform; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRView_h_ diff --git a/dom/vr/XRViewerPose.cpp b/dom/vr/XRViewerPose.cpp new file mode 100644 index 0000000000..af11306c08 --- /dev/null +++ b/dom/vr/XRViewerPose.cpp @@ -0,0 +1,44 @@ +/* -*- 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/XRViewerPose.h" +#include "mozilla/dom/XRView.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(XRViewerPose) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XRViewerPose, XRPose) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mViews) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XRViewerPose, XRPose) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mViews) + // Don't need NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER because + // XRPose does it for us. +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XRViewerPose, XRPose) + +XRViewerPose::XRViewerPose(nsISupports* aParent, XRRigidTransform* aTransform, + bool aEmulatedPosition, + const nsTArray<RefPtr<XRView>>& aViews) + : XRPose(aParent, aTransform, aEmulatedPosition), mViews(aViews.Clone()) {} + +JSObject* XRViewerPose::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRViewerPose_Binding::Wrap(aCx, this, aGivenProto); +} + +RefPtr<XRView>& XRViewerPose::GetEye(int32_t aIndex) { + return mViews.ElementAt(aIndex); +} + +void XRViewerPose::GetViews(nsTArray<RefPtr<XRView>>& aResult) { + aResult = mViews.Clone(); +} + +} // namespace mozilla::dom diff --git a/dom/vr/XRViewerPose.h b/dom/vr/XRViewerPose.h new file mode 100644 index 0000000000..a4340b12f9 --- /dev/null +++ b/dom/vr/XRViewerPose.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XRViewerPose_h_ +#define mozilla_dom_XRViewerPose_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" +#include "mozilla/dom/XRPose.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +class XRRigidTransform; +class XRView; + +class XRViewerPose final : public XRPose { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XRViewerPose, XRPose) + + explicit XRViewerPose(nsISupports* aParent, XRRigidTransform* aTransform, + bool aEmulatedPosition, + const nsTArray<RefPtr<XRView>>& aViews); + RefPtr<XRView>& GetEye(int32_t aIndex); + + // WebIDL Boilerplate + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + void GetViews(nsTArray<RefPtr<XRView>>& aResult); + + protected: + virtual ~XRViewerPose() = default; + nsTArray<RefPtr<XRView>> mViews; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRViewerPose_h_ diff --git a/dom/vr/XRViewport.cpp b/dom/vr/XRViewport.cpp new file mode 100644 index 0000000000..ffa9d5bff8 --- /dev/null +++ b/dom/vr/XRViewport.cpp @@ -0,0 +1,29 @@ +/* -*- 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/XRViewport.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(XRViewport, mParent) + +XRViewport::XRViewport(nsISupports* aParent, const gfx::IntRect& aRect) + : mParent(aParent), mRect(aRect) {} + +JSObject* XRViewport::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XRViewport_Binding::Wrap(aCx, this, aGivenProto); +} + +int32_t XRViewport::X() { return mRect.X(); } + +int32_t XRViewport::Y() { return mRect.Y(); } + +int32_t XRViewport::Width() { return mRect.Width(); } + +int32_t XRViewport::Height() { return mRect.Height(); } + +} // namespace mozilla::dom diff --git a/dom/vr/XRViewport.h b/dom/vr/XRViewport.h new file mode 100644 index 0000000000..5b36a0e8ed --- /dev/null +++ b/dom/vr/XRViewport.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XRViewport_h_ +#define mozilla_dom_XRViewport_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/WebXRBinding.h" +#include "mozilla/gfx/Rect.h" + +#include "gfxVR.h" + +namespace mozilla::dom { + +class XRViewport final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(XRViewport) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(XRViewport) + + explicit XRViewport(nsISupports* aParent, const gfx::IntRect& aRect); + + // WebIDL Boilerplate + nsISupports* GetParentObject() const { return mParent; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Members + int32_t X(); + int32_t Y(); + int32_t Width(); + int32_t Height(); + + protected: + virtual ~XRViewport() = default; + + nsCOMPtr<nsISupports> mParent; + + public: + gfx::IntRect mRect; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XRViewport_h_ diff --git a/dom/vr/moz.build b/dom/vr/moz.build new file mode 100644 index 0000000000..f0c6fc70c0 --- /dev/null +++ b/dom/vr/moz.build @@ -0,0 +1,67 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "WebVR") + +EXPORTS.mozilla.dom += [ + "VRDisplay.h", + "VRDisplayEvent.h", + "VREventObserver.h", + "VRServiceTest.h", + "XRBoundedReferenceSpace.h", + "XRFrame.h", + "XRInputSource.h", + "XRInputSourceArray.h", + "XRNativeOrigin.h", + "XRPermissionRequest.h", + "XRPose.h", + "XRReferenceSpace.h", + "XRRenderState.h", + "XRRigidTransform.h", + "XRSession.h", + "XRSpace.h", + "XRSystem.h", + "XRView.h", + "XRViewerPose.h", + "XRViewport.h", +] + +UNIFIED_SOURCES = [ + "VRDisplay.cpp", + "VRDisplayEvent.cpp", + "VREventObserver.cpp", + "VRServiceTest.cpp", + "XRBoundedReferenceSpace.cpp", + "XRFrame.cpp", + "XRInputSource.cpp", + "XRInputSourceArray.cpp", + "XRInputSpace.cpp", + "XRNativeOriginFixed.cpp", + "XRNativeOriginLocal.cpp", + "XRNativeOriginLocalFloor.cpp", + "XRNativeOriginTracker.cpp", + "XRNativeOriginViewer.cpp", + "XRPermissionRequest.cpp", + "XRPose.cpp", + "XRReferenceSpace.cpp", + "XRRenderState.cpp", + "XRRigidTransform.cpp", + "XRSession.cpp", + "XRSpace.cpp", + "XRSystem.cpp", + "XRView.cpp", + "XRViewerPose.cpp", + "XRViewport.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += ["/dom/base"] + +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] +REFTEST_MANIFESTS += ["test/reftest/reftest.list"] diff --git a/dom/vr/test/crashtests/crashtests.list b/dom/vr/test/crashtests/crashtests.list new file mode 100644 index 0000000000..8e40e0ccce --- /dev/null +++ b/dom/vr/test/crashtests/crashtests.list @@ -0,0 +1 @@ +pref(dom.vr.enabled,true) pref(dom.vr.always_support_vr,true) load enumerate_vr_on_dying_window.html
diff --git a/dom/vr/test/crashtests/enumerate_vr_on_dying_window.html b/dom/vr/test/crashtests/enumerate_vr_on_dying_window.html new file mode 100644 index 0000000000..2906faa79c --- /dev/null +++ b/dom/vr/test/crashtests/enumerate_vr_on_dying_window.html @@ -0,0 +1,14 @@ +<html> +<head> +<script> +window.onload = function(){ + var frame = document.getElementById('test_iframe'); + var win = frame.contentWindow; + frame.remove(); + win.onvrdisplayactivate = function () {} +}; +</script></head> +<body> + <iframe id="test_iframe"></iframe> +</body> +</html> diff --git a/dom/vr/test/mochitest/VRSimulationDriver.js b/dom/vr/test/mochitest/VRSimulationDriver.js new file mode 100644 index 0000000000..5b197b88e4 --- /dev/null +++ b/dom/vr/test/mochitest/VRSimulationDriver.js @@ -0,0 +1,95 @@ +var VRServiceTest; +var vrMockDisplay; + +var VRSimulationDriver = (function () { + "use strict"; + + var AttachWebVRDisplay = function () { + if (vrMockDisplay) { + // Avoid creating multiple displays + return Promise.resolve(vrMockDisplay); + } + var promise = VRServiceTest.attachVRDisplay("VRDisplayTest"); + promise.then(function (display) { + assert_true(display != null, "AttachWebVRDisplay should success."); + vrMockDisplay = display; + }); + + return promise; + }; + + var SetVRDisplayPose = function ( + position, + linearVelocity, + linearAcceleration, + orientation, + angularVelocity, + angularAcceleration + ) { + vrMockDisplay.setPose( + position, + linearVelocity, + linearAcceleration, + orientation, + angularVelocity, + angularAcceleration + ); + }; + + var SetEyeResolution = function (width, height) { + vrMockDisplay.setEyeResolution(width, height); + }; + + var SetEyeParameter = function ( + eye, + offsetX, + offsetY, + offsetZ, + upDegree, + rightDegree, + downDegree, + leftDegree + ) { + vrMockDisplay.setEyeParameter( + eye, + offsetX, + offsetY, + offsetZ, + upDegree, + rightDegree, + downDegree, + leftDegree + ); + }; + + var SetMountState = function (isMounted) { + vrMockDisplay.setMountState(isMounted); + }; + + var UpdateVRDisplay = function () { + vrMockDisplay.update(); + }; + + var AttachVRController = function () { + var promise = VRServiceTest.attachVRController("VRControllerTest"); + promise.then(function (controller) { + assert_true(controller != null, "AttachVRController should success."); + }); + + return promise; + }; + + var API = { + AttachWebVRDisplay, + SetVRDisplayPose, + SetEyeResolution, + SetEyeParameter, + SetMountState, + UpdateVRDisplay, + AttachVRController, + + none: false, + }; + + return API; +})(); diff --git a/dom/vr/test/mochitest/WebVRHelpers.js b/dom/vr/test/mochitest/WebVRHelpers.js new file mode 100644 index 0000000000..e19294a236 --- /dev/null +++ b/dom/vr/test/mochitest/WebVRHelpers.js @@ -0,0 +1,19 @@ +var WebVRHelpers = (function () { + "use strict"; + + var RequestPresentOnVRDisplay = function (vrDisplay, vrLayers, callback) { + if (callback) { + callback(); + } + + return vrDisplay.requestPresent(vrLayers); + }; + + var API = { + RequestPresentOnVRDisplay, + + none: false, + }; + + return API; +})(); diff --git a/dom/vr/test/mochitest/mochitest.ini b/dom/vr/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..87a4174a21 --- /dev/null +++ b/dom/vr/test/mochitest/mochitest.ini @@ -0,0 +1,31 @@ +# Please confirm there is no other VR display connected. Otherwise, VRPuppetDisplay can't be attached. +[DEFAULT] +support-files = + VRSimulationDriver.js + requestPresent.js + runVRTest.js + WebVRHelpers.js +[test_vrController_displayId.html] +# Enable Linux after Bug 1310655 # TIMED_OUT for Android. +# skip-if = (os != "win" && release_or_beta) || (os == "android") +# Dependencies for re-enabling these are tracked by meta bug 1555185. +skip-if = true +[test_vrDisplay_canvas2d.html] +# skip-if = (os != "win" && release_or_beta) # Enable Linux after Bug 1310655 +# Dependencies for re-enabling these are tracked by meta bug 1555185. +skip-if = true +[test_vrDisplay_exitPresent.html] +# skip-if = (os != "win" && release_or_beta) # Enable Linux after Bug 1310655 +# Dependencies for re-enabling these are tracked by meta bug 1555185. +skip-if = true +[test_vrDisplay_getFrameData.html] +# Enable Linux after Bug 1310655, enable Android after Bug 1348246 +# skip-if = (os != "win" && release_or_beta) || (os == "android") +# Dependencies for re-enabling these are tracked by meta bug 1555185. +skip-if = true +[test_vrDisplay_onvrdisplayconnect.html] +skip-if = true +[test_vrDisplay_onvrdisplaydeactivate_crosscontent.html] +skip-if = true +[test_vrDisplay_requestPresent.html] +skip-if = true diff --git a/dom/vr/test/mochitest/requestPresent.js b/dom/vr/test/mochitest/requestPresent.js new file mode 100644 index 0000000000..a2f9dd4d11 --- /dev/null +++ b/dom/vr/test/mochitest/requestPresent.js @@ -0,0 +1,74 @@ +// requestPresent.js +// +// This file provides helpers for testing VRDisplay requestPresent. + +function attachVRDisplay(test) { + assert_equals( + typeof navigator.getVRDisplays, + "function", + "'navigator.getVRDisplays()' must be defined." + ); + return VRSimulationDriver.AttachWebVRDisplay(); +} + +function setupVRDisplay(test) { + assert_equals( + typeof navigator.getVRDisplays, + "function", + "'navigator.getVRDisplays()' must be defined." + ); + return VRSimulationDriver.AttachWebVRDisplay() + .then(() => { + return navigator.getVRDisplays(); + }) + .then(displays => { + assert_equals( + displays.length, + 1, + "displays.length must be one after attach." + ); + vrDisplay = displays[0]; + return validateNewVRDisplay(test, vrDisplay); + }); +} + +// Validate the settings off a freshly created VRDisplay (prior to calling +// requestPresent). +function validateNewVRDisplay(test, display) { + assert_true( + display.capabilities.canPresent, + "display.capabilities.canPresent must always be true for HMDs." + ); + assert_equals( + display.capabilities.maxLayers, + 1, + "display.capabilities.maxLayers must always be 1 when display.capabilities.canPresent is true for current spec revision." + ); + assert_false( + display.isPresenting, + "display.isPresenting must be false before calling requestPresent." + ); + assert_equals( + display.getLayers().length, + 0, + "display.getLayers() should have no layers if not presenting." + ); + var promise = display.exitPresent(); + return promise_rejects(test, null, promise); +} + +// Validate the settings off a VRDisplay after requestPresent promise is +// rejected or after exitPresent is fulfilled. +function validateDisplayNotPresenting(test, display) { + assert_false( + display.isPresenting, + "display.isPresenting must be false if requestPresent is rejected or after exitPresent is fulfilled." + ); + assert_equals( + display.getLayers().length, + 0, + "display.getLayers() should have no layers if requestPresent is rejected or after exitPresent is fulfilled." + ); + var promise = display.exitPresent(); + return promise_rejects(test, null, promise); +} diff --git a/dom/vr/test/mochitest/runVRTest.js b/dom/vr/test/mochitest/runVRTest.js new file mode 100644 index 0000000000..c15003a6c3 --- /dev/null +++ b/dom/vr/test/mochitest/runVRTest.js @@ -0,0 +1,18 @@ +function runVRTest(callback) { + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.vr.enabled", true], + ["dom.vr.puppet.enabled", true], + ["dom.vr.require-gesture", false], + ["dom.vr.test.enabled", true], + ["dom.vr.display.enumerate.interval", 0], + ["dom.vr.controller.enumerate.interval", 0], + ], + }, + () => { + VRServiceTest = navigator.requestVRServiceTest(); + callback(); + } + ); +} diff --git a/dom/vr/test/mochitest/test_vrController_displayId.html b/dom/vr/test/mochitest/test_vrController_displayId.html new file mode 100644 index 0000000000..e69e3920d2 --- /dev/null +++ b/dom/vr/test/mochitest/test_vrController_displayId.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html> + <head> + <title>VRController DisplayId</title> + <meta name="timeout" content="long"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="VRSimulationDriver.js"></script> + <script src="runVRTest.js"></script> + </head> + <body> + <script> + "use strict"; + var vrDisplay; + var controllerCount = 0; + + function addController() { + promise_test((test) => { + return VRSimulationDriver.AttachVRController().then((controller) => { + controller.newButtonEvent(0, true); + }); + }, "Finish to add VRController."); + } + + function listenControllerEvents() { + async_test(function(t) { + window.addEventListener("gamepadbuttondown", function(e) { + assert_equals(e.gamepad.displayId, vrDisplay.displayId, "gamepad.displayId should be equal to vrDisplay."); + assert_equals(e.gamepad.id, "Puppet Gamepad", "gamepad.id must be equal to 'Puppet Gamepad'."); + ++controllerCount; + if (controllerCount == 2) { + t.done(); + } + }); + }, "Finish to verify VRController.displayId."); + } + + function startTest() { + promise_test((test) => { + listenControllerEvents(); + return VRSimulationDriver.AttachWebVRDisplay().then(() => { + return navigator.getVRDisplays().then((displays) => { + vrDisplay = displays[0]; + assert_equals(displays.length, 1, "displays.length must be one after attach."); + assert_equals(vrDisplay.displayName, "Puppet HMD", "display.displayName must be equal to 'Puppet HMD'."); + addController(); + addController(); + }); + }); + }, "Finish to add VRDisplay."); + } + + runVRTest(startTest); + </script> + </body> +</html> diff --git a/dom/vr/test/mochitest/test_vrDisplay_canvas2d.html b/dom/vr/test/mochitest/test_vrDisplay_canvas2d.html new file mode 100644 index 0000000000..4d20e21352 --- /dev/null +++ b/dom/vr/test/mochitest/test_vrDisplay_canvas2d.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> + <head> + <title>VRDisplay Canvas2D</title> + <meta name="timeout" content="long"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="VRSimulationDriver.js"></script> + <script src="runVRTest.js"></script> + </head> + <body> + <script> + "use strict"; + var vrDisplay; + + function requestPresentTest() { + async_test(function (test) { + vrDisplay.requestAnimationFrame(callback); + + function callback() { + vrDisplay.resetPose(); + vrDisplay.getLayers(); + vrDisplay.submitFrame(); + vrDisplay.getEyeParameters("right"); + test.done(); + } + }, "Finish WebVR Canvas2D requestPresentTest."); + } + + function startTest() { + promise_test((test) => { + var canvas = document.createElement('canvas'); + (document.body || document.documentElement).appendChild(canvas); + var context = canvas.getContext('2d'); + var img = document.createElement('img'); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACw="; + + return VRSimulationDriver.AttachWebVRDisplay().then(() => { + return navigator.getVRDisplays().then((displays) => { + assert_equals(displays.length, 1, "displays.length must be one after attach."); + vrDisplay = displays[0]; + var frameData = new VRFrameData(); + return vrDisplay.requestPresent([{source: canvas}]).then(() => { + requestPresentTest(); + }); + }); + }); + }, "Finish running WebVR Canvas2D test."); + } + + runVRTest(startTest); + </script> + </body> +</html> diff --git a/dom/vr/test/mochitest/test_vrDisplay_exitPresent.html b/dom/vr/test/mochitest/test_vrDisplay_exitPresent.html new file mode 100644 index 0000000000..dc2c700723 --- /dev/null +++ b/dom/vr/test/mochitest/test_vrDisplay_exitPresent.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<html> + <head> + <title>VRDisplay ExitPresent</title> + <meta name="timeout" content="long"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="VRSimulationDriver.js"></script> + <script src="runVRTest.js"></script> + </head> + <body> + <script> + function testExitPresentOnOtherIframe(content) { + return content.navigator.getVRDisplays().then((displays) => { + content.vrDisplay = displays[0]; + return content.vrDisplay.exitPresent(); + }); + } + var initVRPresentation = function(content) { + return VRSimulationDriver.AttachWebVRDisplay().then(() => { + return content.navigator.getVRDisplays().then((displays) => { + content.vrDisplay = displays[0]; + content.canvas = content.document.createElement("canvas"); + content.canvas.id = "vrCanvas"; + return content.vrDisplay.requestPresent([{source:content.canvas}]); + }); + }); + } + function startTest() { + var ifr1 = document.getElementById("iframe1"); + var ifr2 = document.getElementById("iframe2"); + var frame1 = ifr1.contentWindow; + var frame2 = ifr2.contentWindow; + promise_test((test) => { + return VRSimulationDriver.AttachWebVRDisplay().then(() => { + return initVRPresentation(frame1).then(() => { + promise_test((test) => { + return promise_rejects(test, null, testExitPresentOnOtherIframe(frame2)); + }, "We cannot exit VR presentation established by another content, this promise is expected to be rejected.") + }); + }); + }, "Finish running WebVR exitPresent test."); + } + runVRTest(startTest); + </script> + + <iframe id="iframe1"></iframe> + <iframe id="iframe2"></iframe> + </body> +</html> diff --git a/dom/vr/test/mochitest/test_vrDisplay_getFrameData.html b/dom/vr/test/mochitest/test_vrDisplay_getFrameData.html new file mode 100644 index 0000000000..c8a986ae8e --- /dev/null +++ b/dom/vr/test/mochitest/test_vrDisplay_getFrameData.html @@ -0,0 +1,148 @@ +<!DOCTYPE html> +<html> +<head> + <title>VRDisplay GetFrameData</title> + <meta name="timeout" content="long"/> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="VRSimulationDriver.js"></script> + <script src="WebVRHelpers.js"></script> + <script src="requestPresent.js"></script> + <script src="runVRTest.js"></script> +</head> +<body id="body"> + <canvas id="webglCanvas"></canvas> + <script> + "use strict"; + var vrDisplay; + var vrRAF; + var canvas = document.getElementById('webglCanvas'); + function startTest() { + promise_test((test) => { + return attachVRDisplay(test).then(() => { + VRSimulationDriver.SetEyeResolution(1332, 1586); + VRSimulationDriver.SetEyeParameter("left", -0.029, 0, 0, 41.65, 35.57, 48.00, 43.97); + VRSimulationDriver.SetEyeParameter("right", 0.029, 0, 0, 41.65, 43.97, 48.00, 35.57); + var poseOrient = new Float32Array([-0.188, -0.007, 0.045, -0.980]); + var posePos = new Float32Array([-0.161, 0.076, -0.250]); + var poseAngVel = new Float32Array([0.008, -0.002, -0.006]); + var poseAngAcc = new Float32Array([3.404, -1.469, -5.901]); + var poseLinVel = new Float32Array([0.001, -0.003, -0.002]); + var poseLinAcc = new Float32Array([0.007, 0.068, -0.052]); + VRSimulationDriver.SetVRDisplayPose(posePos, poseLinVel, poseLinAcc, + poseOrient, poseAngVel, poseAngAcc); + VRSimulationDriver.UpdateVRDisplay(); + }).then(() => { + return promise_test((test) => { + return setupVRDisplay(test).then(() => { + return WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, + [{ source: canvas }]); + }).then(() => { + assert_true(vrDisplay.isPresenting, "vrDisplay.isPresenting must be true if requestPresent is fulfilled."); + assert_equals(vrDisplay.getLayers().length, 1, "vrDisplay.getLayers() should return one layer."); + + verifyFrameData(); + }) + }, "WebVR requestPresent fulfilled"); + }) + }, "Finish setting up VR test data."); + + function verifyFrameData() { + async_test(function (test) { + navigator.getVRDisplays().then((displays) => { + assert_equals(displays.length, 1, "displays.length must be one after attach."); + vrDisplay = displays[0]; + vrDisplay.requestAnimationFrame(callback); + + function callback() { + var frameData1 = new VRFrameData(); + vrDisplay.getFrameData(frameData1); + + // We insert a new frame to confirm we still can get + // the same data as the last getter. + insertNewFrameData(); + + var frameData2 = new VRFrameData(); + vrDisplay.getFrameData(frameData2); + + assert_equals(frameData1.timestamp, frameData2.timestamp, + "frameData.timestamp at a frame should be equal."); + + assert_true(checkValueInFloat32Array(frameData1.leftProjectionMatrix, + frameData2.leftProjectionMatrix), + "frameData.leftProjectionMatrix at a frame should be equal."); + + assert_true(checkValueInFloat32Array(frameData1.leftViewMatrix, + frameData2.leftViewMatrix), + "frameData.leftViewMatrix at a frame should be equal."); + + assert_true(checkValueInFloat32Array(frameData1.rightProjectionMatrix, + frameData2.rightProjectionMatrix), + "frameData.rightProjectionMatrix at a frame should be equal."); + + assert_true(checkValueInFloat32Array(frameData1.rightViewMatrix, + frameData2.rightViewMatrix), + "frameData.rightViewMatrix at a frame should be equal."); + + var pose1 = frameData1.pose; + var pose2 = frameData2.pose; + assert_true(checkValueInFloat32Array(pose1.position, + pose2.position), + "pose.position at a frame should be equal."); + + assert_true(checkValueInFloat32Array(pose1.linearVelocity, + pose2.linearVelocity), + "pose.linearVelocity at a frame should be equal."); + + assert_true(checkValueInFloat32Array(pose1.linearAcceleration, + pose2.linearAcceleration), + "pose.linearAcceleration at a frame should be equal."); + + assert_true(checkValueInFloat32Array(pose1.orientation, + pose2.orientation), + "pose.orientation at a frame should be equal."); + + assert_true(checkValueInFloat32Array(pose1.angularVelocity, + pose2.angularVelocity), + "pose.angularVelocity at a frame should be equal."); + + assert_true(checkValueInFloat32Array(pose1.angularAcceleration, + pose2.angularAcceleration), + "pose.angularAcceleration at a frame should be equal."); + test.done(); + } + }); + }, "WebVR returns the same frameData within a frame fulfilled"); + } + + function insertNewFrameData() { + var poseOrient = new Float32Array([-0.208, -0.017, 0.055, -0.930]); + var posePos = new Float32Array([-0.261, 0.036, -0.150]); + var poseAngVel = new Float32Array([0.018, -0.001, -0.003]); + var poseAngAcc = new Float32Array([1.504, -1.339, -4.901]); + var poseLinVel = new Float32Array([0.002, -0.001, -0.003]); + var poseLinAcc = new Float32Array([0.017, 0.061, -0.022]); + VRSimulationDriver.SetVRDisplayPose(posePos, poseLinVel, poseLinAcc, + poseOrient, poseAngVel, poseAngAcc); + VRSimulationDriver.UpdateVRDisplay(); + } + + function checkValueInFloat32Array(array1, array2) { + if (array1.length != array2.length) { + return false; + } + var index = 0; + while (index < array2.length) { + if (array1[index] != array2[index]) { + return false; + } + ++index; + } + return true; + } + } + + runVRTest(startTest); + </script> +</body> +</html> diff --git a/dom/vr/test/mochitest/test_vrDisplay_onvrdisplayconnect.html b/dom/vr/test/mochitest/test_vrDisplay_onvrdisplayconnect.html new file mode 100644 index 0000000000..e1912c7c41 --- /dev/null +++ b/dom/vr/test/mochitest/test_vrDisplay_onvrdisplayconnect.html @@ -0,0 +1,43 @@ +<html> + <head> + <title>VRDisplay onvrdisplayconnect test</title> + <meta name="timeout" content="long"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="VRSimulationDriver.js"></script> + <script src="WebVRHelpers.js"></script> + <script src="requestPresent.js"></script> + <script src="runVRTest.js"></script> + </head> + <body> + <script> + + function eventAfterConnectedTest() { + async_test(function (test) { + window.addEventListener("vrdisplayconnect", () => { + test.done(); + }); + }, "vrdisplayconnect should fire as soon as content listens for it, \ + even if the VR display was already connected."); + } + + function startTest() { + promise_test((test) => { + return attachVRDisplay(test).then(() => { + return promise_test((test) => { + return setupVRDisplay(test).then(() => { + VRSimulationDriver.UpdateVRDisplay(); + eventAfterConnectedTest(); + VRSimulationDriver.UpdateVRDisplay(); + }); + }); + }); + }); + } + + runVRTest(startTest); + </script> + <iframe id="iframe1"></iframe> + </body> +</html> diff --git a/dom/vr/test/mochitest/test_vrDisplay_onvrdisplaydeactivate_crosscontent.html b/dom/vr/test/mochitest/test_vrDisplay_onvrdisplaydeactivate_crosscontent.html new file mode 100644 index 0000000000..6c58e5efd1 --- /dev/null +++ b/dom/vr/test/mochitest/test_vrDisplay_onvrdisplaydeactivate_crosscontent.html @@ -0,0 +1,54 @@ +<html> + <head> + <title>VRDisplay onvrdisplaydeactivate Crosscontent test</title> + <meta name="timeout" content="long"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="VRSimulationDriver.js"></script> + <script src="WebVRHelpers.js"></script> + <script src="requestPresent.js"></script> + <script src="runVRTest.js"></script> + </head> + <body> + <canvas id="vrCanvas"></canvas> + <script> + + function startTest() { + var canvas = document.getElementById("vrCanvas"); + var iframe1 = document.getElementById("iframe1").contentWindow; + var t = async_test("vrdisplaydeactivate crosscontent test"); + + window.addEventListener("vrdisplaydeactivate", () => { + t.step(() => { + assert_true(vrDisplay.isPresenting, + "VRDisplay should be still presenting now without being affected by the event."); + t.done(); + }); + }); + + iframe1.addEventListener("vrdisplaydeactivate", () => { + t.unreached_func("vrdisplaydeactivate should not be received by other iframe."); + }); + + promise_test((test) => { + return attachVRDisplay(test).then(() => { + return promise_test((test) => { + return setupVRDisplay(test).then(() => { + VRSimulationDriver.SetMountState(true); + VRSimulationDriver.UpdateVRDisplay(); + return WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{source: canvas}]); + }).then(() => { + VRSimulationDriver.SetMountState(false); + VRSimulationDriver.UpdateVRDisplay(); + }); + }); + }); + }); + } + + runVRTest(startTest); + </script> + <iframe id="iframe1"></iframe> + </body> +</html> diff --git a/dom/vr/test/mochitest/test_vrDisplay_requestPresent.html b/dom/vr/test/mochitest/test_vrDisplay_requestPresent.html new file mode 100644 index 0000000000..4d021b6e0f --- /dev/null +++ b/dom/vr/test/mochitest/test_vrDisplay_requestPresent.html @@ -0,0 +1,130 @@ +<!DOCTYPE html> +<html> +<head> + <title>VRDisplay RequestPresent</title> + <meta name="timeout" content="long"/> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="VRSimulationDriver.js"></script> + <script src="WebVRHelpers.js"></script> + <script src="requestPresent.js"></script> + <script src="runVRTest.js"></script> +</head> +<body id="body"> + <canvas id="webglCanvas"></canvas> + <div id="testDiv"></div> + <script> + "use strict"; + var vrDisplay; + var canvas = document.getElementById('webglCanvas'); + var div = document.getElementById('testDiv'); + function startTest() { + promise_test((test) => { + return setupVRDisplay(test).then(() => { + return promise_rejects(test, null, WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{}])); + }).then(() => { + return validateDisplayNotPresenting(test, vrDisplay); + }); + }, "WebVR requestPresent rejected with empty frames"); + + promise_test((test) => { + return setupVRDisplay(test).then(() => { + return promise_rejects(test, null, WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{ source: canvas, leftBounds: [0.0, 0.0] }])); + }).then(() => { + return validateDisplayNotPresenting(test, vrDisplay); + }); + }, "WebVR requestPresent rejected with incorrect bounds (bounds arrays must be 0 or 4 long)"); + + promise_test((test) => { + return setupVRDisplay(test).then(() => { + return promise_rejects(test, null, WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{ source: div }])); + }).then(() => { + return validateDisplayNotPresenting(test, vrDisplay); + }); + }, "WebVR requestPresent rejected with invalid source (must be canvas element)"); + + promise_test((test) => { + return setupVRDisplay(test).then(() => { + return promise_rejects(test, null, WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{ source: canvas, leftBounds: [div] }])); + }).then(() => { + return validateDisplayNotPresenting(test, vrDisplay); + }); + }, "WebVR requestPresent rejected with invalid bounds data type (must be able to convert to float)"); + + const invalidBounds = [ + [2.0, 0.0, 0.0, 0.0], + [0.0, 2.0, 0.0, 0.0], + [0.0, 0.0, 2.0, 0.0], + [0.0, 0.0, 0.0, 2.0], + [-1.0, 0.0, 0.0, 0.0], + [0.0, -1.0, 0.0, 0.0], + [0.0, 0.0, -1.0, 0.0], + [0.0, 0.0, 0.0, -1.0]]; + + invalidBounds.forEach((bound) => { + promise_test((test) => { + return setupVRDisplay(test).then(() => { + return promise_rejects(test, null, WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{ source: canvas, leftBounds: bound }])); + }).then(() => { + return validateDisplayNotPresenting(test, vrDisplay); + }); + }, "WebVR requestPresent rejected with bounds in invalid range: [" + bound + "]"); + }); + + promise_test((test) => { + return setupVRDisplay(test).then(() => { + var promise = vrDisplay.requestPresent({ source: canvas }); + return promise_rejects(test, null, promise); + }).then(() => { + return validateDisplayNotPresenting(test, vrDisplay); + }); + }, "WebVR requestPresent rejected without user initiated action"); + + promise_test((test) => { + return setupVRDisplay(test).then(() => { + return promise_rejects(test, null, WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{ source: canvas }, { source: canvas }])); + }).then(() => { + return validateDisplayNotPresenting(test, vrDisplay); + }); + }, "WebVR requestPresent rejected with more frames than max layers"); + + promise_test((test) => { + return setupVRDisplay(test).then(() => { + function requestAgain() { + // Callback for immediate requestPresent call for further testing. + // Cache this promise on global object since it seems to be the only object + // in scope across calls. + window.promiseSecond = vrDisplay.requestPresent([{ source: canvas }]); + } + return WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{ source: canvas }], requestAgain); + }).then(() => { + // First promise succeeded + assert_true(vrDisplay.isPresenting, "First promise should successfully fulfill"); + // Now, validate that the subsequent requestPresent was rejected + return promise_rejects(test, null, window.promiseSecond); + }).then(() => { + delete window.promiseSecond; + assert_true(vrDisplay.isPresenting, "Should still be presenting after rejected second promise"); + return vrDisplay.exitPresent(); + }); + }, "WebVR requestPresent fails while another one is in progress"); + + promise_test((test) => { + return setupVRDisplay(test).then(() => { + return WebVRHelpers.RequestPresentOnVRDisplay(vrDisplay, [{ source: canvas }]); + }).then(() => { + assert_true(vrDisplay.isPresenting, "vrDisplay.isPresenting must be true if requestPresent is fulfilled."); + assert_equals(vrDisplay.getLayers().length, 1, "vrDisplay.getLayers() should return one layer."); + return vrDisplay.exitPresent(); + }).then(() => { + assert_false(vrDisplay.isPresenting, "vrDisplay.isPresenting must be false if exitPresent is fulfilled."); + // exitPresent() should reject since we are no longer presenting. + return promise_rejects(test, null, vrDisplay.exitPresent()); + }); + }, "WebVR requestPresent fulfilled"); + } + + runVRTest(startTest); + </script> +</body> +</html> diff --git a/dom/vr/test/reftest/VRSimulationDriver.js b/dom/vr/test/reftest/VRSimulationDriver.js new file mode 100644 index 0000000000..971cdb8626 --- /dev/null +++ b/dom/vr/test/reftest/VRSimulationDriver.js @@ -0,0 +1,60 @@ + +var VRServiceTest; +var vrMockDisplay; + +var VRSimulationDriver = (function() { +"use strict"; + +var AttachWebVRDisplay = function() { + if (vrMockDisplay) { + // Avoid creating multiple displays + return Promise.resolve(vrMockDisplay); + } + var promise = VRServiceTest.attachVRDisplay("VRDisplayTest"); + promise.then(function (display) { + vrMockDisplay = display; + }); + + return promise; +}; + +var SetVRDisplayPose = function(position, + linearVelocity, linearAcceleration, + orientation, angularVelocity, + angularAcceleration) { + vrMockDisplay.setPose(position, linearVelocity, linearAcceleration, + orientation, angularVelocity, angularAcceleration); +}; + +var SetEyeResolution = function(width, height) { + vrMockDisplay.setEyeResolution(width, height); +} + +var SetEyeParameter = function(eye, offsetX, offsetY, offsetZ, + upDegree, rightDegree, downDegree, leftDegree) { + vrMockDisplay.setEyeParameter(eye, offsetX, offsetY, offsetZ, upDegree, rightDegree, + downDegree, leftDegree); +} + +var SetMountState = function(isMounted) { + vrMockDisplay.setMountState(isMounted); +} + +var UpdateVRDisplay = function() { + vrMockDisplay.update(); +} + +var API = { + AttachWebVRDisplay: AttachWebVRDisplay, + SetVRDisplayPose: SetVRDisplayPose, + SetEyeResolution: SetEyeResolution, + SetEyeParameter: SetEyeParameter, + SetMountState: SetMountState, + UpdateVRDisplay: UpdateVRDisplay, + + none: false +}; + +return API; + +}());
\ No newline at end of file diff --git a/dom/vr/test/reftest/change_size.html b/dom/vr/test/reftest/change_size.html new file mode 100644 index 0000000000..87d59f6a3c --- /dev/null +++ b/dom/vr/test/reftest/change_size.html @@ -0,0 +1,168 @@ +<!DOCTYPE html> +<meta charset='UTF-8'> +<!-- Viewport size change in WebGL and submit it to the VR device as a base64 image. +If this fails, something is seriously wrong. --> +<html class="reftest-wait"> +<head> + <script type='text/javascript' src='webgl-util.js'></script> + <script type='text/javascript' src="VRSimulationDriver.js"></script> + <script id="vs" type="x-shader/x-vertex"> + attribute vec2 aVertCoord; + + void main(void) { + gl_Position = vec4(aVertCoord, 0.0, 1.0); + } + </script> + <script id="fs" type="x-shader/x-fragment"> + precision mediump float; + + void main(void) { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + </script> + <script type='text/javascript'> + 'use strict'; + + var submitResult = null; + var vrDisplay = null; + var webglCanvas = null; + var gl = null; + var prog = null; + var img = null; + // The resolution is 540 : 300 (the ratio of 2160 * 1200, like Vive and Oculus) + const eyeWidth = 270; + const eyeHeight = 300; + + function setStatus(text) { + var elem = document.getElementById('status'); + elem.innerHTML = text; + } + + function initVRMock() { + VRServiceTest = navigator.requestVRServiceTest(); + if (!VRServiceTest) { + setStatus('VRServiceTest get failed.'); + return; + } + + VRSimulationDriver.AttachWebVRDisplay().then(() => { + VRSimulationDriver.SetEyeResolution(eyeWidth, eyeHeight); + VRSimulationDriver.UpdateVRDisplay(); + }).then(() => { + // Looking for VR displays + if (navigator.getVRDisplays) { + submitResult = new VRSubmitFrameResult(); + navigator.getVRDisplays().then(function (displays) { + if (displays.length > 0) { + window.addEventListener('vrdisplaypresentchange', onVRPresentChange, false); + + vrDisplay = displays[0]; + vrDisplay.requestPresent([{ source: webglCanvas }]); + vrDisplay.requestAnimationFrame(onAnimationFrame); + } + }); + } + }); + } + + function onVRPresentChange() { + if (vrDisplay && vrDisplay.isPresenting) { + const leftEye = vrDisplay.getEyeParameters("left"); + const rightEye = vrDisplay.getEyeParameters("right"); + + if (leftEye.renderWidth != rightEye.renderWidth || + leftEye.renderWidth != eyeWidth) { + setStatus('renderWidth is not equal to eyeWidth.'); + } + + if (leftEye.renderHeight != rightEye.renderHeight || + leftEye.renderHeight != eyeHeight) { + setStatus('renderHeight is not equal to eyeHeight.'); + } + webglCanvas.width = leftEye.renderWidth * 2; + webglCanvas.height = leftEye.renderHeight; + } + } + + function onAnimationFrame() { + if (!vrDisplay.isPresenting) { + return; + } + + gl.clearColor(0.0, 1.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Presenting render a stereo view. + gl.viewport(0, 0, webglCanvas.width * 0.5, webglCanvas.height); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + gl.viewport(webglCanvas.width * 0.5, 0, webglCanvas.width * 0.5, webglCanvas.height); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + // Indicate VRDisplay we're done rendering. + vrDisplay.submitFrame(); + if (vrDisplay.getSubmitFrameResult(submitResult)) { + if (!img) { + img = document.createElement("img"); + img.onload = function(){ + // img width will not be eyeWidth * 2 (540), it would + // be 544. It is because D3D11 CopyResource changes + // the destination image size. + if ((img.height == eyeHeight)) { + webglCanvas.style.display = 'none'; + vrDisplay.exitPresent(); + setTimeout(testComplete, 0); + } + }; + img.src = submitResult.base64Image; + document.body.appendChild(img); + } else { + img.src = submitResult.base64Image; + } + } + vrDisplay.requestAnimationFrame(onAnimationFrame); + } + + function runTest() { + webglCanvas = document.getElementById('canvas'); + gl = webglCanvas.getContext('webgl'); + if (!gl) { + setStatus('WebGL context creation failed.'); + return; + } + gl.disable(gl.DEPTH_TEST); + prog = WebGLUtil.createProgramByIds(gl, 'vs', 'fs'); + if (!prog) { + setStatus('Program linking failed.'); + return; + } + prog.aVertCoord = gl.getAttribLocation(prog, "aVertCoord"); + + var vertCoordArr = new Float32Array([ + -0.5, -0.5, + 0.5, -0.5, + -0.5, 0.5, + 0.5, 0.5, + ]); + var vertCoordBuff = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertCoordBuff); + gl.bufferData(gl.ARRAY_BUFFER, vertCoordArr, gl.STATIC_DRAW); + gl.useProgram(prog); + gl.enableVertexAttribArray(prog.aVertCoord); + gl.vertexAttribPointer(prog.aVertCoord, 2, gl.FLOAT, false, 0, 0); + + initVRMock(); + } + + function testComplete() { + document.documentElement.removeAttribute("class"); + } + </script> +</head> + +<body onload='runTest();'> + <canvas id='canvas' width='128' height='128'></canvas> + <div id='status'></div> +</body> + +</html> diff --git a/dom/vr/test/reftest/change_size.png b/dom/vr/test/reftest/change_size.png Binary files differnew file mode 100644 index 0000000000..fe03114b20 --- /dev/null +++ b/dom/vr/test/reftest/change_size.png diff --git a/dom/vr/test/reftest/draw_rect.html b/dom/vr/test/reftest/draw_rect.html new file mode 100644 index 0000000000..acb8580c1b --- /dev/null +++ b/dom/vr/test/reftest/draw_rect.html @@ -0,0 +1,136 @@ +<!DOCTYPE html> +<meta charset='UTF-8'> +<!-- Draw rect in WebGL and submit it to the VR device as a base64 image. +If this fails, something is seriously wrong. --> +<html class="reftest-wait"> +<head> + <script type='text/javascript' src='webgl-util.js'></script> + <script type='text/javascript' src="VRSimulationDriver.js"></script> + <script id="vs" type="x-shader/x-vertex"> + attribute vec2 aVertCoord; + + void main(void) { + gl_Position = vec4(aVertCoord, 0.0, 1.0); + } + </script> + <script id="fs" type="x-shader/x-fragment"> + precision mediump float; + + void main(void) { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + </script> + <script type='text/javascript'> + 'use strict'; + + var submitResult = null; + var vrDisplay = null; + var webglCanvas = null; + var gl = null; + var prog = null; + var img = null; + + function setStatus(text) { + var elem = document.getElementById('status'); + elem.innerHTML = text; + } + + function initVRMock() { + VRServiceTest = navigator.requestVRServiceTest(); + if (!VRServiceTest) { + setStatus('VRServiceTest get failed.'); + return; + } + + VRSimulationDriver.AttachWebVRDisplay().then(() => { + // Looking for VR displays + if (navigator.getVRDisplays) { + submitResult = new VRSubmitFrameResult(); + navigator.getVRDisplays().then(function (displays) { + if (displays.length > 0) { + vrDisplay = displays[0]; + vrDisplay.requestPresent([{ source: webglCanvas }]); + vrDisplay.requestAnimationFrame(onAnimationFrame); + } + }); + } + }); + } + + function onAnimationFrame() { + if (!vrDisplay.isPresenting) { + return; + } + + gl.clearColor(0.0, 1.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Presenting render a stereo view. + gl.viewport(0, 0, webglCanvas.width * 0.5, webglCanvas.height); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + gl.viewport(webglCanvas.width * 0.5, 0, webglCanvas.width * 0.5, webglCanvas.height); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + // Indicate VRDisplay we're done rendering. + vrDisplay.submitFrame(); + if (vrDisplay.getSubmitFrameResult(submitResult)) { + if (!img) { + img = document.createElement("img"); + img.onload = function(){ + webglCanvas.style.display = 'none'; + vrDisplay.exitPresent(); + setTimeout(testComplete, 0); + }; + img.src = submitResult.base64Image; + document.body.appendChild(img); + } else { + img.src = submitResult.base64Image; + } + } + vrDisplay.requestAnimationFrame(onAnimationFrame); + } + + function runTest() { + webglCanvas = document.getElementById('canvas'); + gl = webglCanvas.getContext('webgl'); + if (!gl) { + setStatus('WebGL context creation failed.'); + return; + } + gl.disable(gl.DEPTH_TEST); + prog = WebGLUtil.createProgramByIds(gl, 'vs', 'fs'); + if (!prog) { + setStatus('Program linking failed.'); + return; + } + prog.aVertCoord = gl.getAttribLocation(prog, "aVertCoord"); + + var vertCoordArr = new Float32Array([ + -0.5, -0.5, + 0.5, -0.5, + -0.5, 0.5, + 0.5, 0.5, + ]); + var vertCoordBuff = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertCoordBuff); + gl.bufferData(gl.ARRAY_BUFFER, vertCoordArr, gl.STATIC_DRAW); + gl.useProgram(prog); + gl.enableVertexAttribArray(prog.aVertCoord); + gl.vertexAttribPointer(prog.aVertCoord, 2, gl.FLOAT, false, 0, 0); + + initVRMock(); + } + + function testComplete() { + document.documentElement.removeAttribute("class"); + } + </script> +</head> + +<body onload='runTest();'> + <canvas id='canvas' width='256' height='256'></canvas> + <div id='status'></div> +</body> + +</html> diff --git a/dom/vr/test/reftest/draw_rect.png b/dom/vr/test/reftest/draw_rect.png Binary files differnew file mode 100644 index 0000000000..0f4d24a0d1 --- /dev/null +++ b/dom/vr/test/reftest/draw_rect.png diff --git a/dom/vr/test/reftest/reftest.list b/dom/vr/test/reftest/reftest.list new file mode 100644 index 0000000000..c1ca292c71 --- /dev/null +++ b/dom/vr/test/reftest/reftest.list @@ -0,0 +1,10 @@ +# WebVR Reftests +# Please confirm there is no other VR display connected. Otherwise, VRPuppetDisplay can't be attached. +defaults pref(dom.vr.enabled,true) pref(dom.vr.puppet.enabled,true) pref(dom.vr.test.enabled,true) pref(dom.vr.require-gesture,false) pref(dom.vr.puppet.submitframe,1) pref(dom.vr.display.rafMaxDuration,200) pref(dom.vr.display.enumerate.interval,0) pref(dom.vr.controller.enumerate.interval,0) +# WebVR Tests have been disabled as refactoring of gfxVRPuppet is landing. Dependencies for re-enabling these are tracked by meta bug 1555185. +# VR SubmitFrame is only implemented for D3D11.1 and MacOSX now. +# Our Windows 7 test machines don't support D3D11.1, so we run these tests on Windows 8+ only. +# skip-if((!winWidget&&release_or_beta)||Android||gtkWidget||!layersGPUAccelerated) == draw_rect.html wrapper.html?draw_rect.png +# On MacOSX platform, getting different color interpolation result. +# For lower resolution Mac hardware, we need to adjust it to fuzzy-if(cocoaWidget,0-1,0-1200). +# fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)||cocoaWidget,0-1,0-600) skip-if((!winWidget&&release_or_beta)||Android||gtkWidget||!layersGPUAccelerated) == change_size.html wrapper.html?change_size.png diff --git a/dom/vr/test/reftest/webgl-util.js b/dom/vr/test/reftest/webgl-util.js new file mode 100644 index 0000000000..42f1c5ccd5 --- /dev/null +++ b/dom/vr/test/reftest/webgl-util.js @@ -0,0 +1,61 @@ +WebGLUtil = (function() { + // --------------------------------------------------------------------------- + // WebGL helpers + + // Returns a valid shader, or null on errors. + function createShaderById(gl, id) { + var elem = document.getElementById(id); + if (!elem) { + throw new Error( + "Failed to create shader from non-existent id '" + id + "'." + ); + } + + var src = elem.innerHTML.trim(); + + var shader; + if (elem.type == "x-shader/x-fragment") { + shader = gl.createShader(gl.FRAGMENT_SHADER); + } else if (elem.type == "x-shader/x-vertex") { + shader = gl.createShader(gl.VERTEX_SHADER); + } else { + throw new Error( + "Bad MIME type for shader '" + id + "': " + elem.type + "." + ); + } + + gl.shaderSource(shader, src); + gl.compileShader(shader); + + return shader; + } + + function createProgramByIds(gl, vsId, fsId) { + var vs = createShaderById(gl, vsId); + var fs = createShaderById(gl, fsId); + if (!vs || !fs) { + return null; + } + + var prog = gl.createProgram(); + gl.attachShader(prog, vs); + gl.attachShader(prog, fs); + gl.linkProgram(prog); + + if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { + var str = "Shader program linking failed:"; + str += "\nShader program info log:\n" + gl.getProgramInfoLog(prog); + str += "\n\nVert shader log:\n" + gl.getShaderInfoLog(vs); + str += "\n\nFrag shader log:\n" + gl.getShaderInfoLog(fs); + console.error(str); + return null; + } + + return prog; + } + + return { + createShaderById, + createProgramByIds, + }; +})(); diff --git a/dom/vr/test/reftest/wrapper.html b/dom/vr/test/reftest/wrapper.html new file mode 100644 index 0000000000..40d0de6e42 --- /dev/null +++ b/dom/vr/test/reftest/wrapper.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<head> +<title>Image reftest wrapper</title> +<style type="text/css"> + #image1 { background-color: rgb(10, 100, 250); } +</style> +<script> + // The image is loaded async after the page loads + // wait for it to finish loading + function onImageLoad() { + document.documentElement.removeAttribute("class"); + }; +</script> +</head> +<body> +<img id="image1"> +<script> + // Use as "wrapper.html?image.png" + var imgURL = document.location.search.substr(1); + document.images[0].onload = onImageLoad; + document.images[0].onerror = onImageLoad; + document.images[0].src = imgURL; +</script> +</body> +</html> |