diff options
Diffstat (limited to 'dom/vr/XRSession.cpp')
-rw-r--r-- | dom/vr/XRSession.cpp | 561 |
1 files changed, 561 insertions, 0 deletions
diff --git a/dom/vr/XRSession.cpp b/dom/vr/XRSession.cpp new file mode 100644 index 0000000000..90789de9db --- /dev/null +++ b/dom/vr/XRSession.cpp @@ -0,0 +1,561 @@ +/* -*- 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(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(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 |