[horizon] Implement VR rendering for CSS transformed elements, part 1
authorVladimir Vukicevic <vladimir@pobox.com>
Thu, 21 May 2015 11:57:21 -0400
changeset 482794 46a56a5bd8ed5fef04f2f1a47fc82e2f3328185e
parent 482793 9a3610c07ed81857a2984cd6574c829a71e655d0
child 482795 b1bc25c9e1867346665bd82adc5e0aaab18a078d
push id73224
push uservladimir@pobox.com
push dateMon, 08 Jun 2015 17:18:13 +0000
treeherdertry@a4f883a11a7e [default view] [failures only]
milestone41.0a1
[horizon] Implement VR rendering for CSS transformed elements, part 1 --- gfx/2d/Matrix.h | 8 ++ gfx/layers/composite/ContainerLayerComposite.cpp | 174 +++++++++++++++++++---- gfx/layers/composite/ContainerLayerComposite.h | 12 +- gfx/layers/composite/PaintedLayerComposite.cpp | 16 +++ gfx/thebes/gfxUtils.cpp | 2 + gfx/thebes/gfxUtils.h | 2 + gfx/vr/gfxVR.cpp | 7 + gfx/vr/gfxVR.h | 4 + gfx/vr/gfxVRCardboard.cpp | 3 + gfx/vr/gfxVROculus.cpp | 20 ++- 10 files changed, 213 insertions(+), 35 deletions(-)
gfx/2d/Matrix.h
gfx/layers/composite/ContainerLayerComposite.cpp
gfx/layers/composite/ContainerLayerComposite.h
gfx/layers/composite/PaintedLayerComposite.cpp
gfx/thebes/gfxUtils.cpp
gfx/thebes/gfxUtils.h
gfx/vr/gfxVR.cpp
gfx/vr/gfxVR.h
gfx/vr/gfxVRCardboard.cpp
gfx/vr/gfxVROculus.cpp
--- a/gfx/2d/Matrix.h
+++ b/gfx/2d/Matrix.h
@@ -453,16 +453,24 @@ public:
       aMatrix->_21 = _21;
       aMatrix->_22 = _22;
       aMatrix->_31 = _41;
       aMatrix->_32 = _42;
     }
     return true;
   }
 
+  bool IsTranslation() const
+  {
+    return gfx::FuzzyEqual(_11, 1.0f) && gfx::FuzzyEqual(_12, 0.0f) && gfx::FuzzyEqual(_13, 0.0f) && gfx::FuzzyEqual(_14, 0.0f) &&
+           gfx::FuzzyEqual(_21, 0.0f) && gfx::FuzzyEqual(_22, 1.0f) && gfx::FuzzyEqual(_23, 0.0f) && gfx::FuzzyEqual(_24, 0.0f) &&
+           gfx::FuzzyEqual(_31, 0.0f) && gfx::FuzzyEqual(_32, 0.0f) && gfx::FuzzyEqual(_33, 1.0f) && gfx::FuzzyEqual(_34, 0.0f) &&
+           gfx::FuzzyEqual(_44, 1.0f);
+  }
+
   Matrix4x4& ProjectTo2D() {
     _31 = 0.0f;
     _32 = 0.0f;
     _13 = 0.0f;
     _23 = 0.0f;
     _33 = 1.0f;
     _43 = 0.0f;
     _34 = 0.0f;
--- a/gfx/layers/composite/ContainerLayerComposite.cpp
+++ b/gfx/layers/composite/ContainerLayerComposite.cpp
@@ -36,16 +36,18 @@
 #include "GeckoProfiler.h"              // for GeckoProfiler
 #ifdef MOZ_ENABLE_PROFILER_SPS
 #include "ProfilerMarkers.h"            // for ProfilerMarkers
 #endif
 
 #define CULLING_LOG(...)
 // #define CULLING_LOG(...) printf_stderr("CULLING: " __VA_ARGS__)
 
+#define DUMP(...) do { if (gfxUtils::sDumpDebug) { printf_stderr(__VA_ARGS__); } } while(0)
+
 namespace mozilla {
 namespace layers {
 
 using namespace gfx;
 
 static bool
 LayerHasCheckerboardingAPZC(Layer* aLayer, gfxRGBA* aOutColor)
 {
@@ -131,18 +133,20 @@ struct PreparedLayer
 
 
 template<class ContainerT> void
 ContainerRenderVR(ContainerT* aContainer,
                   LayerManagerComposite* aManager,
                   const gfx::IntRect& aClipRect,
                   gfx::VRHMDInfo* aHMD)
 {
-  RefPtr<CompositingRenderTarget> surface;
+  RefPtr<CompositingRenderTarget> surface, eyeSurface[2];
 
+  DUMP(">>> ContainerRenderVR [%p]\n", aContainer);
+  
   Compositor* compositor = aManager->GetCompositor();
 
   RefPtr<CompositingRenderTarget> previousTarget = compositor->GetCurrentRenderTarget();
 
   gfx::IntRect visibleRect = aContainer->GetEffectiveVisibleRegion().GetBounds();
 
   float opacity = aContainer->GetEffectiveOpacity();
 
@@ -153,84 +157,174 @@ ContainerRenderVR(ContainerT* aContainer
   // maximum texture size supported by the GL? The present code chooses the compromise
   // of just clamping the framebuffer's size to the max supported size.
   // This gives us a lower resolution rendering of the intermediate surface (children layers).
   // See bug 827170 for a discussion.
   int32_t maxTextureSize = compositor->GetMaxTextureSize();
   surfaceRect.width = std::min(maxTextureSize, surfaceRect.width);
   surfaceRect.height = std::min(maxTextureSize, surfaceRect.height);
 
+  gfx::Size halfSurfaceSize(surfaceRect.width / 2.0f, surfaceRect.height);
+  
   // use NONE here, because we draw black to clear below
-  surface = compositor->CreateRenderTarget(surfaceRect, INIT_MODE_NONE);
+  surface = compositor->CreateRenderTarget(surfaceRect, INIT_MODE_CLEAR);
   if (!surface) {
     return;
   }
 
-  compositor->SetRenderTarget(surface);
+  // The size of each individual eye surface
+  gfx::IntSize eyeResolution = aHMD->SuggestedEyeResolution();
+  gfx::IntRect eyeRect = gfx::IntRect(0, 0, eyeResolution.width, eyeResolution.height);
 
+  gfx::IntRect rtBounds = previousTarget->GetRect();
+  DUMP("eyeResolution: %d %d targetRT: %d %d %d %d\n", eyeResolution.width, eyeResolution.height,
+       rtBounds.x, rtBounds.y, rtBounds.width, rtBounds.height);
+  
   nsAutoTArray<Layer*, 12> children;
   aContainer->SortChildrenBy3DZOrder(children);
 
-  /**
-   * Render this container's contents.
-   */
-  gfx::IntRect surfaceClipRect(0, 0, surfaceRect.width, surfaceRect.height);
-  RenderTargetIntRect rtClipRect(0, 0, surfaceRect.width, surfaceRect.height);
-  for (uint32_t i = 0; i < children.Length(); i++) {
-    LayerComposite* layerToRender = static_cast<LayerComposite*>(children.ElementAt(i)->ImplData());
-    Layer* layer = layerToRender->GetLayer();
+  gfx::Matrix4x4 origTransform = aContainer->GetEffectiveTransform();
+
+  for (uint32_t eye = 0; eye < 2; eye++) {
+    DUMP(" -- ContainerRenderVR [%p] EYE %d\n", aContainer, eye);
 
-    if (layer->GetEffectiveVisibleRegion().IsEmpty() &&
-        !layer->AsContainerLayer()) {
-      continue;
+    eyeSurface[eye] = compositor->CreateRenderTarget(eyeRect, INIT_MODE_CLEAR);
+    if (!eyeSurface[eye]) {
+      compositor->SetRenderTarget(previousTarget);
+      return;
     }
 
-    RenderTargetIntRect clipRect = layer->CalculateScissorRect(rtClipRect);
-    if (clipRect.IsEmpty()) {
-      continue;
+    // Compute pre-rendered VR eye transform:
+    // - Scale the incoming pre-rendered surface's half-size (per eye) to fit the eye resolution
+    //   eyeResolution is what the VR HMD device's recommended render rect will be, so if that's being
+    //   honored, then this will be 1.0f
+    // - For the right eye, take the right half of the surface.
+    gfx::Matrix4x4 preRenderedEyeTransform =
+      gfx::Matrix4x4::Scaling(float(eyeResolution.width) / halfSurfaceSize.width,
+                              float(eyeResolution.height) / halfSurfaceSize.height,
+                              1.0f);
+    preRenderedEyeTransform.PostTranslate(-float(eye * eyeResolution.width), 0.0f, 0.0f);
+
+    // Compute the CSS-rendered VR eye transform:
+    // - Translate by negative of the eye translation (or rather, the inverse of it)
+    // - The convert units so that pixels become 96 dpi "dots", and metres make sense;
+    //   this makes the eye translation in metres turn into an appropriate number of pixels
+    gfx::Point3D eyeTranslation = aHMD->GetEyeTranslation(eye);
+    gfx::Matrix4x4 cssEyeTransform = gfx::Matrix4x4::Translation(gfx::Point3D(-eyeTranslation.x,
+                                                                              -eyeTranslation.y,
+                                                                              -eyeTranslation.z));
+    const float pixelsToMetres = 0.0254f / 96.0f;  // 96 dpi, 2.54 cm per inch, cm -> meters
+    cssEyeTransform.PreScale(pixelsToMetres, pixelsToMetres, pixelsToMetres);
+
+    // The CSS-rendered VR projection matrix.  This is a right-handed projection matrix, to match CSS.
+    // But this projection matrix has Y-up, and CSS has Y-down.  Scale by -1 Y to fix this.
+    gfx::Matrix4x4 cssEyeProjection = aHMD->GetEyeProjectionMatrix(eye);
+    cssEyeProjection.PostScale(1.0f, -1.0f, 1.0f);
+
+    // flag to keep track of which projection mode we're in as we're going through our child
+    // layers
+    int lastLayerWasNativeVR = -1;
+
+    for (uint32_t i = 0; i < children.Length(); i++) {
+      LayerComposite* layerToRender = static_cast<LayerComposite*>(children.ElementAt(i)->ImplData());
+      Layer* layer = layerToRender->GetLayer();
+      uint32_t contentFlags = layer->GetContentFlags();
+
+      if (layer->GetEffectiveVisibleRegion().IsEmpty() &&
+          !layer->AsContainerLayer()) {
+        continue;
+      }
+
+      // We flip between pre-rendered and Gecko-rendered VR based on whether
+      // the child layer of this VR container layer has PRESERVE_3D or not.
+      int thisLayerNativeVR = (contentFlags & Layer::CONTENT_PRESERVE_3D) ? 0 : 1;
+      if (lastLayerWasNativeVR != thisLayerNativeVR) {
+        if (thisLayerNativeVR) {
+          // XXX we still need depth test here, but we have no way of preserving
+          // depth anyway in native VR layers until we have a way to save them
+          // from WebGL (and maybe depth video?)
+          eyeSurface[eye]->ClearProjection();
+          compositor->SetRenderTarget(eyeSurface[eye]);
+
+          aContainer->ReplaceEffectiveTransform(preRenderedEyeTransform);
+          DUMP("%p Switching to pre-rendered VR\n", aContainer);
+        } else {
+          eyeSurface[eye]->SetProjection(cssEyeProjection, true, aHMD->GetZNear(), aHMD->GetZFar());
+          compositor->SetRenderTarget(eyeSurface[eye]);
+
+          aContainer->ReplaceEffectiveTransform(cssEyeTransform);
+          DUMP("%p Switching to Gecko-rendered VR\n", aContainer);
+        }
+        lastLayerWasNativeVR = thisLayerNativeVR;
+      }
+
+      // XXX these are both clip rects, which end up as scissor rects in the compositor.  So we just
+      // pass the full target surface rect here.
+      layerToRender->Prepare(RenderTargetIntRect(eyeRect.x, eyeRect.y, eyeRect.width, eyeRect.height));
+      layerToRender->RenderLayer(eyeRect);
     }
-
-    layerToRender->Prepare(rtClipRect);
-    layerToRender->RenderLayer(surfaceClipRect);
   }
 
-  // Unbind the current surface and rebind the previous one.
-#ifdef MOZ_DUMP_PAINTING
-  if (gfxUtils::sDumpPainting) {
-    RefPtr<gfx::DataSourceSurface> surf = surface->Dump(aManager->GetCompositor());
-    if (surf) {
-      WriteSnapshotToDumpFile(aContainer, surf);
-    }
+  DUMP(" -- ContainerRenderVR [%p] after child layers\n", aContainer);
+
+  // Now put back the original transfom on this container
+  aContainer->ReplaceEffectiveTransform(origTransform);
+
+  // bind and draw each eye to the intermediate surface
+  compositor->SetRenderTarget(surface);
+
+  for (uint32_t eye = 0; eye < 2; eye++) {
+    gfx::Rect destRect(0, 0, eyeResolution.width, eyeResolution.height);
+    gfx::Rect clipRect(surfaceRect.x, surfaceRect.y, surfaceRect.width, surfaceRect.height);
+    EffectChain rtEffect(aContainer);
+    rtEffect.mPrimaryEffect = new EffectRenderTarget(eyeSurface[eye]);
+
+    // when we render, we need to take the eyeResolution quad and scale it to the appropriate size, and put it
+    // in the right place
+    gfx::Matrix4x4 drawTransform = gfx::Matrix4x4::Scaling(halfSurfaceSize.width / eyeResolution.width,
+                                                           halfSurfaceSize.height / eyeResolution.height,
+                                                           1.0f);
+    drawTransform.PostTranslate(eye * halfSurfaceSize.width, 0.0f, 0.0f);
+    compositor->DrawQuad(destRect, clipRect, rtEffect, 1.0f, drawTransform);
   }
-#endif
 
+  // then bind the original target and draw with distortion
   compositor->SetRenderTarget(previousTarget);
 
+  gfx::Rect normRect(0, 0, visibleRect.width, visibleRect.height);
   gfx::Rect rect(visibleRect.x, visibleRect.y, visibleRect.width, visibleRect.height);
   gfx::Rect clipRect(aClipRect.x, aClipRect.y, aClipRect.width, aClipRect.height);
 
   // The VR geometry may not cover the entire area; we need to fill with a solid color
   // first.
   // XXX should DrawQuad handle this on its own?  Is there a time where we wouldn't want
   // to do this? (e.g. something like Cardboard would not require distortion so will fill
   // the entire rect)
   EffectChain solidEffect(aContainer);
   solidEffect.mPrimaryEffect = new EffectSolidColor(Color(0.0, 0.0, 0.0, 1.0));
-  aManager->GetCompositor()->DrawQuad(rect, clipRect, solidEffect, opacity,
-                                      aContainer->GetEffectiveTransform());
+  aManager->GetCompositor()->DrawQuad(normRect, normRect, solidEffect, 1.0, gfx::Matrix4x4());
 
   // draw the temporary surface with VR distortion to the original destination
   EffectChain vrEffect(aContainer);
-  vrEffect.mPrimaryEffect = new EffectVRDistortion(aHMD, surface);
+#ifdef DEBUG
+  if (PR_GetEnv("MOZ_GFX_VR_NO_DISTORTION")) {
+    vrEffect.mPrimaryEffect = new EffectRenderTarget(surface);
+  } else
+#endif
+  {
+    vrEffect.mPrimaryEffect = new EffectVRDistortion(aHMD, surface);
+  }
 
   // XXX we shouldn't use visibleRect here -- the VR distortion needs to know the
   // full rect, not just the visible one.  Luckily, right now, VR distortion is only
   // rendered when the element is fullscreen, so the visibleRect will be right anyway.
   aManager->GetCompositor()->DrawQuad(rect, clipRect, vrEffect, opacity,
                                       aContainer->GetEffectiveTransform());
+
+  DUMP("<<< ContainerRenderVR [%p]\n", aContainer);
 }
 
 /* all of the prepared data that we need in RenderLayer() */
 struct PreparedData
 {
   RefPtr<CompositingRenderTarget> mTmpTarget;
   nsAutoTArray<PreparedLayer, 12> mLayers;
   bool mNeedsSurfaceCopy;
@@ -484,16 +578,34 @@ RenderIntermediate(ContainerT* aContaine
 
 template<class ContainerT> void
 ContainerRender(ContainerT* aContainer,
                  LayerManagerComposite* aManager,
                  const gfx::IntRect& aClipRect)
 {
   MOZ_ASSERT(aContainer->mPrepared);
 
+  if (gfxUtils::sDumpDebug) {
+    nsIntRegion visibleRegion = aContainer->GetEffectiveVisibleRegion();
+    gfx::IntRect bounds = visibleRegion.GetBounds();
+    const gfx::Matrix4x4& xform = aContainer->GetEffectiveTransform();
+    printf_stderr("ContainerLayer[%p]: visible: [%d %d %d %d] clip: [%d %d %d %d] %s\n",
+                  aContainer, bounds.X(), bounds.Y(), bounds.Width(), bounds.Height(),
+                  aClipRect.X(), aClipRect.Y(), aClipRect.Width(), aClipRect.Height(),
+                  aContainer->GetVRHMDInfo() ? "HMD" : "");
+    if (xform.IsTranslation()) {
+      printf_stderr("                  xform: [translate %.2f %.2f %.2f]\n", xform._41, xform._42, xform._43);
+    } else {
+      printf_stderr("   xform: [%3.2f %3.2f %3.2f %3.2f]\n", xform._11, xform._12, xform._13, xform._14);
+      printf_stderr("          [%3.2f %3.2f %3.2f %3.2f]\n", xform._21, xform._22, xform._23, xform._24);
+      printf_stderr("          [%3.2f %3.2f %3.2f %3.2f]\n", xform._31, xform._32, xform._33, xform._34);
+      printf_stderr("          [%3.2f %3.2f %3.2f %3.2f]\n", xform._41, xform._42, xform._43, xform._44);
+    }
+  }
+
   gfx::VRHMDInfo *hmdInfo = aContainer->GetVRHMDInfo();
   if (hmdInfo && hmdInfo->GetConfiguration().IsValid()) {
     ContainerRenderVR(aContainer, aManager, aClipRect, hmdInfo);
     aContainer->mPrepared = nullptr;
     return;
   }
 
   if (aContainer->UseIntermediateSurface()) {
--- a/gfx/layers/composite/ContainerLayerComposite.h
+++ b/gfx/layers/composite/ContainerLayerComposite.h
@@ -24,17 +24,22 @@ class ContainerLayerComposite : public C
 {
   template<class ContainerT>
   friend void ContainerPrepare(ContainerT* aContainer,
                                LayerManagerComposite* aManager,
                                const RenderTargetIntRect& aClipRect);
   template<class ContainerT>
   friend void ContainerRender(ContainerT* aContainer,
                               LayerManagerComposite* aManager,
-                              const RenderTargetIntRect& aClipRect);
+                              const nsIntRect& aClipRect);
+  template<class ContainerT>
+  friend void ContainerRenderVR(ContainerT* aContainer,
+                                LayerManagerComposite* aManager,
+                                const nsIntRect& aClipRect,
+                                gfx::VRHMDInfo& aHMD);
   template<class ContainerT>
   friend void RenderLayers(ContainerT* aContainer,
                            LayerManagerComposite* aManager,
                            const RenderTargetIntRect& aClipRect);
   template<class ContainerT>
   friend void RenderIntermediate(ContainerT* aContainer,
                    LayerManagerComposite* aManager,
                    const gfx::IntRect& aClipRect,
@@ -121,16 +126,21 @@ class RefLayerComposite : public RefLaye
   friend void ContainerPrepare(ContainerT* aContainer,
                                LayerManagerComposite* aManager,
                                const RenderTargetIntRect& aClipRect);
   template<class ContainerT>
   friend void ContainerRender(ContainerT* aContainer,
                               LayerManagerComposite* aManager,
                               const gfx::IntRect& aClipRect);
   template<class ContainerT>
+  friend void ContainerRenderVR(ContainerT* aContainer,
+                                LayerManagerComposite* aManager,
+                                const nsIntRect& aClipRect,
+                                gfx::VRHMDInfo& aHMD);
+  template<class ContainerT>
   friend void RenderLayers(ContainerT* aContainer,
                            LayerManagerComposite* aManager,
                            const gfx::IntRect& aClipRect);
   template<class ContainerT>
   friend void RenderIntermediate(ContainerT* aContainer,
                    LayerManagerComposite* aManager,
                    const gfx::IntRect& aClipRect,
                    RefPtr<CompositingRenderTarget> surface);
--- a/gfx/layers/composite/PaintedLayerComposite.cpp
+++ b/gfx/layers/composite/PaintedLayerComposite.cpp
@@ -130,16 +130,32 @@ PaintedLayerComposite::RenderLayer(const
   if (gfxUtils::sDumpPainting) {
     RefPtr<gfx::DataSourceSurface> surf = mBuffer->GetAsSurface();
     if (surf) {
       WriteSnapshotToDumpFile(this, surf);
     }
   }
 #endif
 
+  if (gfxUtils::sDumpDebug) {
+    nsIntRect bounds = visibleRegion.GetBounds();
+    const gfx::Matrix4x4& xform = GetEffectiveTransform();
+    printf_stderr("PaintedLayer[%p]: visible: [%d %d %d %d] clip: [%.2f %.2f %.2f %.2f]\n",
+                  this, bounds.X(), bounds.Y(), bounds.Width(), bounds.Height(),
+                  clipRect.X(), clipRect.Y(), clipRect.Width(), clipRect.Height());
+    if (xform.IsTranslation()) {
+      printf_stderr("                  xform: [translate %.2f %.2f %.2f]\n", xform._41, xform._42, xform._43);
+    } else {
+      printf_stderr("   xform: [%3.2f %3.2f %3.2f %3.2f]\n", xform._11, xform._12, xform._13, xform._14);
+      printf_stderr("          [%3.2f %3.2f %3.2f %3.2f]\n", xform._21, xform._22, xform._23, xform._24);
+      printf_stderr("          [%3.2f %3.2f %3.2f %3.2f]\n", xform._31, xform._32, xform._33, xform._34);
+      printf_stderr("          [%3.2f %3.2f %3.2f %3.2f]\n", xform._41, xform._42, xform._43, xform._44);
+    }
+  }
+
   EffectChain effectChain(this);
   LayerManagerComposite::AutoAddMaskEffect autoMaskEffect(mMaskLayer, effectChain);
   AddBlendModeEffect(effectChain);
 
   mBuffer->SetPaintWillResample(MayResample());
 
   mBuffer->Composite(effectChain,
                      GetEffectiveOpacity(),
--- a/gfx/thebes/gfxUtils.cpp
+++ b/gfx/thebes/gfxUtils.cpp
@@ -1457,16 +1457,18 @@ bool gfxUtils::sDumpPaintingToFile = get
 bool gfxUtils::sDumpPaintItems = getenv("MOZ_DUMP_PAINT_ITEMS") != 0;
 #else
 bool gfxUtils::sDumpPainting = false;
 bool gfxUtils::sDumpPaintingIntermediate = false;
 bool gfxUtils::sDumpPaintingToFile = false;
 bool gfxUtils::sDumpPaintItems = false;
 #endif
 
+bool gfxUtils::sDumpDebug = getenv("MOZ_GFX_DUMP_DEBUG") != 0;
+
 namespace mozilla {
 namespace gfx {
 
 Color ToDeviceColor(Color aColor)
 {
   // aColor is pass-by-value since to get return value optimization goodness we
   // need to return the same object from all return points in this function. We
   // could declare a local Color variable and use that, but we might as well
--- a/gfx/thebes/gfxUtils.h
+++ b/gfx/thebes/gfxUtils.h
@@ -292,16 +292,18 @@ public:
 
     static bool DumpDisplayList();
 
     static bool sDumpPainting;
     static bool sDumpPaintingIntermediate;
     static bool sDumpPaintingToFile;
     static bool sDumpPaintItems;
     static FILE* sDumpPaintFile;
+
+    static bool sDumpDebug;
 };
 
 namespace mozilla {
 namespace gfx {
 
 /**
  * If the CMS mode is eCMSMode_All, these functions transform the passed
  * color to a device color using the transform returened by gfxPlatform::
--- a/gfx/vr/gfxVR.cpp
+++ b/gfx/vr/gfxVR.cpp
@@ -129,8 +129,15 @@ VRHMDManager::GetAllHMDs(nsTArray<nsRefP
   }
 }
 
 /* static */ uint32_t
 VRHMDManager::AllocateDeviceIndex()
 {
   return ++sDeviceBase;
 }
+
+/* static */ already_AddRefed<nsIScreen>
+VRHMDManager::MakeFakeScreen(int32_t x, int32_t y, uint32_t width, uint32_t height)
+{
+  nsCOMPtr<nsIScreen> screen = new FakeScreen(IntRect(x, y, width, height));
+  return screen.forget();
+}
--- a/gfx/vr/gfxVR.h
+++ b/gfx/vr/gfxVR.h
@@ -157,16 +157,18 @@ public:
 
   /* Suggested resolution for rendering a single eye.
    * Assumption is that left/right rendering will be 2x of this size.
    * XXX fix this for vertical displays
    */
   const IntSize& SuggestedEyeResolution() const { return mEyeResolution; }
   const Point3D& GetEyeTranslation(uint32_t whichEye) const { return mEyeTranslation[whichEye]; }
   const Matrix4x4& GetEyeProjectionMatrix(uint32_t whichEye) const { return mEyeProjectionMatrix[whichEye]; }
+  const double GetZNear() const { return mZNear; }
+  const double GetZFar() const { return mZFar; }
 
   virtual uint32_t GetSupportedSensorStateBits() { return mSupportedSensorBits; }
   virtual bool StartSensorTracking() = 0;
   virtual VRHMDSensorState GetSensorState(double timeOffset = 0.0) = 0;
   virtual void StopSensorTracking() = 0;
 
   virtual void ZeroSensor() = 0;
 
@@ -192,30 +194,32 @@ protected:
   nsCString mDeviceName;
 
   VRFieldOfView mEyeFOV[NumEyes];
   IntSize mEyeResolution;
   Point3D mEyeTranslation[NumEyes];
   Matrix4x4 mEyeProjectionMatrix[NumEyes];
   VRDistortionMesh mDistortionMesh[NumEyes];
   uint32_t mSupportedSensorBits;
+  double mZNear, mZFar;
 
   VRFieldOfView mRecommendedEyeFOV[NumEyes];
   VRFieldOfView mMaximumEyeFOV[NumEyes];
 
   nsCOMPtr<nsIScreen> mScreen;
 };
 
 class VRHMDManager {
 public:
   static void ManagerInit();
   static void ManagerDestroy();
   static void GetAllHMDs(nsTArray<nsRefPtr<VRHMDInfo>>& aHMDResult);
   static uint32_t AllocateDeviceIndex();
 
+  static already_AddRefed<nsIScreen> MakeFakeScreen(int32_t x, int32_t y, uint32_t width, uint32_t height);
 protected:
   typedef nsTArray<nsRefPtr<VRHMDManager>> VRHMDManagerArray;
   static VRHMDManagerArray *sManagers;
   static Atomic<uint32_t> sDeviceBase;
 
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VRHMDManager)
 
--- a/gfx/vr/gfxVRCardboard.cpp
+++ b/gfx/vr/gfxVRCardboard.cpp
@@ -249,16 +249,19 @@ ConstructProjectionMatrix(const VRFieldO
 
 bool
 HMDInfoCardboard::SetFOV(const VRFieldOfView& aFOVLeft,
                          const VRFieldOfView& aFOVRight,
                          double zNear, double zFar)
 {
   const float standardIPD = 0.064f;
 
+  mZNear = zNear;
+  mZFar = zFar;
+
   for (uint32_t eye = 0; eye < NumEyes; eye++) {
     mEyeFOV[eye] = eye == Eye_Left ? aFOVLeft : aFOVRight;
     mEyeTranslation[eye] = Point3D(standardIPD * (eye == Eye_Left ? -1.0 : 1.0), 0.0, 0.0);
     mEyeProjectionMatrix[eye] = ConstructProjectionMatrix(mEyeFOV[eye], true, zNear, zFar);
 
     mDistortionMesh[eye].mVertices.SetLength(4);
     mDistortionMesh[eye].mIndices.SetLength(6);
 
--- a/gfx/vr/gfxVROculus.cpp
+++ b/gfx/vr/gfxVROculus.cpp
@@ -259,19 +259,30 @@ HMDInfoOculus::HMDInfoOculus(ovrHmd aHMD
 
   mMaximumEyeFOV[Eye_Left] = FromFovPort(mHMD->MaxEyeFov[ovrEye_Left]);
   mMaximumEyeFOV[Eye_Right] = FromFovPort(mHMD->MaxEyeFov[ovrEye_Right]);
 
   SetFOV(mRecommendedEyeFOV[Eye_Left], mRecommendedEyeFOV[Eye_Right], 0.01, 10000.0);
 
   nsCOMPtr<nsIScreenManager> screenmgr = do_GetService("@mozilla.org/gfx/screenmanager;1");
   if (screenmgr) {
-    screenmgr->ScreenForRect(mHMD->WindowsPos.x, mHMD->WindowsPos.y,
-                             mHMD->Resolution.w, mHMD->Resolution.h,
-                             getter_AddRefs(mScreen));
+#if 1
+    if (getenv("FAKE_OCULUS_SCREEN")) {
+      const char *env = getenv("FAKE_OCULUS_SCREEN");
+      nsresult err;
+      int32_t xcoord = nsCString(env).ToInteger(&err);
+      if (err != NS_OK) xcoord = 0;
+      mScreen = VRHMDManager::MakeFakeScreen(xcoord, 0, 1920, 1080);
+    } else
+#endif
+    {
+      screenmgr->ScreenForRect(mHMD->WindowsPos.x, mHMD->WindowsPos.y,
+                               mHMD->Resolution.w, mHMD->Resolution.h,
+                               getter_AddRefs(mScreen));
+    }
   }
 }
 
 void
 HMDInfoOculus::Destroy()
 {
   if (mHMD) {
     ovrHmd_Destroy(mHMD);
@@ -283,16 +294,19 @@ bool
 HMDInfoOculus::SetFOV(const VRFieldOfView& aFOVLeft, const VRFieldOfView& aFOVRight,
                       double zNear, double zFar)
 {
   float pixelsPerDisplayPixel = 1.0;
   ovrSizei texSize[2];
 
   uint32_t caps = ovrDistortionCap_Chromatic | ovrDistortionCap_Vignette; // XXX TODO add TimeWarp
 
+  mZNear = zNear;
+  mZFar = zFar;
+
   // get eye parameters and create the mesh
   for (uint32_t eye = 0; eye < NumEyes; eye++) {
     mEyeFOV[eye] = eye == 0 ? aFOVLeft : aFOVRight;
     mFOVPort[eye] = ToFovPort(mEyeFOV[eye]);
 
     ovrEyeRenderDesc renderDesc = ovrHmd_GetRenderDesc(mHMD, (ovrEyeType) eye, mFOVPort[eye]);
 
     // these values are negated so that content can add the adjustment to its camera position,