Bug 1407423 - Part 3: Update Puppet Device Implementation draft
authorKearwood "Kip" Gilbert <kgilbert@mozilla.com>
Tue, 14 Nov 2017 16:03:50 -0800
changeset 701589 438d2635dd0805fd373584616dbfdee2b2912066
parent 701588 eaa768231bf18b7df5cfb633675807933b9613c3
child 741212 3a8c6fbb1c8708b47d70e9d09afcaf451bc8be94
push id90210
push userbmo:kgilbert@mozilla.com
push dateTue, 21 Nov 2017 21:42:05 +0000
bugs1407423
milestone59.0a1
Bug 1407423 - Part 3: Update Puppet Device Implementation - Update prefs to accomodate tests, disabling enumeration throttling - Updated Puppet display and controller implementation to act more like the actual devices. - Updated tests to ensure that they explicitly create a VR mock display and don't create duplicate mock displays. MozReview-Commit-ID: 7GSoYVxmqgM
dom/vr/test/mochitest/VRSimulationDriver.js
dom/vr/test/mochitest/runVRTest.js
dom/vr/test/mochitest/test_vrController_displayId.html
dom/vr/test/mochitest/test_vrDisplay_canvas2d.html
dom/vr/test/mochitest/test_vrDisplay_exitPresent.html
dom/vr/test/reftest/VRSimulationDriver.js
dom/vr/test/reftest/reftest.list
gfx/vr/VRManager.cpp
gfx/vr/VRManager.h
gfx/vr/gfxVRPuppet.cpp
gfx/vr/gfxVRPuppet.h
gfx/vr/ipc/VRManagerChild.cpp
gfx/vr/ipc/VRManagerParent.cpp
gfx/vr/ipc/VRManagerParent.h
--- a/dom/vr/test/mochitest/VRSimulationDriver.js
+++ b/dom/vr/test/mochitest/VRSimulationDriver.js
@@ -1,16 +1,20 @@
 
 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;
 };
--- a/dom/vr/test/mochitest/runVRTest.js
+++ b/dom/vr/test/mochitest/runVRTest.js
@@ -1,9 +1,11 @@
 function runVRTest(callback) {
   SpecialPowers.pushPrefEnv({"set" : [["dom.vr.puppet.enabled", true],
                                       ["dom.vr.require-gesture", false],
-                                      ["dom.vr.test.enabled", true]]},
+                                      ["dom.vr.test.enabled", true],
+                                      ["dom.vr.display.enumerate.interval", 0],
+                                      ["dom.vr.controller.enumerate.interval", 0]]},
   () => {
     VRServiceTest = navigator.requestVRServiceTest();
     callback();
   });
 }
\ No newline at end of file
--- a/dom/vr/test/mochitest/test_vrController_displayId.html
+++ b/dom/vr/test/mochitest/test_vrController_displayId.html
@@ -33,22 +33,24 @@
               }
           });
         }, "Finish to verify VRController.displayId.");
       }
 
       function startTest() {
         promise_test((test) => {
           listenControllerEvents();
-          return navigator.getVRDisplays().then((displays) => {
-            vrDisplay = displays[0];
-            assert_equals(displays.length, 1, "displays.length must be one after attach.");
-            assert_equals(displays[0].displayId, 1, "displayId must be one.");
-            addController();
-            addController();
+          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(displays[0].displayId, 1, "displayId must be one.");
+              addController();
+              addController();
+            });
           });
         }, "Finish to add VRDisplay.");
       }
 
       runVRTest(startTest);
     </script>
   </body>
 </html>
\ No newline at end of file
--- a/dom/vr/test/mochitest/test_vrDisplay_canvas2d.html
+++ b/dom/vr/test/mochitest/test_vrDisplay_canvas2d.html
@@ -1,16 +1,17 @@
 <!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() {
@@ -30,22 +31,24 @@
       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 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();
+          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>
--- a/dom/vr/test/mochitest/test_vrDisplay_exitPresent.html
+++ b/dom/vr/test/mochitest/test_vrDisplay_exitPresent.html
@@ -1,44 +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 content.navigator.getVRDisplays().then((displays) => {
+        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;
-        initVRPresentation(frame1).then(() => {
-          promise_test((test) => {
-            return promise_rejects(test, null, testExitPresentOnOtherIframe(frame2));
-          }, "We cannot exist VR presentation established by another content, this promise is expected to be rejected.")
-        });
+        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>
\ No newline at end of file
--- a/dom/vr/test/reftest/VRSimulationDriver.js
+++ b/dom/vr/test/reftest/VRSimulationDriver.js
@@ -1,16 +1,20 @@
 
 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;
 };
 
--- a/dom/vr/test/reftest/reftest.list
+++ b/dom/vr/test/reftest/reftest.list
@@ -1,10 +1,10 @@
 # WebVR Reftests
 # Please confirm there is no other VR display connected. Otherwise, VRPuppetDisplay can't be attached.
-default-preferences 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)
+default-preferences 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)
 
 # 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||/^Windows\x20NT\x206\.1/.test(http.oscpu)) == 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,1,1200).
 fuzzy-if(cocoaWidget,1,600) skip-if((!winWidget&&release_or_beta)||Android||gtkWidget||!layersGPUAccelerated||/^Windows\x20NT\x206\.1/.test(http.oscpu)) == change_size.html wrapper.html?change_size.png
--- a/gfx/vr/VRManager.cpp
+++ b/gfx/vr/VRManager.cpp
@@ -50,17 +50,16 @@ VRManager::ManagerInit()
   if (sVRManagerSingleton == nullptr) {
     sVRManagerSingleton = new VRManager();
     ClearOnShutdown(&sVRManagerSingleton);
   }
 }
 
 VRManager::VRManager()
   : mInitialized(false)
-  , mVRTestSystemCreated(false)
   , mVRDisplaysRequested(false)
   , mVRControllersRequested(false)
 {
   MOZ_COUNT_CTOR(VRManager);
   MOZ_ASSERT(sVRManagerSingleton == nullptr);
 
   RefPtr<VRSystemManager> mgr;
 
@@ -493,25 +492,30 @@ VRManager::RemoveControllers()
     mManagers[i]->RemoveControllers();
   }
   mVRControllers.Clear();
 }
 
 void
 VRManager::CreateVRTestSystem()
 {
-  if (mVRTestSystemCreated) {
+  if (mPuppetManager) {
+    mPuppetManager->ClearTestDisplays();
     return;
   }
 
-  RefPtr<VRSystemManager> mgr = VRSystemManagerPuppet::Create();
-  if (mgr) {
-    mManagers.AppendElement(mgr);
-    mVRTestSystemCreated = true;
-  }
+  mPuppetManager = VRSystemManagerPuppet::Create();
+  mManagers.AppendElement(mPuppetManager);
+}
+
+VRSystemManagerPuppet*
+VRManager::GetPuppetManager()
+{
+  MOZ_ASSERT(mPuppetManager);
+  return mPuppetManager;
 }
 
 template<class T>
 void
 VRManager::NotifyGamepadChange(uint32_t aIndex, const T& aInfo)
 {
   dom::GamepadChangeEventBody body(aInfo);
   dom::GamepadChangeEvent e(aIndex, dom::GamepadServiceType::VR, body);
--- a/gfx/vr/VRManager.h
+++ b/gfx/vr/VRManager.h
@@ -18,16 +18,17 @@ namespace mozilla {
 namespace layers {
 class TextureHost;
 }
 namespace gfx {
 
 class VRLayerParent;
 class VRManagerParent;
 class VRDisplayHost;
+class VRSystemManagerPuppet;
 
 class VRManager
 {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::gfx::VRManager)
 
 public:
   static void ManagerInit();
   static VRManager* Get();
@@ -42,16 +43,18 @@ public:
   void ScanForControllers();
   void RemoveControllers();
   template<class T> void NotifyGamepadChange(uint32_t aIndex, const T& aInfo);
   RefPtr<gfx::VRDisplayHost> GetDisplay(const uint32_t& aDisplayID);
   void GetVRDisplayInfo(nsTArray<VRDisplayInfo>& aDisplayInfo);
   RefPtr<gfx::VRControllerHost> GetController(const uint32_t& aControllerID);
   void GetVRControllerInfo(nsTArray<VRControllerInfo>& aControllerInfo);
   void CreateVRTestSystem();
+  VRSystemManagerPuppet* GetPuppetManager();
+
   void VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
                      double aIntensity, double aDuration, uint32_t aPromiseID);
   void StopVibrateHaptic(uint32_t aControllerIdx);
   void NotifyVibrateHapticCompleted(uint32_t aPromiseID);
   void DispatchSubmitFrameResult(uint32_t aDisplayID, const VRSubmitFrameResultInfo& aResult);
 
 protected:
   VRManager();
@@ -80,17 +83,17 @@ private:
   typedef nsRefPtrHashtable<nsUint32HashKey, gfx::VRControllerHost> VRControllerHostHashMap;
   VRControllerHostHashMap mVRControllers;
 
   Atomic<bool> mInitialized;
 
   TimeStamp mLastControllerEnumerationTime;
   TimeStamp mLastDisplayEnumerationTime;
   TimeStamp mLastActiveTime;
-  bool mVRTestSystemCreated;
+  RefPtr<VRSystemManagerPuppet> mPuppetManager;
   bool mVRDisplaysRequested;
   bool mVRControllersRequested;
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif // GFX_VR_MANAGER_H
--- a/gfx/vr/gfxVRPuppet.cpp
+++ b/gfx/vr/gfxVRPuppet.cpp
@@ -670,16 +670,19 @@ VRControllerPuppet::GetAxisMove(uint32_t
 
 void
 VRControllerPuppet::SetAxisMove(uint32_t aAxis, float aValue)
 {
   mAxisMove[aAxis] = aValue;
 }
 
 VRSystemManagerPuppet::VRSystemManagerPuppet()
+  : mPuppetDisplayCount(0)
+  , mPuppetDisplayInfo{}
+  , mPuppetDisplaySensorState{}
 {
 }
 
 /*static*/ already_AddRefed<VRSystemManagerPuppet>
 VRSystemManagerPuppet::Create()
 {
   if (!gfxPrefs::VREnabled() || !gfxPrefs::VRPuppetEnabled()) {
     return nullptr;
@@ -693,68 +696,105 @@ void
 VRSystemManagerPuppet::Destroy()
 {
   Shutdown();
 }
 
 void
 VRSystemManagerPuppet::Shutdown()
 {
-  mPuppetHMD = nullptr;
+  mPuppetHMDs.Clear();
 }
 
 void
 VRSystemManagerPuppet::NotifyVSync()
 {
   VRSystemManager::NotifyVSync();
-  if (mPuppetHMD) {
-    mPuppetHMD->Refresh();
+
+  for (const auto& display: mPuppetHMDs) {
+    display->Refresh();
   }
 }
 
+uint32_t
+VRSystemManagerPuppet::CreateTestDisplay()
+{
+  if (mPuppetDisplayCount >= kMaxPuppetDisplays) {
+    MOZ_ASSERT(false);
+    return mPuppetDisplayCount;
+  }
+  return mPuppetDisplayCount++;
+}
+
+void
+VRSystemManagerPuppet::ClearTestDisplays()
+{
+  mPuppetDisplayCount = 0;
+}
+
 void
 VRSystemManagerPuppet::Enumerate()
 {
-  if (mPuppetHMD == nullptr) {
-    mPuppetHMD = new VRDisplayPuppet();
+  while (mPuppetHMDs.Length() < mPuppetDisplayCount) {
+    VRDisplayPuppet* puppetDisplay = new VRDisplayPuppet();
+    uint32_t deviceID = mPuppetHMDs.Length();
+    puppetDisplay->SetDisplayInfo(mPuppetDisplayInfo[deviceID]);
+    puppetDisplay->SetSensorState(mPuppetDisplaySensorState[deviceID]);
+    mPuppetHMDs.AppendElement(puppetDisplay);
+  }
+  while (mPuppetHMDs.Length() > mPuppetDisplayCount) {
+    mPuppetHMDs.RemoveElementAt(mPuppetHMDs.Length() - 1);
   }
 }
 
-bool
-VRSystemManagerPuppet::ShouldInhibitEnumeration()
+void
+VRSystemManagerPuppet::SetPuppetDisplayInfo(const uint32_t& aDeviceID,
+                                            const VRDisplayInfo& aDisplayInfo)
 {
-  if (VRSystemManager::ShouldInhibitEnumeration()) {
-    return true;
+  if (aDeviceID >= mPuppetDisplayCount) {
+    MOZ_ASSERT(false);
+    return;
+  }
+  mPuppetDisplayInfo[aDeviceID] = aDisplayInfo;
+  if (mPuppetHMDs.Length() > aDeviceID) {
+    mPuppetHMDs[aDeviceID]->SetDisplayInfo(aDisplayInfo);
   }
-  if (mPuppetHMD) {
-    // When we find an a VR device, don't
-    // allow any further enumeration as it
-    // may get picked up redundantly by other
-    // API's.
-    return true;
+}
+
+void
+VRSystemManagerPuppet::SetPuppetDisplaySensorState(const uint32_t& aDeviceID,
+                                                   const VRHMDSensorState& aSensorState)
+{
+  if (aDeviceID >= mPuppetDisplayCount) {
+    MOZ_ASSERT(false);
+    return;
   }
-  return false;
+  mPuppetDisplaySensorState[aDeviceID] = aSensorState;
+  if (mPuppetHMDs.Length() > aDeviceID) {
+    mPuppetHMDs[aDeviceID]->SetSensorState(aSensorState);
+  }
 }
 
 void
 VRSystemManagerPuppet::GetHMDs(nsTArray<RefPtr<VRDisplayHost>>& aHMDResult)
 {
-  if (mPuppetHMD) {
-    aHMDResult.AppendElement(mPuppetHMD);
+  for (auto display: mPuppetHMDs) {
+    aHMDResult.AppendElement(display);
   }
 }
 
 bool
 VRSystemManagerPuppet::GetIsPresenting()
 {
-  if (mPuppetHMD) {
-    VRDisplayInfo displayInfo(mPuppetHMD->GetDisplayInfo());
-    return displayInfo.GetPresentingGroups() != kVRGroupNone;
+  for (const auto& display: mPuppetHMDs) {
+    const VRDisplayInfo& displayInfo(display->GetDisplayInfo());
+    if (displayInfo.GetPresentingGroups() != kVRGroupNone) {
+      return true;
+    }
   }
-
   return false;
 }
 
 void
 VRSystemManagerPuppet::HandleInput()
 {
   RefPtr<impl::VRControllerPuppet> controller;
   for (uint32_t i = 0; i < mPuppetController.Length(); ++i) {
@@ -846,38 +886,37 @@ VRSystemManagerPuppet::GetControllers(ns
   for (uint32_t i = 0; i < mPuppetController.Length(); ++i) {
     aControllerResult.AppendElement(mPuppetController[i]);
   }
 }
 
 void
 VRSystemManagerPuppet::ScanForControllers()
 {
-  // mPuppetHMD is available after VRDisplay is created
-  // at GetHMDs().
-  if (!mPuppetHMD) {
-    return;
-  }
-  // We make VRSystemManagerPuppet has two controllers always.
-  const uint32_t newControllerCount = 2;
+  // We make sure VRSystemManagerPuppet has two controllers
+  // for each display
+  const uint32_t newControllerCount = mPuppetHMDs.Length() * 2;
 
   if (newControllerCount != mControllerCount) {
     RemoveControllers();
 
     // Re-adding controllers to VRControllerManager.
-    for (uint32_t i = 0; i < newControllerCount; ++i) {
-      dom::GamepadHand hand = (i % 2) ? dom::GamepadHand::Right :
-                                        dom::GamepadHand::Left;
-      RefPtr<VRControllerPuppet> puppetController = new VRControllerPuppet(hand,
-                                                      mPuppetHMD->GetDisplayInfo().GetDisplayID());
-      mPuppetController.AppendElement(puppetController);
+    for (const auto& display: mPuppetHMDs) {
+      uint32_t displayID = display->GetDisplayInfo().GetDisplayID();
+      for (uint32_t i = 0; i < 2; i++) {
+        dom::GamepadHand hand = (i % 2) ? dom::GamepadHand::Right :
+                                          dom::GamepadHand::Left;
+        RefPtr<VRControllerPuppet> puppetController;
+        puppetController = new VRControllerPuppet(hand, displayID);
+        mPuppetController.AppendElement(puppetController);
 
-      // Not already present, add it.
-      AddGamepad(puppetController->GetControllerInfo());
-      ++mControllerCount;
+        // Not already present, add it.
+        AddGamepad(puppetController->GetControllerInfo());
+        ++mControllerCount;
+      }
     }
   }
 }
 
 void
 VRSystemManagerPuppet::RemoveControllers()
 {
   // controller count is changed, removing the existing gamepads first.
--- a/gfx/vr/gfxVRPuppet.h
+++ b/gfx/vr/gfxVRPuppet.h
@@ -4,16 +4,17 @@
  * 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 GFX_VR_PUPPET_H
 #define GFX_VR_PUPPET_H
 
 #include "nsTArray.h"
 #include "mozilla/RefPtr.h"
+#include "nsRefPtrHashtable.h"
 
 #include "gfxVR.h"
 #include "VRDisplayHost.h"
 
 #if defined(XP_MACOSX)
 class MacIOSurface;
 #endif
 namespace mozilla {
@@ -102,21 +103,26 @@ private:
 };
 
 } // namespace impl
 
 class VRSystemManagerPuppet : public VRSystemManager
 {
 public:
   static already_AddRefed<VRSystemManagerPuppet> Create();
+  uint32_t CreateTestDisplay();
+  void ClearTestDisplays();
+  void SetPuppetDisplayInfo(const uint32_t& aDeviceID,
+                            const VRDisplayInfo& aDisplayInfo);
+  void SetPuppetDisplaySensorState(const uint32_t& aDeviceID,
+                                   const VRHMDSensorState& aSensorState);
 
   virtual void Destroy() override;
   virtual void Shutdown() override;
   virtual void Enumerate() override;
-  virtual bool ShouldInhibitEnumeration() override;
   virtual void GetHMDs(nsTArray<RefPtr<VRDisplayHost>>& aHMDResult) override;
   virtual bool GetIsPresenting() override;
   virtual void HandleInput() override;
   virtual void GetControllers(nsTArray<RefPtr<VRControllerHost>>&
                               aControllerResult) override;
   virtual void ScanForControllers() override;
   virtual void RemoveControllers() override;
   virtual void VibrateHaptic(uint32_t aControllerIdx,
@@ -137,17 +143,23 @@ private:
                          uint64_t aButtonPressed,
                          uint64_t aButtonTouched);
   void HandleAxisMove(uint32_t aControllerIndex, uint32_t aAxis,
                       float aValue);
   void HandlePoseTracking(uint32_t aControllerIndex,
                           const dom::GamepadPoseState& aPose,
                           VRControllerHost* aController);
 
-  // there can only be one
-  RefPtr<impl::VRDisplayPuppet> mPuppetHMD;
+  // Enumerated puppet hardware devices, as seen by Web APIs:
+  nsTArray<RefPtr<impl::VRDisplayPuppet>> mPuppetHMDs;
   nsTArray<RefPtr<impl::VRControllerPuppet>> mPuppetController;
+
+  // Emulated hardware state, persistent through VRSystemManager::Shutdown():
+  static const uint32_t kMaxPuppetDisplays = 5;
+  uint32_t mPuppetDisplayCount;
+  VRDisplayInfo mPuppetDisplayInfo[kMaxPuppetDisplays];
+  VRHMDSensorState mPuppetDisplaySensorState[kMaxPuppetDisplays];
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif  /* GFX_VR_PUPPET_H*/
--- a/gfx/vr/ipc/VRManagerChild.cpp
+++ b/gfx/vr/ipc/VRManagerChild.cpp
@@ -400,17 +400,24 @@ VRManagerChild::RecvReplyCreateVRService
                                                        const uint32_t& aPromiseID,
                                                        const uint32_t& aDeviceID)
 {
   RefPtr<dom::Promise> p;
   if (!mPromiseList.Get(aPromiseID, getter_AddRefs(p))) {
     MOZ_CRASH("We should always have a promise.");
   }
 
-  p->MaybeResolve(new VRMockController(aID, aDeviceID));
+  if (aDeviceID == 0) {
+    // A value of 0 indicates that the controller could not
+    // be created.  Most likely due to having no VR display
+    // to associate it with.
+    p->MaybeRejectWithUndefined();
+  } else {
+    p->MaybeResolve(new VRMockController(aID, aDeviceID));
+  }
   mPromiseList.Remove(aPromiseID);
   return IPC_OK();
 }
 
 void
 VRManagerChild::RunFrameRequestCallbacks()
 {
   AUTO_PROFILER_TRACING("VR", "RunFrameRequestCallbacks");
--- a/gfx/vr/ipc/VRManagerParent.cpp
+++ b/gfx/vr/ipc/VRManagerParent.cpp
@@ -15,18 +15,17 @@
 #include "VRThread.h"
 #include "gfxVRPuppet.h"
 
 namespace mozilla {
 using namespace layers;
 namespace gfx {
 
 VRManagerParent::VRManagerParent(ProcessId aChildProcessId, bool aIsContentChild)
-  : mDisplayTestID(0)
-  , mControllerTestID(0)
+  : mControllerTestID(1)
   , mHaveEventListener(false)
   , mHaveControllerListener(false)
   , mIsContentChild(aIsContentChild)
 {
   MOZ_COUNT_CTOR(VRManagerParent);
   MOZ_ASSERT(NS_IsMainThread());
 
   SetOtherProcessId(aChildProcessId);
@@ -242,150 +241,150 @@ VRManagerParent::RecvControllerListenerR
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 VRManagerParent::RecvCreateVRTestSystem()
 {
   VRManager* vm = VRManager::Get();
   vm->CreateVRTestSystem();
-  mDisplayTestID = 0;
-  mControllerTestID = 0;
+  // The mControllerTestID is 1 based
+  mControllerTestID = 1;
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 VRManagerParent::RecvCreateVRServiceTestDisplay(const nsCString& aID, const uint32_t& aPromiseID)
 {
-  nsTArray<VRDisplayInfo> displayInfoArray;
-  impl::VRDisplayPuppet* displayPuppet = nullptr;
   VRManager* vm = VRManager::Get();
-  vm->RefreshVRDisplays();
+  VRSystemManagerPuppet* puppetManager = vm->GetPuppetManager();
+  uint32_t deviceID = puppetManager->CreateTestDisplay();
 
-  // Get VRDisplayPuppet from VRManager
-  vm->GetVRDisplayInfo(displayInfoArray);
-  for (auto& displayInfo : displayInfoArray) {
-    if (displayInfo.GetType() == VRDeviceType::Puppet) {
-        displayPuppet = static_cast<impl::VRDisplayPuppet*>(
-                        vm->GetDisplay(displayInfo.GetDisplayID()).get());
-        break;
-    }
-  }
-
-  MOZ_ASSERT(displayPuppet);
-  MOZ_ASSERT(!mDisplayTestID); // We have only one display in VRSystemManagerPuppet.
-
-  if (!mVRDisplayTests.Get(mDisplayTestID, nullptr)) {
-    mVRDisplayTests.Put(mDisplayTestID, displayPuppet);
-  }
-
-  if (SendReplyCreateVRServiceTestDisplay(aID, aPromiseID, mDisplayTestID)) {
+  if (SendReplyCreateVRServiceTestDisplay(aID, aPromiseID, deviceID)) {
     return IPC_OK();
   }
 
   return IPC_FAIL(this, "SendReplyCreateVRServiceTestController fail");
 }
 
 mozilla::ipc::IPCResult
 VRManagerParent::RecvCreateVRServiceTestController(const nsCString& aID, const uint32_t& aPromiseID)
 {
-  uint32_t controllerIdx = 0;
+  uint32_t controllerIdx = 1; // ID's are 1 based
   nsTArray<VRControllerInfo> controllerInfoArray;
   impl::VRControllerPuppet* controllerPuppet = nullptr;
   VRManager* vm = VRManager::Get();
 
-  if (mHaveControllerListener) {
-    vm->RefreshVRControllers();
-  }
+  /**
+   * When running headless mochitests on some of our automated test
+   * infrastructure, 2d display vsyncs are not always generated.
+   * In this case, the test controllers can't be created immediately
+   * after the VR display was created as the state of the VR displays
+   * are updated during vsync.
+   * To workaround, we produce a vsync manually.
+   */
+  vm->NotifyVsync(TimeStamp::Now());
 
   // Get VRControllerPuppet from VRManager
   vm->GetVRControllerInfo(controllerInfoArray);
   for (auto& controllerInfo : controllerInfoArray) {
     if (controllerInfo.GetType() == VRDeviceType::Puppet) {
       if (controllerIdx == mControllerTestID) {
         controllerPuppet = static_cast<impl::VRControllerPuppet*>(
                            vm->GetController(controllerInfo.GetControllerID()).get());
         break;
       }
       ++controllerIdx;
     }
   }
 
-  MOZ_ASSERT(controllerPuppet);
-  MOZ_ASSERT(mControllerTestID < 2); // We have only two controllers in VRSystemManagerPuppet.
+  
+  // We might not have a controllerPuppet if the test did
+  // not create a VR display first.
+  if (!controllerPuppet) {
+    // We send a device ID of "0" to indicate failure
+    if (SendReplyCreateVRServiceTestController(aID, aPromiseID, 0)) {
+      return IPC_OK();
+    }
+  } else {
+    if (!mVRControllerTests.Get(mControllerTestID, nullptr)) {
+      mVRControllerTests.Put(mControllerTestID, controllerPuppet);
+    }
 
-  if (!mVRControllerTests.Get(mControllerTestID, nullptr)) {
-    mVRControllerTests.Put(mControllerTestID, controllerPuppet);
-  }
-
-  if (SendReplyCreateVRServiceTestController(aID, aPromiseID, mControllerTestID)) {
-    ++mControllerTestID;
-    return IPC_OK();
+    if (SendReplyCreateVRServiceTestController(aID, aPromiseID, mControllerTestID)) {
+      ++mControllerTestID;
+      return IPC_OK();
+    }
   }
 
   return IPC_FAIL(this, "SendReplyCreateVRServiceTestController fail");
 }
 
 mozilla::ipc::IPCResult
 VRManagerParent::RecvSetDisplayInfoToMockDisplay(const uint32_t& aDeviceID,
                                                  const VRDisplayInfo& aDisplayInfo)
 {
-  RefPtr<impl::VRDisplayPuppet> displayPuppet;
-  mVRDisplayTests.Get(aDeviceID,
-                      getter_AddRefs(displayPuppet));
-  MOZ_ASSERT(displayPuppet);
-  displayPuppet->SetDisplayInfo(aDisplayInfo);
+  VRManager* vm = VRManager::Get();
+  VRSystemManagerPuppet* puppetManager = vm->GetPuppetManager();
+  puppetManager->SetPuppetDisplayInfo(aDeviceID, aDisplayInfo);
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 VRManagerParent::RecvSetSensorStateToMockDisplay(const uint32_t& aDeviceID,
                                                  const VRHMDSensorState& aSensorState)
 {
-  RefPtr<impl::VRDisplayPuppet> displayPuppet;
-  mVRDisplayTests.Get(aDeviceID,
-                      getter_AddRefs(displayPuppet));
-  MOZ_ASSERT(displayPuppet);
-  displayPuppet->SetSensorState(aSensorState);
+  VRManager* vm = VRManager::Get();
+  VRSystemManagerPuppet* puppetManager = vm->GetPuppetManager();
+  puppetManager->SetPuppetDisplaySensorState(aDeviceID, aSensorState);
   return IPC_OK();
 }
 
+already_AddRefed<impl::VRControllerPuppet>
+VRManagerParent::GetControllerPuppet(uint32_t aDeviceID)
+{
+  // aDeviceID for controllers start at 1 and are
+  // used as a key to mVRControllerTests
+  RefPtr<impl::VRControllerPuppet> controllerPuppet;
+  mVRControllerTests.Get(aDeviceID,
+                         getter_AddRefs(controllerPuppet));
+  MOZ_ASSERT(controllerPuppet);
+  return controllerPuppet.forget();
+}
+
 mozilla::ipc::IPCResult
 VRManagerParent::RecvNewButtonEventToMockController(const uint32_t& aDeviceID, const long& aButton,
                                                     const bool& aPressed)
 {
-  RefPtr<impl::VRControllerPuppet> controllerPuppet;
-  mVRControllerTests.Get(aDeviceID,
-                         getter_AddRefs(controllerPuppet));
-  MOZ_ASSERT(controllerPuppet);
-  controllerPuppet->SetButtonPressState(aButton, aPressed);
+  RefPtr<impl::VRControllerPuppet> controllerPuppet = GetControllerPuppet(aDeviceID);
+  if (controllerPuppet) {
+    controllerPuppet->SetButtonPressState(aButton, aPressed);
+  }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 VRManagerParent::RecvNewAxisMoveEventToMockController(const uint32_t& aDeviceID, const long& aAxis,
                                                       const double& aValue)
 {
-  RefPtr<impl::VRControllerPuppet> controllerPuppet;
-  mVRControllerTests.Get(aDeviceID,
-                         getter_AddRefs(controllerPuppet));
-  MOZ_ASSERT(controllerPuppet);
-  controllerPuppet->SetAxisMoveState(aAxis, aValue);
+  RefPtr<impl::VRControllerPuppet> controllerPuppet = GetControllerPuppet(aDeviceID);
+  if (controllerPuppet) {
+    controllerPuppet->SetAxisMoveState(aAxis, aValue);
+  }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 VRManagerParent::RecvNewPoseMoveToMockController(const uint32_t& aDeviceID,
                                                  const GamepadPoseState& pose)
 {
-  RefPtr<impl::VRControllerPuppet> controllerPuppet;
-  mVRControllerTests.Get(aDeviceID,
-                         getter_AddRefs(controllerPuppet));
-  MOZ_ASSERT(controllerPuppet);
-  controllerPuppet->SetPoseMoveState(pose);
+  RefPtr<impl::VRControllerPuppet> controllerPuppet = GetControllerPuppet(aDeviceID);
+  if (controllerPuppet) {
+    controllerPuppet->SetPoseMoveState(pose);
+  }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 VRManagerParent::RecvVibrateHaptic(const uint32_t& aControllerIdx,
                                    const uint32_t& aHapticIndex,
                                    const double& aIntensity,
                                    const double& aDuration,
--- a/gfx/vr/ipc/VRManagerParent.h
+++ b/gfx/vr/ipc/VRManagerParent.h
@@ -78,27 +78,26 @@ private:
   void RegisterWithManager();
   void UnregisterFromManager();
 
   void Bind(Endpoint<PVRManagerParent>&& aEndpoint);
 
   static void RegisterVRManagerInVRListenerThread(VRManagerParent* aVRManager);
 
   void DeferredDestroy();
+  already_AddRefed<impl::VRControllerPuppet> GetControllerPuppet(uint32_t aDeviceID);
 
   // This keeps us alive until ActorDestroy(), at which point we do a
   // deferred destruction of ourselves.
   RefPtr<VRManagerParent> mSelfRef;
   RefPtr<VRListenerThreadHolder> mVRListenerThreadHolder;
 
   // Keep the VRManager alive, until we have destroyed ourselves.
   RefPtr<VRManager> mVRManagerHolder;
-  nsRefPtrHashtable<nsUint32HashKey, impl::VRDisplayPuppet> mVRDisplayTests;
   nsRefPtrHashtable<nsUint32HashKey, impl::VRControllerPuppet> mVRControllerTests;
-  uint32_t mDisplayTestID;
   uint32_t mControllerTestID;
   bool mHaveEventListener;
   bool mHaveControllerListener;
   bool mIsContentChild;
 };
 
 } // namespace mozilla
 } // namespace gfx