Bug 637852. Part 18: Support computing the "residual transform" for a ThebesLayer --- the difference between its snapped transform and the ideal transform --- and use it to align ThebesLayer drawing for transforms that aren't changing. r=tnikkel
authorRobert O'Callahan <robert@ocallahan.org>
Thu, 23 Jun 2011 00:11:28 +1200
changeset 71807 4c323a5e40c2a43d2a204bf5c9a34c1a229a163c
parent 71806 500265c61f37887aa038076384a94717bdb65737
child 71808 e96e2e5829cd244abcffa8ca6f95807e523eb234
push id209
push userbzbarsky@mozilla.com
push dateTue, 05 Jul 2011 17:42:16 +0000
treeherdermozilla-aurora@cc6e30cce8af [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel
bugs637852, 637597
milestone7.0a1
Bug 637852. Part 18: Support computing the "residual transform" for a ThebesLayer --- the difference between its snapped transform and the ideal transform --- and use it to align ThebesLayer drawing for transforms that aren't changing. r=tnikkel This fixes bug 637597 and probably other bugs.
gfx/layers/Layers.h
gfx/layers/basic/BasicLayers.cpp
layout/base/FrameLayerBuilder.cpp
layout/base/FrameLayerBuilder.h
layout/reftests/bugs/637597-1-ref.html
layout/reftests/bugs/637597-1.html
layout/reftests/bugs/reftest.list
--- a/gfx/layers/Layers.h
+++ b/gfx/layers/Layers.h
@@ -938,53 +938,100 @@ class THEBES_API ThebesLayer : public La
 public:
   /**
    * CONSTRUCTION PHASE ONLY
    * Tell this layer that the content in some region has changed and
    * will need to be repainted. This area is removed from the valid
    * region.
    */
   virtual void InvalidateRegion(const nsIntRegion& aRegion) = 0;
+  /**
+   * CONSTRUCTION PHASE ONLY
+   * Set whether ComputeEffectiveTransforms should compute the
+   * "residual translation" --- the translation that should be applied *before*
+   * mEffectiveTransform to get the ideal transform for this ThebesLayer.
+   * When this is true, ComputeEffectiveTransforms will compute the residual
+   * and ensure that the layer is invalidated whenever the residual changes.
+   * When it's false, a change in the residual will not trigger invalidation
+   * and GetResidualTranslation will return 0,0.
+   * So when the residual is to be ignored, set this to false for better
+   * performance.
+   */
+  void SetAllowResidualTranslation(bool aAllow) { mAllowResidualTranslation = aAllow; }
 
   /**
    * Can be used anytime
    */
   const nsIntRegion& GetValidRegion() const { return mValidRegion; }
 
   virtual ThebesLayer* AsThebesLayer() { return this; }
 
   MOZ_LAYER_DECL_NAME("ThebesLayer", TYPE_THEBES)
 
   virtual void ComputeEffectiveTransforms(const gfx3DMatrix& aTransformToSurface)
   {
     // The default implementation just snaps 0,0 to pixels.
     gfx3DMatrix idealTransform = GetLocalTransform()*aTransformToSurface;
-    mEffectiveTransform = SnapTransform(idealTransform, gfxRect(0, 0, 0, 0), nsnull);
+    gfxMatrix residual;
+    mEffectiveTransform = SnapTransform(idealTransform, gfxRect(0, 0, 0, 0),
+        mAllowResidualTranslation ? &residual : nsnull);
+    // The residual can only be a translation because ThebesLayer snapping
+    // only aligns a single point with the pixel grid; scale factors are always
+    // preserved exactly
+    NS_ASSERTION(!residual.HasNonTranslation(),
+                 "Residual transform can only be a translation");
+    if (residual.GetTranslation() != mResidualTranslation) {
+      mResidualTranslation = residual.GetTranslation();
+      NS_ASSERTION(-0.5 <= mResidualTranslation.x && mResidualTranslation.x < 0.5 &&
+                   -0.5 <= mResidualTranslation.y && mResidualTranslation.y < 0.5,
+                   "Residual translation out of range");
+      mValidRegion.SetEmpty();
+    }
   }
 
   bool UsedForReadback() { return mUsedForReadback; }
   void SetUsedForReadback(bool aUsed) { mUsedForReadback = aUsed; }
+  /**
+   * Returns the residual translation. Apply this translation when drawing
+   * into the ThebesLayer so that when mEffectiveTransform is applied afterwards
+   * by layer compositing, the results exactly match the "ideal transform"
+   * (the product of the transform of this layer and its ancestors).
+   * Returns 0,0 unless SetAllowResidualTranslation(true) has been called.
+   * The residual translation components are always in the range [-0.5, 0.5).
+   */
+  gfxPoint GetResidualTranslation() const { return mResidualTranslation; }
 
 protected:
   ThebesLayer(LayerManager* aManager, void* aImplData)
     : Layer(aManager, aImplData)
     , mValidRegion()
     , mUsedForReadback(false)
+    , mAllowResidualTranslation(false)
   {
     mContentFlags = 0; // Clear NO_TEXT, NO_TEXT_OVER_TRANSPARENT
   }
 
   virtual nsACString& PrintInfo(nsACString& aTo, const char* aPrefix);
 
+  /**
+   * ComputeEffectiveTransforms snaps the ideal transform to get mEffectiveTransform.
+   * mResidualTranslation is the translation that should be applied *before*
+   * mEffectiveTransform to get the ideal transform.
+   */
+  gfxPoint mResidualTranslation;
   nsIntRegion mValidRegion;
   /**
    * Set when this ThebesLayer is participating in readback, i.e. some
    * ReadbackLayer (may) be getting its background from this layer.
    */
   bool mUsedForReadback;
+  /**
+   * True when
+   */
+  bool mAllowResidualTranslation;
 };
 
 /**
  * A Layer which other layers render into. It holds references to its
  * children.
  */
 class THEBES_API ContainerLayer : public Layer {
 public:
--- a/gfx/layers/basic/BasicLayers.cpp
+++ b/gfx/layers/basic/BasicLayers.cpp
@@ -499,16 +499,20 @@ public:
   }
 
   virtual void ComputeEffectiveTransforms(const gfx3DMatrix& aTransformToSurface)
   {
     if (!BasicManager()->IsRetained()) {
       // Don't do any snapping of our transform, since we're just going to
       // draw straight through without intermediate buffers.
       mEffectiveTransform = GetLocalTransform()*aTransformToSurface;
+      if (gfxPoint(0,0) != mResidualTranslation) {
+        mResidualTranslation = gfxPoint(0,0);
+        mValidRegion.SetEmpty();
+      }
       return;
     }
     ThebesLayer::ComputeEffectiveTransforms(aTransformToSurface);
   }
 
 protected:
   BasicLayerManager* BasicManager()
   {
--- a/layout/base/FrameLayerBuilder.cpp
+++ b/layout/base/FrameLayerBuilder.cpp
@@ -839,16 +839,21 @@ ContainerState::CreateOrRecycleThebesLay
     if (!layer)
       return nsnull;
     // Mark this layer as being used for Thebes-painting display items
     data = new ThebesDisplayItemLayerUserData();
     layer->SetUserData(&gThebesDisplayItemLayerUserData, data);
   }
   data->mXScale = mParameters.mXScale;
   data->mYScale = mParameters.mYScale;
+  // If we're in a transformed subtree, but no ancestor transform is actively
+  // changing, we'll use the residual translation when drawing into the
+  // ThebesLayer to ensure that snapping exactly matches the ideal transform.
+  layer->SetAllowResidualTranslation(
+      mParameters.mInTransformedSubtree && !mParameters.mInActiveTransformedSubtree);
 
   mBuilder->LayerBuilder()->SaveLastPaintOffset(layer);
 
   // Set up transform so that 0,0 in the Thebes layer corresponds to the
   // (pixel-snapped) top-left of the aActiveScrolledRoot.
   // XXX if the transform has changed, and the difference between the old and
   // new offsets (not transforms!) is not an integer number of pixels after
   // scaling, we need to invalidate the entire layer.
@@ -1688,17 +1693,26 @@ ChooseScaleAndSetTransform(FrameLayerBui
     }
   } else {
     scale = gfxSize(1.0, 1.0);
   }
 
   // Apply the inverse of our resolution-scale before the rest of our transform
   transform = gfx3DMatrix::Scale(1.0/scale.width, 1.0/scale.height, 1.0)*transform;
   aLayer->SetTransform(transform);
-  return FrameLayerBuilder::ContainerParameters(scale.width, scale.height);
+
+  FrameLayerBuilder::ContainerParameters
+    result(scale.width, scale.height, aIncomingScale);
+  if (aTransform) {
+    result.mInTransformedSubtree = true;
+    if (aContainerFrame->AreLayersMarkedActive(nsChangeHint_UpdateTransformLayer)) {
+      result.mInActiveTransformedSubtree = true;
+    }
+  }
+  return result;
 }
 
 already_AddRefed<ContainerLayer>
 FrameLayerBuilder::BuildContainerLayerFor(nsDisplayListBuilder* aBuilder,
                                           LayerManager* aManager,
                                           nsIFrame* aContainerFrame,
                                           nsDisplayItem* aContainerItem,
                                           const nsDisplayList& aChildren,
@@ -1928,16 +1942,44 @@ FrameLayerBuilder::GetDedicatedLayer(nsI
           !layer->HasUserData(&gImageLayerUserData) &&
           !layer->HasUserData(&gThebesDisplayItemLayerUserData))
         return layer;
     }
   }
   return nsnull;
 }
 
+/*
+ * A note on residual transforms:
+ *
+ * In a transformed subtree we sometimes apply the ThebesLayer's
+ * "residual transform" when drawing content into the ThebesLayer.
+ * This is a translation by components in the range [-0.5,0.5) provided
+ * by the layer system; applying the residual transform followed by the
+ * transforms used by layer compositing ensures that the subpixel alignment
+ * of the content of the ThebesLayer exactly matches what it would be if
+ * we used cairo/Thebes to draw directly to the screen without going through
+ * retained layer buffers.
+ *
+ * The visible and valid regions of the ThebesLayer are computed without
+ * knowing the residual transform (because we don't know what the residual
+ * transform is going to be until we've built the layer tree!). So we have to
+ * consider whether content painted in the range [x, xmost) might be painted
+ * outside the visible region we computed for that content. The visible region
+ * would be [floor(x), ceil(xmost)). The content would be rendered at
+ * [x + r, xmost + r), where -0.5 <= r < 0.5. So some half-rendered pixels could
+ * indeed fall outside the computed visible region, which is not a big deal;
+ * similar issues already arise when we snap cliprects to nearest pixels.
+ * Note that if the rendering of the content is snapped to nearest pixels ---
+ * which it often is --- then the content is actually rendered at
+ * [snap(x + r), snap(xmost + r)). It turns out that floor(x) <= snap(x + r)
+ * and ceil(xmost) >= snap(xmost + r), so the rendering of snapped content
+ * always falls within the visible region we computed.
+ */
+
 /* static */ void
 FrameLayerBuilder::DrawThebesLayer(ThebesLayer* aLayer,
                                    gfxContext* aContext,
                                    const nsIntRegion& aRegionToDraw,
                                    const nsIntRegion& aRegionToInvalidate,
                                    void* aCallbackData)
 {
   nsDisplayListBuilder* builder = static_cast<nsDisplayListBuilder*>
@@ -1971,17 +2013,20 @@ FrameLayerBuilder::DrawThebesLayer(Thebe
     aContext->Fill();
   }
 
   // make the origin of the context coincide with the origin of the
   // ThebesLayer
   gfxContextMatrixAutoSaveRestore saveMatrix(aContext);
   nsIntPoint offset = GetTranslationForThebesLayer(aLayer);
   aContext->Scale(userData->mXScale, userData->mYScale);
-  aContext->Translate(-gfxPoint(offset.x, offset.y));
+  // Apply the residual transform if it has been enabled, to ensure that
+  // snapping when we draw into aContext exactly matches the ideal transform.
+  // See above for why this is OK.
+  aContext->Translate(aLayer->GetResidualTranslation() - gfxPoint(offset.x, offset.y));
 
   nsPresContext* presContext = containerLayerFrame->PresContext();
   PRInt32 appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
   nsRect r = (aRegionToInvalidate.GetBounds() + offset).
     ToAppUnits(appUnitsPerDevPixel);
   r.ScaleInverseRoundOut(userData->mXScale, userData->mYScale);
   containerLayerFrame->InvalidateWithFlags(r,
       nsIFrame::INVALIDATE_NO_THEBES_LAYERS |
--- a/layout/base/FrameLayerBuilder.h
+++ b/layout/base/FrameLayerBuilder.h
@@ -137,20 +137,30 @@ public:
   /**
    * Call this after we end a transaction on aManager. If aManager
    * is not the retained layer manager then it must be a temporary layer
    * manager that will not be used again.
    */
   void DidEndTransaction(LayerManager* aManager);
 
   struct ContainerParameters {
-    ContainerParameters() : mXScale(1), mYScale(1) {}
+    ContainerParameters() :
+      mXScale(1), mYScale(1),
+      mInTransformedSubtree(false), mInActiveTransformedSubtree(false) {}
     ContainerParameters(float aXScale, float aYScale) :
-      mXScale(aXScale), mYScale(aYScale) {}
+      mXScale(aXScale), mYScale(aYScale),
+      mInTransformedSubtree(false), mInActiveTransformedSubtree(false) {}
+    ContainerParameters(float aXScale, float aYScale,
+                        const ContainerParameters& aParent) :
+      mXScale(aXScale), mYScale(aYScale),
+      mInTransformedSubtree(aParent.mInTransformedSubtree),
+      mInActiveTransformedSubtree(aParent.mInActiveTransformedSubtree) {}
     float mXScale, mYScale;
+    bool mInTransformedSubtree;
+    bool mInActiveTransformedSubtree;
   };
   /**
    * Build a container layer for a display item that contains a child
    * list, either reusing an existing one or creating a new one. It
    * sets the container layer children to layers which together render
    * the contents of the display list. It reuses existing layers from
    * the retained layer manager if possible.
    * aContainer may be null, in which case we construct a root layer.
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/637597-1-ref.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<style>
+#parent {
+  position: absolute;
+  top: 100.8px;
+  -moz-transform: scale(1.1232,1.1232);
+  transform: scale(1.1232,1.1232);
+}
+#b1 {
+  position: absolute;
+  width: 100px;
+  height: 109px;
+  background: #c00;
+  -moz-transform: translate(100px,100px);
+  transform: translate(100px,100px);
+}
+#b2 {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100px;
+  height: 100px;
+  background: white;
+  -moz-transform: translate(400px,0);
+  transform: translate(400px,0);
+}
+</style>
+</head>
+<body>
+<div id="parent">
+  <div id="b1"></div>
+  <div id="b2"></div>
+</div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/637597-1.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<style>
+html { background:white; }
+#parent {
+  position: absolute;
+  top: 100.8px;
+  -moz-transform: scale(1.1232,1.1232);
+  transform: scale(1.1232,1.1232);
+}
+#b1 {
+  position: absolute;
+  width: 100px;
+  height: 109px;
+  background: #c00;
+  -moz-transform: translate(100px,100px);
+  transform: translate(100px,100px);
+}
+#b2 {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100px;
+  height: 100px;
+  background: white;
+  -moz-transform: translate(400px,200px);
+  transform: translate(400px,200px);
+}
+#b2.done {
+  -moz-transform: translate(400px,0);
+  transform: translate(400px,0);
+}
+</style>
+</head>
+<body>
+<div id="parent">
+  <div id="b1"></div>
+  <div id="b2"></div>
+</div>
+<script>
+function doTest() {
+  document.getElementById('b2').setAttribute("class", "done");
+  document.documentElement.removeAttribute("class");
+}
+window.addEventListener("MozReftestInvalidate", doTest, false);
+</script>
+</body>
+</html>
--- a/layout/reftests/bugs/reftest.list
+++ b/layout/reftests/bugs/reftest.list
@@ -1624,16 +1624,17 @@ skip-if(Android) fails-if(winWidget) == 
 == 632781-normalsize.html 632781-ref.html
 fails-if(Android) == 634232-1.html 634232-1-ref.html
 fails-if(Android) == 635302-1.html 635302-1-ref.html
 == 635373-1.html 635373-1-ref.html
 == 635373-2.html 635373-2-ref.html
 fails-if(http.platform=="X11"&&!layersGPUAccelerated) == 635373-3.html 635373-3-ref.html
 HTTP(..) == 635639-1.html 635639-1-ref.html
 HTTP(..) == 635639-2.html 635639-2-ref.html
+== 637597-1.html 637597-1-ref.html
 == 637852-1.html 637852-1-ref.html
 == 641770-1.html 641770-1-ref.html
 == 641856-1.html 641856-1-ref.html
 == 645491-1.html 645491-1-ref.html
 == 645768-1.html 645768-1-ref.html
 fails-if(layersGPUAccelerated&&cocoaWidget) == 650228-1.html 650228-1-ref.html # Quartz alpha blending doesn't match GL alpha blending
 == 653930-1.html 653930-1-ref.html
 HTTP(..) == 654057-1.html 654057-1-ref.html