Bug 1603825 - Suppress the VR permission UI when no VR runtimes are detected r=daoshengmu,bzbarsky
authorKearwood "Kip" Gilbert <kgilbert@mozilla.com>
Fri, 03 Jan 2020 22:47:26 +0000
changeset 508795 b09464ca18bd3f119657670507730969709afe73
parent 508794 7373e508deee3e4c24063db928596a6f76e8f062
child 508796 2080b0fc57668fad161482d5a25dd0bf07d36eff
push id36978
push useraiakab@mozilla.com
push dateSat, 04 Jan 2020 09:46:47 +0000
treeherdermozilla-central@d5402d5ef78b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdaoshengmu, bzbarsky
bugs1603825
milestone73.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1603825 - Suppress the VR permission UI when no VR runtimes are detected r=daoshengmu,bzbarsky This patch suppresses VR device access permission prompts for users that do not have any VR runtimes installed. We could not depend on the existing VR device enumeration functions to suppress the permission prompts, as the act of enumerating VR devices will result in some hardware physically powering on and software starting up (and staying running) in the background. This patch includes logic to spawn the VR process with an additional flag indicating that it should attempt only to detect the runtimes, without proceeding to enumerate and activate hardware and software. VRManager now includes an enum to more clearly organize it's state machine model, which now must ensure that the runtime detection happens on-demand when the VR session support capabilities are first determined. There is a new pref to disable the suppression of permission prompts for use within permission UI tests on machines without VR runtimes. Renamed some variables and added comments to make code in nsGlobalWindowInner and Navigator clearer and better represent the updated logic -- to allow the separate detection of VR runtimes and VR session activation. Both the runtime detection and VR session activity uses VREventObserver to send events to nsGlobalWindowInner. Differential Revision: https://phabricator.services.mozilla.com/D57568
browser/base/content/test/permissions/browser_temporary_permissions_expiry.js
browser/base/content/test/popupNotifications/browser_displayURI.js
browser/components/privatebrowsing/test/browser/browser_privatebrowsing_rememberprompt.js
dom/base/Navigator.cpp
dom/base/nsGlobalWindowInner.cpp
dom/base/nsGlobalWindowInner.h
dom/vr/VREventObserver.cpp
dom/vr/VREventObserver.h
dom/vr/test/crashtests/crashtests.list
dom/vr/test/crashtests/enumerate_vr_on_dying_window.html
dom/vr/test/mochitest/mochitest.ini
gfx/vr/VRManager.cpp
gfx/vr/VRManager.h
gfx/vr/ipc/PVRManager.ipdl
gfx/vr/ipc/VRManagerChild.cpp
gfx/vr/ipc/VRManagerChild.h
gfx/vr/ipc/VRManagerParent.cpp
gfx/vr/ipc/VRManagerParent.h
gfx/vr/ipc/VRMessageUtils.h
modules/libpref/init/StaticPrefList.yaml
testing/crashtest/crashtests.list
--- a/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js
@@ -22,16 +22,17 @@ const kVREnabled = SpecialPowers.getBool
 
 // Test that temporary permissions can be re-requested after they expired
 // and that the identity block is updated accordingly.
 add_task(async function testTempPermissionRequestAfterExpiry() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["privacy.temporary_permission_expire_time_ms", EXPIRE_TIME_MS],
       ["media.navigator.permission.fake", true],
+      ["dom.vr.always_support_vr", true],
     ],
   });
 
   let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
     ORIGIN
   );
   let ids = ["geo", "camera"];
 
--- a/browser/base/content/test/popupNotifications/browser_displayURI.js
+++ b/browser/base/content/test/popupNotifications/browser_displayURI.js
@@ -84,16 +84,17 @@ async function check(contentTask, option
   }
 }
 
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["media.navigator.permission.fake", true],
       ["media.navigator.permission.force", true],
+      ["dom.vr.always_support_vr", true],
     ],
   });
 });
 
 add_task(async function test_displayURI_geo() {
   await check(async function() {
     content.navigator.geolocation.getCurrentPosition(() => {});
   });
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_rememberprompt.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_rememberprompt.js
@@ -4,17 +4,20 @@
 
 // This test makes sure that the geolocation prompt does not show a remember
 // control inside the private browsing mode.
 
 // This test leaks intermittently with Fission. Make it behave more like
 // non-Fission e10s tests to try to avoid them.
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({
-    set: [["dom.ipc.keepProcessesAlive.webIsolated.perOrigin", 1]],
+    set: [
+      ["dom.ipc.keepProcessesAlive.webIsolated.perOrigin", 1],
+      ["dom.vr.always_support_vr", true],
+    ],
   });
 });
 
 add_task(async function test() {
   function checkPrompt(aURL, aName, aPrivateMode, aWindow) {
     return (async function() {
       aWindow.gBrowser.selectedTab = BrowserTestUtils.addTab(
         aWindow.gBrowser,
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1514,17 +1514,17 @@ void Navigator::FinishGetVRDisplays(bool
     p->MaybeResolve(vrDisplaysEmpty);
     return;
   }
 
   // Since FinishGetVRDisplays can be called asynchronously after an IPC
   // response, it's possible that the Window can be torn down before this
   // call. In that case, the Window's cyclic references to VR objects are
   // also torn down and should not be recreated via
-  // NotifyVREventListenerAdded.
+  // NotifyHasXRSession.
   nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(mWindow);
   if (win->IsDying()) {
     // The Window has been torn down, so there is no further work that can
     // be done.
     p->MaybeRejectWithTypeError(
         u"Unable to return VRDisplays for a closed window.");
     return;
   }
@@ -1558,17 +1558,17 @@ void Navigator::OnXRPermissionRequestCan
 
 void Navigator::GetActiveVRDisplays(
     nsTArray<RefPtr<VRDisplay>>& aDisplays) const {
   /**
    * Get only the active VR displays.
    * GetActiveVRDisplays should only enumerate displays that
    * are already active without causing any other hardware to be
    * activated.
-   * We must not call nsGlobalWindow::NotifyVREventListenerAdded here,
+   * We must not call nsGlobalWindow::NotifyHasXRSession here,
    * as that would cause enumeration and activation of other VR hardware.
    * Activating VR hardware is intrusive to the end user, as it may
    * involve physically powering on devices that the user did not
    * intend to use.
    */
   if (!mWindow || !mWindow->GetDocShell()) {
     return;
   }
@@ -1603,17 +1603,17 @@ void Navigator::NotifyVRDisplaysUpdated(
 
 void Navigator::NotifyActiveVRDisplaysChanged() {
   Navigator_Binding::ClearCachedActiveVRDisplaysValue(this);
 }
 
 VRServiceTest* Navigator::RequestVRServiceTest() {
   // Ensure that the Mock VR devices are not released prematurely
   nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(mWindow);
-  win->NotifyVREventListenerAdded();
+  win->NotifyHasXRSession();
 
   if (!mVRServiceTest) {
     mVRServiceTest = VRServiceTest::CreateTestService(mWindow);
   }
   return mVRServiceTest;
 }
 
 bool Navigator::IsWebVRContentDetected() const {
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -834,18 +834,19 @@ nsGlobalWindowInner::nsGlobalWindowInner
       mIsChrome(false),
       mCleanMessageManager(false),
       mNeedsFocus(true),
       mHasFocus(false),
       mShowFocusRingForContent(false),
       mFocusByKeyOccurred(false),
       mDidFireDocElemInserted(false),
       mHasGamepad(false),
-      mHasVREvents(false),
+      mHasXRSession(false),
       mHasVRDisplayActivateEvents(false),
+      mXRRuntimeDetectionInFlight(false),
       mXRPermissionRequestInFlight(false),
       mXRPermissionGranted(false),
       mWasCurrentInnerWindow(false),
       mHasSeenGamepadInput(false),
       mSuspendDepth(0),
       mFreezeDepth(0),
 #ifdef DEBUG
       mSerial(0),
@@ -1137,18 +1138,19 @@ void nsGlobalWindowInner::FreeInnerObjec
     mAudioContexts[i]->Shutdown();
   }
   mAudioContexts.Clear();
 
   DisableGamepadUpdates();
   mHasGamepad = false;
   mGamepads.Clear();
   DisableVRUpdates();
-  mHasVREvents = false;
+  mHasXRSession = false;
   mHasVRDisplayActivateEvents = false;
+  mXRRuntimeDetectionInFlight = false;
   mXRPermissionRequestInFlight = false;
   mXRPermissionGranted = false;
   mVRDisplays.Clear();
 
   // This breaks a cycle between the window and the ClientSource object.
   mClientSource.reset();
 
   if (mWindowGlobalChild) {
@@ -3980,22 +3982,33 @@ void nsGlobalWindowInner::DisableGamepad
     RefPtr<GamepadManager> gamepadManager(GamepadManager::GetService());
     if (gamepadManager) {
       gamepadManager->RemoveListener(this);
     }
   }
 }
 
 void nsGlobalWindowInner::EnableVRUpdates() {
-  if (mHasVREvents && !mVREventObserver) {
-    MOZ_ASSERT(!IsDying());
+  // We need to create a VREventObserver before we can either detect XR runtimes
+  // or start an XR session
+  if (!mVREventObserver && (mHasXRSession || mXRRuntimeDetectionInFlight)) {
+    // Assert that we are not creating the observer while IsDying() as
+    // that would result in a leak.  VREventObserver holds a RefPtr to
+    // this nsGlobalWindowInner and would prevent it from being deallocated.
+    MOZ_ASSERT(!IsDying(),
+               "Creating a VREventObserver for an nsGlobalWindow that is "
+               "dying would cause it to leak.");
     mVREventObserver = new VREventObserver(this);
+  }
+  // If the content has an XR session, then we need to tell
+  // VREventObserver that there is VR activity.
+  if (mHasXRSession) {
     nsPIDOMWindowOuter* outer = GetOuterWindow();
     if (outer && !outer->IsBackground()) {
-      mVREventObserver->StartActivity();
+      StartVRActivity();
     }
   }
 }
 
 void nsGlobalWindowInner::DisableVRUpdates() {
   if (mVREventObserver) {
     mVREventObserver->DisconnectFromOwner();
     mVREventObserver = nullptr;
@@ -4004,23 +4017,46 @@ void nsGlobalWindowInner::DisableVRUpdat
 
 void nsGlobalWindowInner::ResetVRTelemetry(bool aUpdate) {
   if (mVREventObserver) {
     mVREventObserver->UpdateSpentTimeIn2DTelemetry(aUpdate);
   }
 }
 
 void nsGlobalWindowInner::StartVRActivity() {
-  if (mVREventObserver) {
+  /**
+   * If the content has an XR session, tell
+   * the VREventObserver that the window is accessing
+   * VR devices.
+   *
+   * It's possible to have a VREventObserver without
+   * and XR session, if we are using it to get updates
+   * about XR runtime enumeration.  In this case,
+   * we would not tell the VREventObserver that
+   * we are accessing VR devices.
+   */
+  if (mVREventObserver && mHasXRSession) {
     mVREventObserver->StartActivity();
   }
 }
 
 void nsGlobalWindowInner::StopVRActivity() {
-  if (mVREventObserver) {
+  /**
+   * If the content has an XR session, tell
+   * the VReventObserver that the window is no longer
+   * accessing VR devices.  This does not stop the
+   * XR session itself, which may be resumed with
+   * EnableVRUpdates.
+   * It's possible to have a VREventObserver without
+   * and XR session, if we are using it to get updates
+   * about XR runtime enumeration.  In this case,
+   * we would not tell the VREventObserver that
+   * we ending an activity that accesses VR devices.
+   */
+  if (mVREventObserver && mHasXRSession) {
     mVREventObserver->StopActivity();
   }
 }
 
 #ifndef XP_WIN  // This guard should match the guard at the callsite.
 static bool ShouldShowFocusRingIfFocusedByMouse(nsIContent* aNode) {
   if (!aNode) {
     return true;
@@ -5989,46 +6025,96 @@ void nsGlobalWindowInner::DisableOrienta
 void nsGlobalWindowInner::SetHasGamepadEventListener(
     bool aHasGamepad /* = true*/) {
   mHasGamepad = aHasGamepad;
   if (aHasGamepad) {
     EnableGamepadUpdates();
   }
 }
 
+void nsGlobalWindowInner::NotifyDetectXRRuntimesCompleted() {
+  if (!mXRRuntimeDetectionInFlight) {
+    return;
+  }
+  mXRRuntimeDetectionInFlight = false;
+  if (mXRPermissionRequestInFlight) {
+    return;
+  }
+  gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
+  bool supported = vm->RuntimeSupportsVR();
+  if (!supported) {
+    // A VR runtime was not installed; we can suppress
+    // the permission prompt
+    OnXRPermissionRequestCancel();
+    return;
+  }
+  // A VR runtime was found.  Display a permission prompt before
+  // allowing it to be accessed.
+  // Connect to the VRManager in order to receive the runtime
+  // detection results.
+  mXRPermissionRequestInFlight = true;
+  RefPtr<XRPermissionRequest> request =
+      new XRPermissionRequest(this, WindowID());
+  Unused << NS_WARN_IF(NS_FAILED(request->Start()));
+}
+
 void nsGlobalWindowInner::RequestXRPermission() {
+  if (IsDying()) {
+    // Do not proceed if the window is dying, as that will result
+    // in leaks of objects that get re-allocated after FreeInnerObjects
+    // has been called, including mVREventObserver.
+    return;
+  }
   if (mXRPermissionGranted) {
     // Don't prompt redundantly once permission to
     // access XR devices has been granted.
     OnXRPermissionRequestAllow();
     return;
   }
-  if (mXRPermissionRequestInFlight) {
+  if (mXRRuntimeDetectionInFlight || mXRPermissionRequestInFlight) {
     // Don't allow multiple simultaneous permissions requests;
     return;
   }
-  mXRPermissionRequestInFlight = true;
-  RefPtr<XRPermissionRequest> request =
-      new XRPermissionRequest(this, WindowID());
-  Unused << NS_WARN_IF(NS_FAILED(request->Start()));
+  // Before displaying a permission prompt, detect
+  // if there is any VR runtime installed.
+  gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
+  mXRRuntimeDetectionInFlight = true;
+  EnableVRUpdates();
+  vm->DetectRuntimes();
 }
 
 void nsGlobalWindowInner::OnXRPermissionRequestAllow() {
   mXRPermissionRequestInFlight = false;
+  if (IsDying()) {
+    // The window may have started dying while the permission request
+    // is in flight.
+    // Do not proceed if the window is dying, as that will result
+    // in leaks of objects that get re-allocated after FreeInnerObjects
+    // has been called, including mNavigator.
+    return;
+  }
   mXRPermissionGranted = true;
 
-  NotifyVREventListenerAdded();
+  NotifyHasXRSession();
 
   dom::Navigator* nav = Navigator();
   MOZ_ASSERT(nav != nullptr);
   nav->OnXRPermissionRequestAllow();
 }
 
 void nsGlobalWindowInner::OnXRPermissionRequestCancel() {
   mXRPermissionRequestInFlight = false;
+  if (IsDying()) {
+    // The window may have started dying while the permission request
+    // is in flight.
+    // Do not proceed if the window is dying, as that will result
+    // in leaks of objects that get re-allocated after FreeInnerObjects
+    // has been called, including mNavigator.
+    return;
+  }
   dom::Navigator* nav = Navigator();
   MOZ_ASSERT(nav != nullptr);
   nav->OnXRPermissionRequestCancel();
 }
 
 void nsGlobalWindowInner::EventListenerAdded(nsAtom* aType) {
   if (aType == nsGkAtoms::onvrdisplayactivate ||
       aType == nsGkAtoms::onvrdisplayconnect ||
@@ -6079,25 +6165,32 @@ void nsGlobalWindowInner::EventListenerR
         !mListenerManager->HasListenersFor(nsGkAtoms::onstorage)) {
       auto object = static_cast<LSObject*>(mLocalStorage.get());
 
       object->DropObserver();
     }
   }
 }
 
-void nsGlobalWindowInner::NotifyVREventListenerAdded() {
-  mHasVREvents = true;
+void nsGlobalWindowInner::NotifyHasXRSession() {
+  if (IsDying()) {
+    // Do not proceed if the window is dying, as that will result
+    // in leaks of objects that get re-allocated after FreeInnerObjects
+    // has been called, including mVREventObserver.
+    return;
+  }
+  mHasXRSession = true;
   EnableVRUpdates();
 }
 
 bool nsGlobalWindowInner::HasUsedVR() const {
-  // Returns true only if any WebVR API call or related event
-  // has been used
-  return mHasVREvents;
+  // Returns true only if content has enumerated and activated
+  // XR devices.  Detection of XR runtimes without activation
+  // will not cause true to be returned.
+  return mHasXRSession;
 }
 
 bool nsGlobalWindowInner::IsVRContentDetected() const {
   // Returns true only if the content will respond to
   // the VRDisplayActivate event.
   return mHasVRDisplayActivateEvents;
 }
 
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -363,17 +363,17 @@ class nsGlobalWindowInner final : public
   // Inner windows only.
   void RefreshRealmPrincipal();
 
   // For accessing protected field mFullscreen
   friend class FullscreenTransitionTask;
 
   // Inner windows only.
   virtual void SetHasGamepadEventListener(bool aHasGamepad = true) override;
-  void NotifyVREventListenerAdded();
+  void NotifyHasXRSession();
   bool HasUsedVR() const;
   bool IsVRContentDetected() const;
   bool IsVRContentPresenting() const;
   void RequestXRPermission();
   void OnXRPermissionRequestAllow();
   void OnXRPermissionRequestCancel();
 
   using EventTarget::EventListenerAdded;
@@ -516,16 +516,17 @@ class nsGlobalWindowInner final : public
   void StopVRActivity();
 
   // Update the VR displays for this window
   bool UpdateVRDisplays(nsTArray<RefPtr<mozilla::dom::VRDisplay>>& aDisplays);
 
   // Inner windows only.
   // Called to inform that the set of active VR displays has changed.
   void NotifyActiveVRDisplaysChanged();
+  void NotifyDetectXRRuntimesCompleted();
   void NotifyPresentationGenerationChanged(uint32_t aDisplayID);
 
   void DispatchVRDisplayActivate(uint32_t aDisplayID,
                                  mozilla::dom::VRDisplayEventReason aReason);
   void DispatchVRDisplayDeactivate(uint32_t aDisplayID,
                                    mozilla::dom::VRDisplayEventReason aReason);
   void DispatchVRDisplayConnect(uint32_t aDisplayID);
   void DispatchVRDisplayDisconnect(uint32_t aDisplayID);
@@ -1268,22 +1269,27 @@ class nsGlobalWindowInner final : public
 
   // True if we have notified document-element-inserted observers for this
   // document.
   bool mDidFireDocElemInserted : 1;
 
   // Indicates whether this window wants gamepad input events
   bool mHasGamepad : 1;
 
-  // Indicates whether this window wants VR events
-  bool mHasVREvents : 1;
+  // Indicates whether this window has content that has an XR session
+  // An XR session results in enumeration and activation of XR devices.
+  bool mHasXRSession : 1;
 
   // Indicates whether this window wants VRDisplayActivate events
   bool mHasVRDisplayActivateEvents : 1;
 
+  // Indicates that a request for XR runtime detection has been
+  // requested, but has not yet been resolved
+  bool mXRRuntimeDetectionInFlight : 1;
+
   // Indicates that an XR permission request has been requested
   // but has not yet been resolved.
   bool mXRPermissionRequestInFlight : 1;
 
   // Indicates that an XR permission request has been granted.
   // The page should not request permission multiple times.
   bool mXRPermissionGranted : 1;
 
--- a/dom/vr/VREventObserver.cpp
+++ b/dom/vr/VREventObserver.cpp
@@ -152,17 +152,24 @@ void VREventObserver::NotifyVRDisplayPre
     mWindow->NotifyActiveVRDisplaysChanged();
     MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
     mWindow->DispatchVRDisplayPresentChange(aDisplayID);
   }
 }
 
 void VREventObserver::NotifyPresentationGenerationChanged(uint32_t aDisplayID) {
   if (mWindow && mWindow->IsCurrentInnerWindow()) {
+    MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
     mWindow->NotifyPresentationGenerationChanged(aDisplayID);
-    MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
   }
 }
 
 void VREventObserver::NotifyEnumerationCompleted() {}
 
+void VREventObserver::NotifyDetectRuntimesCompleted() {
+  if (mWindow && mWindow->IsCurrentInnerWindow()) {
+    MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
+    mWindow->NotifyDetectXRRuntimesCompleted();
+  }
+}
+
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/vr/VREventObserver.h
+++ b/dom/vr/VREventObserver.h
@@ -26,16 +26,17 @@ class VREventObserver final : public gfx
   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:
new file mode 100644
--- /dev/null
+++ b/dom/vr/test/crashtests/crashtests.list
@@ -0,0 +1,1 @@
+pref(dom.vr.always_support_vr,true) load enumerate_vr_on_dying_window.html
new file mode 100644
--- /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>
--- a/dom/vr/test/mochitest/mochitest.ini
+++ b/dom/vr/test/mochitest/mochitest.ini
@@ -1,16 +1,15 @@
 # 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.
--- a/gfx/vr/VRManager.cpp
+++ b/gfx/vr/VRManager.cpp
@@ -102,26 +102,30 @@ void VRManager::ManagerInit() {
     sVRManagerSingleton = new VRManager();
     ClearOnShutdown(&sVRManagerSingleton);
   }
 }
 
 VRManager::VRManager()
     : mState(VRManagerState::Disabled),
       mAccumulator100ms(0.0f),
+      mRuntimeDetectionRequested(false),
+      mRuntimeDetectionCompleted(false),
+      mEnumerationRequested(false),
+      mEnumerationCompleted(false),
       mVRDisplaysRequested(false),
       mVRDisplaysRequestedNonFocus(false),
       mVRControllersRequested(false),
       mFrameStarted(false),
       mTaskInterval(0),
       mCurrentSubmitTaskMonitor("CurrentSubmitTaskMonitor"),
       mCurrentSubmitTask(nullptr),
       mLastSubmittedFrameId(0),
       mLastStartedFrame(0),
-      mEnumerationCompleted(false),
+      mRuntimeSupportFlags(VRDisplayCapabilityFlags::Cap_None),
       mAppPaused(false),
       mShmem(nullptr),
       mHapticPulseRemaining{},
       mDisplayInfo{},
       mLastUpdateDisplayInfo{},
       mBrowserState{},
       mLastSensorState{} {
   MOZ_ASSERT(sVRManagerSingleton == nullptr);
@@ -167,16 +171,25 @@ void VRManager::OpenShmem() {
       return;
     }
 #else
     mShmem->CreateShMemForAndroid();
 #endif
   } else {
     mShmem->ClearShMem();
   }
+
+  // Reset local information for new connection
+  mDisplayInfo.Clear();
+  mLastUpdateDisplayInfo.Clear();
+  mFrameStarted = false;
+  mBrowserState.Clear();
+  mLastSensorState.Clear();
+  mEnumerationCompleted = false;
+  mDisplayInfo.mGroupMask = kVRGroupContent;
 }
 
 void VRManager::CloseShmem() {
   if (mShmem != nullptr) {
     mShmem->CloseShMem();
     delete mShmem;
     mShmem = nullptr;
   }
@@ -201,31 +214,37 @@ VRManager::~VRManager() {
 void VRManager::AddLayer(VRLayerParent* aLayer) {
   mLayers.AppendElement(aLayer);
   mDisplayInfo.mPresentingGroups |= aLayer->GetGroup();
   if (mLayers.Length() == 1) {
     StartPresentation();
   }
 
   // Ensure that the content process receives the change immediately
-  RefreshVRDisplays();
+  if (mState != VRManagerState::Enumeration &&
+      mState != VRManagerState::RuntimeDetection) {
+    DispatchVRDisplayInfoUpdate();
+  }
 }
 
 void VRManager::RemoveLayer(VRLayerParent* aLayer) {
   mLayers.RemoveElement(aLayer);
   if (mLayers.Length() == 0) {
     StopPresentation();
   }
   mDisplayInfo.mPresentingGroups = 0;
   for (auto layer : mLayers) {
     mDisplayInfo.mPresentingGroups |= layer->GetGroup();
   }
 
   // Ensure that the content process receives the change immediately
-  RefreshVRDisplays();
+  if (mState != VRManagerState::Enumeration &&
+      mState != VRManagerState::RuntimeDetection) {
+    DispatchVRDisplayInfoUpdate();
+  }
 }
 
 void VRManager::AddVRManagerParent(VRManagerParent* aVRManagerParent) {
   mVRManagerParents.PutEntry(aVRManagerParent);
 }
 
 void VRManager::RemoveVRManagerParent(VRManagerParent* aVRManagerParent) {
   mVRManagerParents.RemoveEntry(aVRManagerParent);
@@ -417,19 +436,17 @@ void VRManager::Run100msTasks() {
   // for events that we must fire such as Window.onvrdisplayconnect
   // Note that enumeration itself may activate display hardware, such
   // as Oculus, so we only do this when we know we are displaying content
   // that is looking for VR displays.
 #if !defined(MOZ_WIDGET_ANDROID)
   mServiceHost->Refresh();
   CheckForPuppetCompletion();
 #endif
-  RefreshVRDisplays();
-  CheckForInactiveTimeout();
-  CheckForShutdown();
+  ProcessManagerState();
 }
 
 void VRManager::ProcessTelemetryEvent() {
   mozilla::gfx::VRShMem shmem(nullptr, true /*aRequiresMutex*/);
   MOZ_ASSERT(!shmem.HasExternalShmem());
   if (shmem.JoinShMem()) {
     mozilla::gfx::VRTelemetryState telemetryState = {0};
     shmem.PullTelemetryState(telemetryState);
@@ -456,17 +473,19 @@ void VRManager::ProcessTelemetryEvent() 
       shmem.PushTelemetryState(telemetryState);
     }
   }
 }
 
 void VRManager::CheckForInactiveTimeout() {
   // Shut down the VR devices when not in use
   if (mVRDisplaysRequested || mVRDisplaysRequestedNonFocus ||
-      mVRControllersRequested) {
+      mVRControllersRequested || mEnumerationRequested ||
+      mRuntimeDetectionRequested || mState == VRManagerState::Enumeration ||
+      mState == VRManagerState::RuntimeDetection) {
     // We are using a VR device, keep it alive
     mLastActiveTime = TimeStamp::Now();
   } else if (mLastActiveTime.IsNull()) {
     Shutdown();
   } else {
     TimeDuration duration = TimeStamp::Now() - mLastActiveTime;
     if (duration.ToMilliseconds() > StaticPrefs::dom_vr_inactive_timeout()) {
       Shutdown();
@@ -477,18 +496,17 @@ void VRManager::CheckForInactiveTimeout(
       // site.
       mLastDisplayEnumerationTime = TimeStamp();
     }
   }
 }
 
 void VRManager::CheckForShutdown() {
   // Check for remote end shutdown
-  if (mState != VRManagerState::Disabled && mState != VRManagerState::Idle &&
-      mDisplayInfo.mDisplayState.shutdown) {
+  if (mDisplayInfo.mDisplayState.shutdown) {
     Shutdown();
   }
 }
 
 #if !defined(MOZ_WIDGET_ANDROID)
 void VRManager::CheckForPuppetCompletion() {
   // Notify content process about completion of puppet test resets
   if (mState != VRManagerState::Active) {
@@ -537,156 +555,322 @@ void VRManager::StartFrame() {
   mDisplayInfo.mLastSensorState[bufferIndex] = mLastSensorState;
   mLastFrameStart[bufferIndex] = now;
   mFrameStarted = true;
   mLastStartedFrame = mDisplayInfo.mFrameId;
 
   DispatchVRDisplayInfoUpdate();
 }
 
-void VRManager::EnumerateVRDisplays() {
+void VRManager::DetectRuntimes() {
+  if (mState == VRManagerState::RuntimeDetection) {
+    // Runtime detection has already been started.
+    // This additional request will also receive the
+    // result from the first request.
+    return;
+  }
+
+  // Detect XR runtimes to determine if they are
+  // capable of supporting VR or AR sessions, while
+  // avoiding activating any XR devices or persistent
+  // background software.
+  if (mRuntimeDetectionCompleted) {
+    // We have already detected runtimes, so we can
+    // immediately respond with the same results.
+    // This will require the user to restart the browser
+    // after installing or removing an XR device
+    // runtime.
+    DispatchRuntimeCapabilitiesUpdate();
+    return;
+  }
+  mRuntimeDetectionRequested = true;
+  ProcessManagerState();
+}
+
+void VRManager::EnumerateDevices() {
+  if (mState == VRManagerState::Enumeration) {
+    // Enumeration has already been started.
+    // This additional request will also receive the
+    // result from the first request.
+    return;
+  }
+  // Activate XR runtimes and enumerate XR devices.
+  mEnumerationRequested = true;
+  ProcessManagerState();
+}
+
+void VRManager::ProcessManagerState() {
+  switch (mState) {
+    case VRManagerState::Disabled:
+      ProcessManagerState_Disabled();
+      break;
+    case VRManagerState::Idle:
+      ProcessManagerState_Idle();
+      break;
+    case VRManagerState::RuntimeDetection:
+      ProcessManagerState_DetectRuntimes();
+      break;
+    case VRManagerState::Enumeration:
+      ProcessManagerState_Enumeration();
+      break;
+    case VRManagerState::Active:
+      ProcessManagerState_Active();
+      break;
+    case VRManagerState::Stopping:
+      ProcessManagerState_Stopping();
+      break;
+  }
+  CheckForInactiveTimeout();
+  CheckForShutdown();
+}
+
+void VRManager::ProcessManagerState_Disabled() {
+  MOZ_ASSERT(mState == VRManagerState::Disabled);
+
   if (!StaticPrefs::dom_vr_enabled()) {
     return;
   }
 
-  if (mState == VRManagerState::Disabled) {
+  if (mRuntimeDetectionRequested || mEnumerationRequested ||
+      mVRDisplaysRequested) {
     StartTasks();
     mState = VRManagerState::Idle;
   }
+}
 
-  if (mState == VRManagerState::Idle) {
-    /**
-     * Throttle the rate of enumeration to the interval set in
-     * VRDisplayEnumerateInterval
-     */
-    if (!mLastDisplayEnumerationTime.IsNull()) {
-      TimeDuration duration = TimeStamp::Now() - mLastDisplayEnumerationTime;
-      if (duration.ToMilliseconds() <
-          StaticPrefs::dom_vr_display_enumerate_interval()) {
-        return;
-      }
-    }
+void VRManager::ProcessManagerState_Stopping() {
+  MOZ_ASSERT(mState == VRManagerState::Stopping);
+  PullState();
+  /**
+   * In the case of Desktop, the VRService shuts itself down.
+   * Before it's finished stopping, it sets a flag in the ShMem
+   * to let VRManager know that it's done.  VRManager watches for
+   * this flag and transitions out of the VRManagerState::Stopping
+   * state to VRManagerState::Idle.
+   */
+#if defined(MOZ_WIDGET_ANDROID)
+  // On Android, the VR service never actually shuts
+  // down or requests VRManager to stop.
+  Shutdown();
+#endif  // defined(MOZ_WIDGET_ANDROID)
+}
 
-    if (!mEarliestRestartTime.IsNull() &&
-        mEarliestRestartTime > TimeStamp::Now()) {
-      // When the VR Service shuts down it informs us of how long we
-      // must wait until we can re-start it.
-      // We must wait until mEarliestRestartTime before attempting
-      // to enumerate again.
+void VRManager::ProcessManagerState_Idle_StartEnumeration() {
+  MOZ_ASSERT(mState == VRManagerState::Idle);
+
+  if (!mEarliestRestartTime.IsNull() &&
+      mEarliestRestartTime > TimeStamp::Now()) {
+    // When the VR Service shuts down it informs us of how long we
+    // must wait until we can re-start it.
+    // We must wait until mEarliestRestartTime before attempting
+    // to enumerate again.
+    return;
+  }
+
+  /**
+   * Throttle the rate of enumeration to the interval set in
+   * VRDisplayEnumerateInterval
+   */
+  if (!mLastDisplayEnumerationTime.IsNull()) {
+    TimeDuration duration = TimeStamp::Now() - mLastDisplayEnumerationTime;
+    if (duration.ToMilliseconds() <
+        StaticPrefs::dom_vr_display_enumerate_interval()) {
       return;
     }
+  }
 
-    /**
-     * If we get this far, don't try again until
-     * the VRDisplayEnumerateInterval elapses
-     */
-    mLastDisplayEnumerationTime = TimeStamp::Now();
+  /**
+   * If we get this far, don't try again until
+   * the VRDisplayEnumerateInterval elapses
+   */
+  mLastDisplayEnumerationTime = TimeStamp::Now();
 
-    OpenShmem();
+  OpenShmem();
 
-    /**
-     * We must start the VR Service thread
-     * and VR Process before enumeration.
-     * We don't want to start this until we will
-     * actualy enumerate, to avoid continuously
-     * re-launching the thread/process when
-     * no hardware is found or a VR software update
-     * is in progress
-     */
-#if !defined(MOZ_WIDGET_ANDROID)
-    mServiceHost->StartService();
-#endif
-    if (mShmem) {
-      mDisplayInfo.Clear();
-      mLastUpdateDisplayInfo.Clear();
-      mFrameStarted = false;
-      mBrowserState.Clear();
-      mLastSensorState.Clear();
-      mEnumerationCompleted = false;
-      mDisplayInfo.mGroupMask = kVRGroupContent;
-      // We must block until enumeration has completed in order
-      // to signal that the WebVR promise should be resolved at the
-      // right time.
+  mEnumerationRequested = false;
+  // We must block until enumeration has completed in order
+  // to signal that the WebVR promise should be resolved at the
+  // right time.
 #if defined(MOZ_WIDGET_ANDROID)
-      // In Android, we need to make sure calling
-      // GeckoVRManager::SetExternalContext() from an external VR service
-      // before doing enumeration.
-      if (!mShmem->GetExternalShmem()) {
-        mShmem->CreateShMemForAndroid();
-      }
-      if (mShmem->GetExternalShmem()) {
-        mState = VRManagerState::Enumeration;
-      }
+  // In Android, we need to make sure calling
+  // GeckoVRManager::SetExternalContext() from an external VR service
+  // before doing enumeration.
+  if (!mShmem->GetExternalShmem()) {
+    mShmem->CreateShMemForAndroid();
+  }
+  if (mShmem->GetExternalShmem()) {
+    mState = VRManagerState::Enumeration;
+  } else {
+    // Not connected to shmem, so no devices to enumerate.
+    mDisplayInfo.Clear();
+    DispatchVRDisplayInfoUpdate();
+  }
 #else
-      mState = VRManagerState::Enumeration;
+
+  PushState();
+
+  /**
+   * We must start the VR Service thread
+   * and VR Process before enumeration.
+   * We don't want to start this until we will
+   * actualy enumerate, to avoid continuously
+   * re-launching the thread/process when
+   * no hardware is found or a VR software update
+   * is in progress
+   */
+  mServiceHost->StartService();
+  mState = VRManagerState::Enumeration;
 #endif  // MOZ_WIDGET_ANDROID
-    }
-  }  // if (mState == VRManagerState::Idle)
-
-  if (mState == VRManagerState::Enumeration) {
-    MOZ_ASSERT(mShmem != nullptr);
-
-    PullState();
-    if (mEnumerationCompleted) {
-      if (mDisplayInfo.mDisplayState.isConnected) {
-        mDisplayInfo.mDisplayID = VRManager::AllocateDisplayID();
-        mState = VRManagerState::Active;
-      } else {
-        Shutdown();
-      }
-    }
-  }  // if (mState == VRManagerState::Enumeration)
 }
 
-void VRManager::RefreshVRDisplays(bool aMustDispatch) {
-  uint32_t previousDisplayID = mDisplayInfo.GetDisplayID();
+void VRManager::ProcessManagerState_Idle_StartRuntimeDetection() {
+  MOZ_ASSERT(mState == VRManagerState::Idle);
+
+  OpenShmem();
+  mBrowserState.detectRuntimesOnly = true;
+  mRuntimeDetectionRequested = false;
+
+  // We must block until enumeration has completed in order
+  // to signal that the WebVR promise should be resolved at the
+  // right time.
+#if defined(MOZ_WIDGET_ANDROID)
+  // In Android, we need to make sure calling
+  // GeckoVRManager::SetExternalContext() from an external VR service
+  // before doing enumeration.
+  if (!mShmem->GetExternalShmem()) {
+    mShmem->CreateShMemForAndroid();
+  }
+  if (mShmem->GetExternalShmem()) {
+    mState = VRManagerState::RuntimeDetection;
+  } else {
+    // Not connected to shmem, so no runtimes to detect.
+    mRuntimeSupportFlags = VRDisplayCapabilityFlags::Cap_None;
+    mRuntimeDetectionCompleted = true;
+    DispatchRuntimeCapabilitiesUpdate();
+  }
+#else
+
+  PushState();
 
   /**
-   * If we aren't viewing WebVR content, don't enumerate
-   * new hardware, as it will cause some devices to power on
-   * or interrupt other VR activities.
+   * We must start the VR Service thread
+   * and VR Process before enumeration.
+   * We don't want to start this until we will
+   * actualy enumerate, to avoid continuously
+   * re-launching the thread/process when
+   * no hardware is found or a VR software update
+   * is in progress
    */
-  if (mVRDisplaysRequested || aMustDispatch) {
-    EnumerateVRDisplays();
-  }
-  if (mState == VRManagerState::Enumeration) {
-    // If we are enumerating VR Displays, do not dispatch
-    // updates until the enumeration has completed.
+  mServiceHost->StartService();
+  mState = VRManagerState::RuntimeDetection;
+#endif  // MOZ_WIDGET_ANDROID
+}
+
+void VRManager::ProcessManagerState_Idle() {
+  MOZ_ASSERT(mState == VRManagerState::Idle);
+
+  if (!mRuntimeDetectionCompleted) {
+    // Check if we should start detecting runtimes
+    // We must alwasy detect runtimes before doing anything
+    // else with the VR process.
+    // This will happen only once per browser startup.
+    if (mRuntimeDetectionRequested || mEnumerationRequested) {
+      ProcessManagerState_Idle_StartRuntimeDetection();
+    }
     return;
   }
-  bool changed = false;
 
-  if (previousDisplayID != mDisplayInfo.GetDisplayID()) {
-    changed = true;
+  // Check if we should start activating enumerating XR hardware
+  if (mRuntimeDetectionCompleted &&
+      (mVRDisplaysRequested || mEnumerationRequested)) {
+    ProcessManagerState_Idle_StartEnumeration();
   }
+}
+
+void VRManager::ProcessManagerState_DetectRuntimes() {
+  MOZ_ASSERT(mState == VRManagerState::RuntimeDetection);
+  MOZ_ASSERT(mShmem != nullptr);
 
-  if (mState == VRManagerState::Active &&
-      mDisplayInfo != mLastUpdateDisplayInfo) {
-    // This display's info has changed
-    changed = true;
+  PullState();
+  if (mEnumerationCompleted) {
+    /**
+     * When mBrowserState.detectRuntimesOnly is set, the
+     * VRService and VR process will shut themselves down
+     * automatically after detecting runtimes.
+     * mEnumerationCompleted is also used in this case,
+     * but to mean "enumeration of runtimes" not
+     * "enumeration of VR devices".
+     *
+     * We set mState to `VRManagerState::Stopping`
+     * to make sure that we don't try to do anything
+     * else with the active VRService until it has stopped.
+     * We must start another one when an XR session will be
+     * requested.
+     *
+     * This logic is optimized for the WebXR design, but still
+     * works for WebVR so it can continue to function until
+     * deprecated and removed.
+     */
+    mState = VRManagerState::Stopping;
+    mRuntimeSupportFlags = mDisplayInfo.mDisplayState.capabilityFlags &
+                           (VRDisplayCapabilityFlags::Cap_ImmersiveVR |
+                            VRDisplayCapabilityFlags::Cap_ImmersiveAR);
+    mRuntimeDetectionCompleted = true;
+    DispatchRuntimeCapabilitiesUpdate();
   }
+}
 
-  if (changed || aMustDispatch) {
+void VRManager::ProcessManagerState_Enumeration() {
+  MOZ_ASSERT(mState == VRManagerState::Enumeration);
+  MOZ_ASSERT(mShmem != nullptr);
+
+  PullState();
+  if (mEnumerationCompleted) {
+    if (mDisplayInfo.mDisplayState.isConnected) {
+      mDisplayInfo.mDisplayID = VRManager::AllocateDisplayID();
+      mState = VRManagerState::Active;
+    } else {
+      mDisplayInfo.Clear();
+      mState = VRManagerState::Stopping;
+    }
+    DispatchVRDisplayInfoUpdate();
+  }
+}
+
+void VRManager::ProcessManagerState_Active() {
+  MOZ_ASSERT(mState == VRManagerState::Active);
+
+  if (mDisplayInfo != mLastUpdateDisplayInfo) {
+    // While the display is active, send continuous updates
     DispatchVRDisplayInfoUpdate();
   }
 }
 
 void VRManager::DispatchVRDisplayInfoUpdate() {
-  // This could be simplified further by only supporting one display
-  nsTArray<VRDisplayInfo> displayUpdates;
-  if (mState == VRManagerState::Active) {
-    MOZ_ASSERT(mDisplayInfo.mDisplayID != 0);
-    displayUpdates.AppendElement(mDisplayInfo);
-  }
   for (auto iter = mVRManagerParents.Iter(); !iter.Done(); iter.Next()) {
-    Unused << iter.Get()->GetKey()->SendUpdateDisplayInfo(displayUpdates);
+    Unused << iter.Get()->GetKey()->SendUpdateDisplayInfo(mDisplayInfo);
   }
   mLastUpdateDisplayInfo = mDisplayInfo;
 }
 
+void VRManager::DispatchRuntimeCapabilitiesUpdate() {
+  VRDisplayCapabilityFlags flags = mRuntimeSupportFlags;
+  if (StaticPrefs::dom_vr_always_support_vr()) {
+    flags |= VRDisplayCapabilityFlags::Cap_ImmersiveVR;
+  }
+
+  if (StaticPrefs::dom_vr_always_support_ar()) {
+    flags |= VRDisplayCapabilityFlags::Cap_ImmersiveAR;
+  }
+
+  for (auto iter = mVRManagerParents.Iter(); !iter.Done(); iter.Next()) {
+    Unused << iter.Get()->GetKey()->SendUpdateRuntimeCapabilities(flags);
+  }
+}
+
 void VRManager::StopAllHaptics() {
   for (size_t i = 0; i < mozilla::ArrayLength(mBrowserState.hapticState); i++) {
     ClearHapticSlot(i);
   }
   PushState();
 }
 
 void VRManager::VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
@@ -893,21 +1077,41 @@ void VRManager::Shutdown() {
         now + TimeDuration::FromMilliseconds(
                   (double)mDisplayInfo.mDisplayState.minRestartInterval);
   }
 
   StopAllHaptics();
   StopPresentation();
   CancelCurrentSubmitTask();
   ShutdownSubmitThread();
+
   mDisplayInfo.Clear();
   mEnumerationCompleted = false;
 
+  if (mState == VRManagerState::RuntimeDetection) {
+    /**
+     * We have failed to detect runtimes before shutting down.
+     * Ensure that promises are resolved
+     *
+     * This call to DispatchRuntimeCapabilitiesUpdate will only
+     * happen when we have failed to detect runtimes. In that case,
+     * mRuntimeSupportFlags will be 0 and send the correct message
+     * to the content process.
+     *
+     * When we are successful, we store the result in mRuntimeSupportFlags
+     * and never try again unless the browser is restarted. mRuntimeSupportFlags
+     * is never reset back to 0 in that case but we will never re-enter the
+     * VRManagerState::RuntimeDetection state and hit this code path again.
+     */
+    DispatchRuntimeCapabilitiesUpdate();
+  }
+
   if (mState == VRManagerState::Enumeration) {
-    // Ensure that enumeration promises are resolved
+    // We have failed to enumerate VR devices before shutting down.
+    // Ensure that promises are resolved
     DispatchVRDisplayInfoUpdate();
   }
 
 #if !defined(MOZ_WIDGET_ANDROID)
   mServiceHost->StopService();
 #endif
   mState = VRManagerState::Idle;
 
--- a/gfx/vr/VRManager.h
+++ b/gfx/vr/VRManager.h
@@ -21,34 +21,38 @@ class VRLayerParent;
 class VRManagerParent;
 class VRServiceHost;
 class VRThread;
 class VRShMem;
 
 enum class VRManagerState : uint32_t {
   Disabled,  // All VRManager activity is stopped
   Idle,  // No VR hardware has been activated, but background tasks are running
-  Enumeration,  // Waiting for enumeration and startup of VR hardware
-  Active        // VR hardware is active
+  RuntimeDetection,  // Waiting for detection of runtimes without starting up VR
+                     // hardware
+  Enumeration,       // Waiting for enumeration and startup of VR hardware
+  Active,            // VR hardware is active
+  Stopping,          // Waiting for the VRService to stop
 };
 
 class VRManager : nsIObserver {
  public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
   static void ManagerInit();
   static VRManager* Get();
 
   void AddVRManagerParent(VRManagerParent* aVRManagerParent);
   void RemoveVRManagerParent(VRManagerParent* aVRManagerParent);
 
   void NotifyVsync(const TimeStamp& aVsyncTimestamp);
 
-  void RefreshVRDisplays(bool aMustDispatch = false);
+  void DetectRuntimes();
+  void EnumerateDevices();
   void StopAllHaptics();
 
   void VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
                      double aIntensity, double aDuration,
                      const VRManagerPromise& aPromise);
   void StopVibrateHaptic(uint32_t aControllerIdx);
   void NotifyVibrateHapticCompleted(const VRManagerPromise& aPromise);
   void StartVRNavigation(const uint32_t& aDisplayID);
@@ -76,24 +80,32 @@ class VRManager : nsIObserver {
   void StartTasks();
   void StopTasks();
   static void TaskTimerCallback(nsITimer* aTimer, void* aClosure);
   void RunTasks();
   void Run1msTasks(double aDeltaTime);
   void Run10msTasks();
   void Run100msTasks();
   uint32_t GetOptimalTaskInterval();
+  void ProcessManagerState();
+  void ProcessManagerState_Disabled();
+  void ProcessManagerState_Idle();
+  void ProcessManagerState_Idle_StartRuntimeDetection();
+  void ProcessManagerState_Idle_StartEnumeration();
+  void ProcessManagerState_DetectRuntimes();
+  void ProcessManagerState_Enumeration();
+  void ProcessManagerState_Active();
+  void ProcessManagerState_Stopping();
   void PullState(const std::function<bool()>& aWaitCondition = nullptr);
   void PushState(const bool aNotifyCond = false);
   void ProcessTelemetryEvent();
   static uint32_t AllocateDisplayID();
-
   void DispatchVRDisplayInfoUpdate();
+  void DispatchRuntimeCapabilitiesUpdate();
   void UpdateRequestedDevices();
-  void EnumerateVRDisplays();
   void CheckForInactiveTimeout();
 #if !defined(MOZ_WIDGET_ANDROID)
   void CheckForPuppetCompletion();
 #endif
   void CheckForShutdown();
   void CheckWatchDog();
   void ExpireNavigationTransition();
   void OpenShmem();
@@ -126,27 +138,31 @@ class VRManager : nsIObserver {
 
   TimeStamp mLastDisplayEnumerationTime;
   TimeStamp mLastActiveTime;
   TimeStamp mLastTickTime;
   TimeStamp mEarliestRestartTime;
   TimeStamp mVRNavigationTransitionEnd;
   TimeStamp mLastFrameStart[kVRMaxLatencyFrames];
   double mAccumulator100ms;
+  bool mRuntimeDetectionRequested;
+  bool mRuntimeDetectionCompleted;
+  bool mEnumerationRequested;
+  bool mEnumerationCompleted;
   bool mVRDisplaysRequested;
   bool mVRDisplaysRequestedNonFocus;
   bool mVRControllersRequested;
   bool mFrameStarted;
   uint32_t mTaskInterval;
   RefPtr<nsITimer> mTaskTimer;
   mozilla::Monitor mCurrentSubmitTaskMonitor;
   RefPtr<CancelableRunnable> mCurrentSubmitTask;
   uint64_t mLastSubmittedFrameId;
   uint64_t mLastStartedFrame;
-  bool mEnumerationCompleted;
+  VRDisplayCapabilityFlags mRuntimeSupportFlags;
   bool mAppPaused;
 
   // Note: mShmem doesn't support RefPtr; thus, do not share this private
   // pointer so that its lifetime can still be controlled by VRManager
   VRShMem* mShmem;
   bool mVRProcessEnabled;
 
 #if !defined(MOZ_WIDGET_ANDROID)
--- a/gfx/vr/ipc/PVRManager.ipdl
+++ b/gfx/vr/ipc/PVRManager.ipdl
@@ -13,16 +13,17 @@ include GamepadEventTypes;
 include "VRMessageUtils.h";
 
 using struct mozilla::gfx::VRFieldOfView from "gfxVR.h";
 using struct mozilla::gfx::VRDisplayInfo from "gfxVR.h";
 using struct mozilla::gfx::VRSensorUpdate from "gfxVR.h";
 using struct mozilla::gfx::VRHMDSensorState from "gfxVR.h";
 using struct mozilla::gfx::VRControllerInfo from "gfxVR.h";
 using struct mozilla::gfx::VRSubmitFrameResultInfo from "gfxVR.h";
+using mozilla::gfx::VRDisplayCapabilityFlags from "moz_external_vr.h";
 using mozilla::layers::LayersBackend from "mozilla/layers/LayersTypes.h";
 using mozilla::layers::TextureFlags from "mozilla/layers/CompositorTypes.h";
 
 
 namespace mozilla {
 namespace gfx {
 
 /**
@@ -32,16 +33,20 @@ namespace gfx {
  */
 sync protocol PVRManager
 {
   manages PVRLayer;
 
 parent:
   async PVRLayer(uint32_t aDisplayID, uint32_t aGroup);
 
+  // Detect runtime capabilities.  This will return the presense of VR and/or AR
+  // runtime software, without enumerating or activating any hardware devices.
+  async DetectRuntimes();
+
   // (Re)Enumerate VR Displays.  An updated list of VR displays will be returned
   // asynchronously to children via UpdateDisplayInfo.
   async RefreshDisplays();
 
   async SetGroupMask(uint32_t aDisplayID, uint32_t aGroupMask);
   async SetHaveEventListener(bool aHaveEventListener);
 
   async ControllerListenerAdded();
@@ -57,17 +62,19 @@ parent:
   async RunPuppet(uint64_t[] buffer);
   async ResetPuppet();
 
 child:
   // Notify children of updated VR display enumeration and details.  This will
   // be sent to all children when the parent receives RefreshDisplays, even
   // if no changes have been detected.  This ensures that Promises exposed
   // through DOM calls are always resolved.
-  async UpdateDisplayInfo(VRDisplayInfo[] aDisplayUpdates);
+  async UpdateDisplayInfo(VRDisplayInfo aDisplayInfo);
+
+  async UpdateRuntimeCapabilities(VRDisplayCapabilityFlags aCapabilities);
 
   async ReplyGamepadVibrateHaptic(uint32_t aPromiseID);
   async NotifyPuppetCommandBufferCompleted(bool aSuccess);
   async NotifyPuppetResetComplete();
 
   async __delete__();
 
 };
--- a/gfx/vr/ipc/VRManagerChild.cpp
+++ b/gfx/vr/ipc/VRManagerChild.cpp
@@ -30,17 +30,17 @@ namespace mozilla {
 namespace gfx {
 
 static StaticRefPtr<VRManagerChild> sVRManagerChildSingleton;
 static StaticRefPtr<VRManagerParent> sVRManagerParentSingleton;
 
 void ReleaseVRManagerParentSingleton() { sVRManagerParentSingleton = nullptr; }
 
 VRManagerChild::VRManagerChild()
-    : mDisplaysInitialized(false),
+    : mRuntimeCapabilities(VRDisplayCapabilityFlags::Cap_None),
       mMessageLoop(MessageLoop::current()),
       mFrameRequestCallbackCounter(0),
       mWaitingForEnumeration(false),
       mBackend(layers::LayersBackend::LAYERS_NONE) {
   MOZ_ASSERT(NS_IsMainThread());
 
   mStartTimeStamp = TimeStamp::Now();
 }
@@ -160,30 +160,29 @@ PVRLayerChild* VRManagerChild::AllocPVRL
                                                   const uint32_t& aGroup) {
   return VRLayerChild::CreateIPDLActor();
 }
 
 bool VRManagerChild::DeallocPVRLayerChild(PVRLayerChild* actor) {
   return VRLayerChild::DestroyIPDLActor(actor);
 }
 
-void VRManagerChild::UpdateDisplayInfo(
-    nsTArray<VRDisplayInfo>& aDisplayUpdates) {
+void VRManagerChild::UpdateDisplayInfo(const VRDisplayInfo& aDisplayInfo) {
   nsTArray<uint32_t> disconnectedDisplays;
   nsTArray<uint32_t> connectedDisplays;
 
   nsTArray<RefPtr<VRDisplayClient>> prevDisplays;
   prevDisplays = mDisplays;
 
   // Check if any displays have been disconnected
   for (auto& display : prevDisplays) {
     bool found = false;
-    for (auto& displayUpdate : aDisplayUpdates) {
+    if (aDisplayInfo.GetDisplayID() != 0) {
       if (display->GetDisplayInfo().GetDisplayID() ==
-          displayUpdate.GetDisplayID()) {
+          aDisplayInfo.GetDisplayID()) {
         found = true;
         break;
       }
     }
     if (!found) {
       // In order to make the current VRDisplay can continue to apply for the
       // newest VRDisplayInfo, we need to exit presentionation before
       // disconnecting.
@@ -199,56 +198,78 @@ void VRManagerChild::UpdateDisplayInfo(
       disconnectedDisplays.AppendElement(
           display->GetDisplayInfo().GetDisplayID());
     }
   }
 
   // mDisplays could be a hashed container for more scalability, but not worth
   // it now as we expect < 10 entries.
   nsTArray<RefPtr<VRDisplayClient>> displays;
-  for (VRDisplayInfo& displayUpdate : aDisplayUpdates) {
+  if (aDisplayInfo.GetDisplayID() != 0) {
     bool isNewDisplay = true;
     for (auto& display : prevDisplays) {
       const VRDisplayInfo& prevInfo = display->GetDisplayInfo();
-      if (prevInfo.GetDisplayID() == displayUpdate.GetDisplayID()) {
-        if (displayUpdate.GetIsConnected() && !prevInfo.GetIsConnected()) {
-          connectedDisplays.AppendElement(displayUpdate.GetDisplayID());
+      if (prevInfo.GetDisplayID() == aDisplayInfo.GetDisplayID()) {
+        if (aDisplayInfo.GetIsConnected() && !prevInfo.GetIsConnected()) {
+          connectedDisplays.AppendElement(aDisplayInfo.GetDisplayID());
         }
-        if (!displayUpdate.GetIsConnected() && prevInfo.GetIsConnected()) {
-          disconnectedDisplays.AppendElement(displayUpdate.GetDisplayID());
+        if (!aDisplayInfo.GetIsConnected() && prevInfo.GetIsConnected()) {
+          disconnectedDisplays.AppendElement(aDisplayInfo.GetDisplayID());
         }
-        display->UpdateDisplayInfo(displayUpdate);
+        display->UpdateDisplayInfo(aDisplayInfo);
         displays.AppendElement(display);
         isNewDisplay = false;
         break;
       }
     }
     if (isNewDisplay) {
-      displays.AppendElement(new VRDisplayClient(displayUpdate));
-      connectedDisplays.AppendElement(displayUpdate.GetDisplayID());
+      displays.AppendElement(new VRDisplayClient(aDisplayInfo));
+      connectedDisplays.AppendElement(aDisplayInfo.GetDisplayID());
     }
   }
 
   mDisplays = displays;
 
   // We wish to fire the events only after mDisplays is updated
   for (uint32_t displayID : disconnectedDisplays) {
     FireDOMVRDisplayDisconnectEvent(displayID);
   }
 
   for (uint32_t displayID : connectedDisplays) {
     FireDOMVRDisplayConnectEvent(displayID);
   }
+}
 
-  mDisplaysInitialized = true;
+bool VRManagerChild::RuntimeSupportsVR() const {
+  return bool(mRuntimeCapabilities & VRDisplayCapabilityFlags::Cap_ImmersiveVR);
+}
+bool VRManagerChild::RuntimeSupportsAR() const {
+  return bool(mRuntimeCapabilities & VRDisplayCapabilityFlags::Cap_ImmersiveAR);
+}
+
+mozilla::ipc::IPCResult VRManagerChild::RecvUpdateRuntimeCapabilities(
+    const VRDisplayCapabilityFlags& aCapabilities) {
+  mRuntimeCapabilities = aCapabilities;
+  nsContentUtils::AddScriptRunner(NewRunnableMethod<>(
+      "gfx::VRManagerChild::NotifyRuntimeCapabilitiesUpdatedInternal", this,
+      &VRManagerChild::NotifyRuntimeCapabilitiesUpdatedInternal));
+  return IPC_OK();
+}
+
+void VRManagerChild::NotifyRuntimeCapabilitiesUpdatedInternal() {
+  nsTArray<RefPtr<VRManagerEventObserver>> listeners;
+  listeners = mListeners;
+  for (auto& listener : listeners) {
+    listener->NotifyDetectRuntimesCompleted();
+  }
 }
 
 mozilla::ipc::IPCResult VRManagerChild::RecvUpdateDisplayInfo(
-    nsTArray<VRDisplayInfo>&& aDisplayUpdates) {
-  UpdateDisplayInfo(aDisplayUpdates);
+    const VRDisplayInfo& aDisplayInfo) {
+  UpdateDisplayInfo(aDisplayInfo);
   for (auto& windowId : mNavigatorCallbacks) {
     /** We must call NotifyVRDisplaysUpdated for every
      * window's Navigator in mNavigatorCallbacks to ensure that
      * the promise returned by Navigator.GetVRDevices
      * can resolve.  This must happen even if no changes
      * to VRDisplays have been detected here.
      */
     nsGlobalWindowInner* window =
@@ -334,16 +355,18 @@ bool VRManagerChild::RefreshVRDisplaysWi
 bool VRManagerChild::EnumerateVRDisplays() {
   bool success = SendRefreshDisplays();
   if (success) {
     mWaitingForEnumeration = true;
   }
   return success;
 }
 
+void VRManagerChild::DetectRuntimes() { Unused << SendDetectRuntimes(); }
+
 PVRLayerChild* VRManagerChild::CreateVRLayer(uint32_t aDisplayID,
                                              nsIEventTarget* aTarget,
                                              uint32_t aGroup) {
   PVRLayerChild* vrLayerChild = AllocPVRLayerChild(aDisplayID, aGroup);
   // Do the DOM labeling.
   if (aTarget) {
     SetEventTargetForActor(vrLayerChild, aTarget);
     MOZ_ASSERT(vrLayerChild->GetActorEventTarget());
--- a/gfx/vr/ipc/VRManagerChild.h
+++ b/gfx/vr/ipc/VRManagerChild.h
@@ -36,16 +36,17 @@ class VRManagerEventObserver {
   virtual void NotifyVRDisplayMounted(uint32_t aDisplayID) = 0;
   virtual void NotifyVRDisplayUnmounted(uint32_t aDisplayID) = 0;
   virtual void NotifyVRDisplayConnect(uint32_t aDisplayID) = 0;
   virtual void NotifyVRDisplayDisconnect(uint32_t aDisplayID) = 0;
   virtual void NotifyVRDisplayPresentChange(uint32_t aDisplayID) = 0;
   virtual void NotifyPresentationGenerationChanged(uint32_t aDisplayID) = 0;
   virtual bool GetStopActivityStatus() const = 0;
   virtual void NotifyEnumerationCompleted() = 0;
+  virtual void NotifyDetectRuntimesCompleted() = 0;
 
  protected:
   virtual ~VRManagerEventObserver() = default;
 };
 
 class VRManagerChild : public PVRManagerChild {
   friend class PVRManagerChild;
 
@@ -55,20 +56,23 @@ class VRManagerChild : public PVRManager
   static VRManagerChild* Get();
 
   // Indicate that an observer wants to receive VR events.
   void AddListener(VRManagerEventObserver* aObserver);
   // Indicate that an observer should no longer receive VR events.
   void RemoveListener(VRManagerEventObserver* aObserver);
   void StartActivity();
   void StopActivity();
+  bool RuntimeSupportsVR() const;
+  bool RuntimeSupportsAR() const;
 
   void GetVRDisplays(nsTArray<RefPtr<VRDisplayClient>>& aDisplays);
   bool RefreshVRDisplaysWithCallback(uint64_t aWindowId);
   bool EnumerateVRDisplays();
+  void DetectRuntimes();
   void AddPromise(const uint32_t& aID, dom::Promise* aPromise);
 
   static void InitSameProcess();
   static void InitWithGPUProcess(Endpoint<PVRManagerChild>&& aEndpoint);
   static bool InitForContent(Endpoint<PVRManagerChild>&& aEndpoint);
   static bool ReinitForContent(Endpoint<PVRManagerChild>&& aEndpoint);
   static void ShutDown();
 
@@ -86,17 +90,17 @@ class VRManagerChild : public PVRManager
   nsresult ScheduleFrameRequestCallback(
       mozilla::dom::FrameRequestCallback& aCallback, int32_t* aHandle);
   void CancelFrameRequestCallback(int32_t aHandle);
   MOZ_CAN_RUN_SCRIPT
   void RunFrameRequestCallbacks();
   void NotifyPresentationGenerationChanged(uint32_t aDisplayID);
 
   MOZ_CAN_RUN_SCRIPT
-  void UpdateDisplayInfo(nsTArray<VRDisplayInfo>& aDisplayUpdates);
+  void UpdateDisplayInfo(const VRDisplayInfo& aDisplayInfo);
   void FireDOMVRDisplayMountedEvent(uint32_t aDisplayID);
   void FireDOMVRDisplayUnmountedEvent(uint32_t aDisplayID);
   void FireDOMVRDisplayConnectEvent(uint32_t aDisplayID);
   void FireDOMVRDisplayDisconnectEvent(uint32_t aDisplayID);
   void FireDOMVRDisplayPresentChangeEvent(uint32_t aDisplayID);
   void FireDOMVRDisplayConnectEventsForLoad(VRManagerEventObserver* aObserver);
 
   virtual void HandleFatalError(const char* aMsg) const override;
@@ -114,17 +118,19 @@ class VRManagerChild : public PVRManager
   PVRLayerChild* AllocPVRLayerChild(const uint32_t& aDisplayID,
                                     const uint32_t& aGroup);
   bool DeallocPVRLayerChild(PVRLayerChild* actor);
 
   // MOZ_CAN_RUN_SCRIPT_BOUNDARY until we can mark ipdl-generated things as
   // MOZ_CAN_RUN_SCRIPT.
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   mozilla::ipc::IPCResult RecvUpdateDisplayInfo(
-      nsTArray<VRDisplayInfo>&& aDisplayUpdates);
+      const VRDisplayInfo& aDisplayInfo);
+  mozilla::ipc::IPCResult RecvUpdateRuntimeCapabilities(
+      const VRDisplayCapabilityFlags& aCapabilities);
   mozilla::ipc::IPCResult RecvReplyGamepadVibrateHaptic(
       const uint32_t& aPromiseID);
 
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   mozilla::ipc::IPCResult RecvNotifyPuppetCommandBufferCompleted(bool aSuccess);
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   mozilla::ipc::IPCResult RecvNotifyPuppetResetComplete();
 
@@ -135,18 +141,20 @@ class VRManagerChild : public PVRManager
   void FireDOMVRDisplayUnmountedEventInternal(uint32_t aDisplayID);
   void FireDOMVRDisplayConnectEventInternal(uint32_t aDisplayID);
   void FireDOMVRDisplayDisconnectEventInternal(uint32_t aDisplayID);
   void FireDOMVRDisplayPresentChangeEventInternal(uint32_t aDisplayID);
   void FireDOMVRDisplayConnectEventsForLoadInternal(
       uint32_t aDisplayID, VRManagerEventObserver* aObserver);
   void NotifyPresentationGenerationChangedInternal(uint32_t aDisplayID);
   void NotifyEnumerationCompletedInternal();
+  void NotifyRuntimeCapabilitiesUpdatedInternal();
 
   nsTArray<RefPtr<VRDisplayClient>> mDisplays;
+  VRDisplayCapabilityFlags mRuntimeCapabilities;
   bool mDisplaysInitialized;
   nsTArray<uint64_t> mNavigatorCallbacks;
 
   MessageLoop* mMessageLoop;
 
   struct FrameRequest;
 
   nsTArray<FrameRequest> mFrameRequestCallbacks;
--- a/gfx/vr/ipc/VRManagerParent.cpp
+++ b/gfx/vr/ipc/VRManagerParent.cpp
@@ -19,17 +19,17 @@ namespace mozilla {
 using namespace layers;
 namespace gfx {
 
 VRManagerParent::VRManagerParent(ProcessId aChildProcessId,
                                  bool aIsContentChild)
     : mHaveEventListener(false),
       mHaveControllerListener(false),
       mIsContentChild(aIsContentChild),
-      mVRActiveStatus(true) {
+      mVRActiveStatus(false) {
   MOZ_COUNT_CTOR(VRManagerParent);
   MOZ_ASSERT(NS_IsMainThread());
 
   SetOtherProcessId(aChildProcessId);
 }
 
 VRManagerParent::~VRManagerParent() {
   MOZ_ASSERT(!mVRManagerHolder);
@@ -135,23 +135,34 @@ void VRManagerParent::ActorDestroy(Actor
       NewRunnableMethod("gfx::VRManagerParent::DeferredDestroy", this,
                         &VRManagerParent::DeferredDestroy));
 }
 
 void VRManagerParent::OnChannelConnected(int32_t aPid) {
   mCompositorThreadHolder = CompositorThreadHolder::GetSingleton();
 }
 
+mozilla::ipc::IPCResult VRManagerParent::RecvDetectRuntimes() {
+  // Detect runtime capabilities. This will return the presense of VR and/or AR
+  // runtime software, without enumerating or activating any hardware devices.
+  // UpdateDisplayInfo will be sent to VRManagerChild with the results of the
+  // detection.
+  VRManager* vm = VRManager::Get();
+  vm->DetectRuntimes();
+
+  return IPC_OK();
+}
+
 mozilla::ipc::IPCResult VRManagerParent::RecvRefreshDisplays() {
-  // This is called to refresh the VR Displays for Navigator.GetVRDevices().
-  // We must pass "true" to VRManager::RefreshVRDisplays()
-  // to ensure that the promise returned by Navigator.GetVRDevices
-  // can resolve even if there are no changes to the VR Displays.
+  // This is called to activate the VR runtimes, detecting the
+  // presence and capabilities of XR hardware.
+  // UpdateDisplayInfo will be sent to VRManagerChild with the results of the
+  // enumerated hardware.
   VRManager* vm = VRManager::Get();
-  vm->RefreshVRDisplays(true);
+  vm->EnumerateDevices();
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult VRManagerParent::RecvSetGroupMask(
     const uint32_t& aDisplayID, const uint32_t& aGroupMask) {
   VRManager* vm = VRManager::Get();
   vm->SetGroupMask(aGroupMask);
--- a/gfx/vr/ipc/VRManagerParent.h
+++ b/gfx/vr/ipc/VRManagerParent.h
@@ -44,16 +44,17 @@ class VRManagerParent final : public PVR
 
   PVRLayerParent* AllocPVRLayerParent(const uint32_t& aDisplayID,
                                       const uint32_t& aGroup);
   bool DeallocPVRLayerParent(PVRLayerParent* actor);
 
   virtual void ActorDestroy(ActorDestroyReason why) override;
   void OnChannelConnected(int32_t pid) override;
 
+  mozilla::ipc::IPCResult RecvDetectRuntimes();
   mozilla::ipc::IPCResult RecvRefreshDisplays();
   mozilla::ipc::IPCResult RecvSetGroupMask(const uint32_t& aDisplayID,
                                            const uint32_t& aGroupMask);
   mozilla::ipc::IPCResult RecvSetHaveEventListener(
       const bool& aHaveEventListener);
   mozilla::ipc::IPCResult RecvControllerListenerAdded();
   mozilla::ipc::IPCResult RecvControllerListenerRemoved();
   mozilla::ipc::IPCResult RecvVibrateHaptic(const uint32_t& aControllerIdx,
--- a/gfx/vr/ipc/VRMessageUtils.h
+++ b/gfx/vr/ipc/VRMessageUtils.h
@@ -59,11 +59,17 @@ struct ParamTraits<mozilla::gfx::VRSubmi
         !ReadParam(aMsg, aIter, &(aResult->mFrameNum))) {
       return false;
     }
 
     return true;
   }
 };
 
+template <>
+struct ParamTraits<mozilla::gfx::VRDisplayCapabilityFlags>
+    : public BitFlagsEnumSerializer<
+          mozilla::gfx::VRDisplayCapabilityFlags,
+          mozilla::gfx::VRDisplayCapabilityFlags::Cap_All> {};
+
 }  // namespace IPC
 
 #endif  // mozilla_gfx_vr_VRMessageUtils_h
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -2446,16 +2446,36 @@
   type: RelaxedAtomicBool
 #if defined(XP_WIN) || defined(XP_DARWIN) || !defined(RELEASE_OR_BETA)
   value: true
 #else
   value: false
 #endif
   mirror: always
 
+# Should VR sessions always be reported as supported, without first
+# checking for VR runtimes?  This will prevent permission prompts
+# from being suppressed on machines without VR runtimes and cause
+# navigatior.xr.isSessionSupported to always report that immersive-vr
+# is supported.
+- name: dom.vr.always_support_vr
+  type: RelaxedAtomicBool
+  value: false
+  mirror: always
+
+# Should AR sessions always be reported as supported, without first
+# checking for AR runtimes?  This will prevent permission prompts
+# from being suppressed on machines without AR runtimes and cause
+# navigatior.xr.isSessionSupported to always report that immersive-ar
+# is supported.
+- name: dom.vr.always_support_ar
+  type: RelaxedAtomicBool
+  value: false
+  mirror: always
+
 # It is often desirable to automatically start vr presentation when
 # a user puts on the VR headset.  This is done by emitting the
 # Window.vrdisplayactivate event when the headset's sensors detect it
 # being worn.  This can result in WebVR content taking over the headset
 # when the user is using it outside the browser or inadvertent start of
 # presentation due to the high sensitivity of the proximity sensor in some
 # headsets, so it is off by default.
 - name: dom.vr.autoactivate.enabled
--- a/testing/crashtest/crashtests.list
+++ b/testing/crashtest/crashtests.list
@@ -23,16 +23,17 @@ include ../../dom/media/mediasource/test
 include ../../dom/media/test/crashtests/crashtests.list
 skip-if(!webrtc) include ../../dom/media/tests/crashtests/crashtests.list
 include ../../dom/media/webspeech/synth/crashtests/crashtests.list
 include ../../dom/offline/crashtests/crashtests.list
 include ../../dom/plugins/test/crashtests/crashtests.list
 include ../../dom/security/test/crashtests/crashtests.list
 include ../../dom/smil/crashtests/crashtests.list
 include ../../dom/svg/crashtests/crashtests.list
+include ../../dom/vr/test/crashtests/crashtests.list
 include ../../dom/workers/test/crashtests/crashtests.list
 include ../../dom/xhr/tests/crashtests/crashtests.list
 include ../../dom/xml/crashtests/crashtests.list
 include ../../dom/xslt/crashtests/crashtests.list
 include ../../dom/xul/crashtests/crashtests.list
 
 include ../../editor/composer/crashtests/crashtests.list
 include ../../editor/libeditor/crashtests/crashtests.list