summaryrefslogtreecommitdiffstats
path: root/dom/vr/XRSession.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/vr/XRSession.cpp')
-rw-r--r--dom/vr/XRSession.cpp561
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