summaryrefslogtreecommitdiffstats
path: root/dom/vr
diff options
context:
space:
mode:
Diffstat (limited to 'dom/vr')
-rw-r--r--dom/vr/VRDisplay.cpp780
-rw-r--r--dom/vr/VRDisplay.h367
-rw-r--r--dom/vr/VRDisplayEvent.cpp74
-rw-r--r--dom/vr/VRDisplayEvent.h65
-rw-r--r--dom/vr/VREventObserver.cpp181
-rw-r--r--dom/vr/VREventObserver.h57
-rw-r--r--dom/vr/VRServiceTest.cpp740
-rw-r--r--dom/vr/VRServiceTest.h206
-rw-r--r--dom/vr/XRBoundedReferenceSpace.cpp73
-rw-r--r--dom/vr/XRBoundedReferenceSpace.h42
-rw-r--r--dom/vr/XRFrame.cpp202
-rw-r--r--dom/vr/XRFrame.h63
-rw-r--r--dom/vr/XRInputSource.cpp400
-rw-r--r--dom/vr/XRInputSource.h83
-rw-r--r--dom/vr/XRInputSourceArray.cpp167
-rw-r--r--dom/vr/XRInputSourceArray.h55
-rw-r--r--dom/vr/XRInputSpace.cpp34
-rw-r--r--dom/vr/XRInputSpace.h32
-rw-r--r--dom/vr/XRNativeOrigin.h32
-rw-r--r--dom/vr/XRNativeOriginFixed.cpp16
-rw-r--r--dom/vr/XRNativeOriginFixed.h30
-rw-r--r--dom/vr/XRNativeOriginLocal.cpp34
-rw-r--r--dom/vr/XRNativeOriginLocal.h36
-rw-r--r--dom/vr/XRNativeOriginLocalFloor.cpp41
-rw-r--r--dom/vr/XRNativeOriginLocalFloor.h38
-rw-r--r--dom/vr/XRNativeOriginTracker.cpp30
-rw-r--r--dom/vr/XRNativeOriginTracker.h30
-rw-r--r--dom/vr/XRNativeOriginViewer.cpp31
-rw-r--r--dom/vr/XRNativeOriginViewer.h35
-rw-r--r--dom/vr/XRPermissionRequest.cpp75
-rw-r--r--dom/vr/XRPermissionRequest.h39
-rw-r--r--dom/vr/XRPose.cpp40
-rw-r--r--dom/vr/XRPose.h48
-rw-r--r--dom/vr/XRReferenceSpace.cpp45
-rw-r--r--dom/vr/XRReferenceSpace.h47
-rw-r--r--dom/vr/XRRenderState.cpp90
-rw-r--r--dom/vr/XRRenderState.h65
-rw-r--r--dom/vr/XRRigidTransform.cpp171
-rw-r--r--dom/vr/XRRigidTransform.h67
-rw-r--r--dom/vr/XRSession.cpp561
-rw-r--r--dom/vr/XRSession.h159
-rw-r--r--dom/vr/XRSpace.cpp81
-rw-r--r--dom/vr/XRSpace.h56
-rw-r--r--dom/vr/XRSystem.cpp703
-rw-r--r--dom/vr/XRSystem.h174
-rw-r--r--dom/vr/XRView.cpp80
-rw-r--r--dom/vr/XRView.h56
-rw-r--r--dom/vr/XRViewerPose.cpp44
-rw-r--r--dom/vr/XRViewerPose.h45
-rw-r--r--dom/vr/XRViewport.cpp29
-rw-r--r--dom/vr/XRViewport.h47
-rw-r--r--dom/vr/moz.build67
-rw-r--r--dom/vr/test/crashtests/crashtests.list1
-rw-r--r--dom/vr/test/crashtests/enumerate_vr_on_dying_window.html14
-rw-r--r--dom/vr/test/mochitest/VRSimulationDriver.js95
-rw-r--r--dom/vr/test/mochitest/WebVRHelpers.js19
-rw-r--r--dom/vr/test/mochitest/mochitest.ini31
-rw-r--r--dom/vr/test/mochitest/requestPresent.js74
-rw-r--r--dom/vr/test/mochitest/runVRTest.js18
-rw-r--r--dom/vr/test/mochitest/test_vrController_displayId.html57
-rw-r--r--dom/vr/test/mochitest/test_vrDisplay_canvas2d.html55
-rw-r--r--dom/vr/test/mochitest/test_vrDisplay_exitPresent.html51
-rw-r--r--dom/vr/test/mochitest/test_vrDisplay_getFrameData.html148
-rw-r--r--dom/vr/test/mochitest/test_vrDisplay_onvrdisplayconnect.html43
-rw-r--r--dom/vr/test/mochitest/test_vrDisplay_onvrdisplaydeactivate_crosscontent.html54
-rw-r--r--dom/vr/test/mochitest/test_vrDisplay_requestPresent.html130
-rw-r--r--dom/vr/test/reftest/VRSimulationDriver.js60
-rw-r--r--dom/vr/test/reftest/change_size.html168
-rw-r--r--dom/vr/test/reftest/change_size.pngbin0 -> 1439 bytes
-rw-r--r--dom/vr/test/reftest/draw_rect.html136
-rw-r--r--dom/vr/test/reftest/draw_rect.pngbin0 -> 1747 bytes
-rw-r--r--dom/vr/test/reftest/reftest.list10
-rw-r--r--dom/vr/test/reftest/webgl-util.js61
-rw-r--r--dom/vr/test/reftest/wrapper.html26
74 files changed, 8014 insertions, 0 deletions
diff --git a/dom/vr/VRDisplay.cpp b/dom/vr/VRDisplay.cpp
new file mode 100644
index 0000000000..7c788a4c57
--- /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, DOMEventTargetHelper)
+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..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
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..5129485926
--- /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..a2037bf821
--- /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
new file mode 100644
index 0000000000..fe03114b20
--- /dev/null
+++ b/dom/vr/test/reftest/change_size.png
Binary files differ
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
new file mode 100644
index 0000000000..0f4d24a0d1
--- /dev/null
+++ b/dom/vr/test/reftest/draw_rect.png
Binary files differ
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>