Bug 777194. Part 5: When choosing a subpixel position to scroll to, align the new position with the position that was most recently used to rerender the scrolled layer(s). r=tnikkel
authorRobert O'Callahan <robert@ocallahan.org>
Fri, 17 Aug 2012 11:40:10 +1200
changeset 107473 2e2c7ed818d6f352bbce323909d3a6e0b0b8415d
parent 107472 f2ef195a022a7dac87e0f0eca2383c422b8bb148
child 107474 54d5957d6dbb5fcaa6fbbce855543a94d376ea5a
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewerstnikkel
bugs777194
milestone18.0a1
Bug 777194. Part 5: When choosing a subpixel position to scroll to, align the new position with the position that was most recently used to rerender the scrolled layer(s). r=tnikkel If we always align the new scroll position with the previous scroll position, we can accumulate error and gradually drift out of alignment with the layer pixels until we exceed the tolerance and are forced to rerender everything.
layout/base/FrameLayerBuilder.cpp
layout/base/nsLayoutUtils.cpp
layout/base/nsLayoutUtils.h
layout/generic/nsGfxScrollFrame.cpp
layout/generic/nsGfxScrollFrame.h
layout/generic/nsIScrollableFrame.h
--- a/layout/base/FrameLayerBuilder.cpp
+++ b/layout/base/FrameLayerBuilder.cpp
@@ -12,16 +12,17 @@
 #include "BasicLayers.h"
 #include "nsSubDocumentFrame.h"
 #include "nsCSSRendering.h"
 #include "nsCSSFrameConstructor.h"
 #include "gfxUtils.h"
 #include "nsImageFrame.h"
 #include "nsRenderingContext.h"
 #include "MaskLayerImageCache.h"
+#include "nsIScrollableFrame.h"
 
 #include "mozilla/Preferences.h"
 #include "sampler.h"
 
 #include "nsAnimationManager.h"
 #include "nsTransitionManager.h"
 
 #ifdef DEBUG
@@ -1092,23 +1093,16 @@ GetTranslationForThebesLayer(ThebesLayer
       (aLayer->GetUserData(&gThebesDisplayItemLayerUserData));
   NS_ASSERTION(data, "Must be a tracked thebes layer!");
 
   return data->mTranslation;
 }
 
 static const double SUBPIXEL_OFFSET_EPSILON = 0.02;
 
-static bool
-SubpixelOffsetFuzzyEqual(gfxPoint aV1, gfxPoint aV2)
-{
-  return fabs(aV2.x - aV1.x) < SUBPIXEL_OFFSET_EPSILON &&
-         fabs(aV2.y - aV1.y) < SUBPIXEL_OFFSET_EPSILON;
-}
-
 /**
  * This normally computes NSToIntRoundUp(aValue). However, if that would
  * give a residual near 0.5 while aOldResidual is near -0.5, or
  * it would give a residual near -0.5 while aOldResidual is near 0.5, then
  * instead we return the integer in the other direction so that the residual
  * is close to aOldResidual.
  */
 static int32_t
@@ -1125,22 +1119,40 @@ RoundToMatchResidual(double aValue, doub
     if (residual < 0 && fabs(residual + 1.0 - aOldResidual) < SUBPIXEL_OFFSET_EPSILON) {
       // Round down instead
       return int32_t(floor(aValue));
     }
   }
   return v;
 }
 
+static void
+ResetScrollPositionForLayerPixelAlignment(const nsIFrame* aActiveScrolledRoot)
+{
+  nsIScrollableFrame* sf = nsLayoutUtils::GetScrollableFrameFor(aActiveScrolledRoot);
+  if (sf) {
+    sf->ResetScrollPositionForLayerPixelAlignment();
+  }
+}
+
+static void
+InvalidateEntireThebesLayer(ThebesLayer* aLayer, const nsIFrame* aActiveScrolledRoot)
+{
+  nsIntRect invalidate = aLayer->GetValidRegion().GetBounds();
+  aLayer->InvalidateRegion(invalidate);
+  ResetScrollPositionForLayerPixelAlignment(aActiveScrolledRoot);
+}
+
 already_AddRefed<ThebesLayer>
 ContainerState::CreateOrRecycleThebesLayer(const nsIFrame* aActiveScrolledRoot, const nsIFrame* aReferenceFrame)
 {
   // We need a new thebes layer
   nsRefPtr<ThebesLayer> layer;
   ThebesDisplayItemLayerUserData* data;
+  bool didResetScrollPositionForLayerPixelAlignment = false;
   if (mNextFreeRecycledThebesLayer < mRecycledThebesLayers.Length()) {
     // Recycle a layer
     layer = mRecycledThebesLayers[mNextFreeRecycledThebesLayer];
     ++mNextFreeRecycledThebesLayer;
     // Clear clip rect and mask layer so we don't accidentally stay clipped.
     // We will reapply any necessary clipping.
     layer->SetClipRect(nullptr);
     layer->SetMaskLayer(nullptr);
@@ -1155,33 +1167,38 @@ ContainerState::CreateOrRecycleThebesLay
     // to, or if we need to invalidate the entire layer, we can do that.
     // This needs to be done before we update the ThebesLayer to its new
     // transform. See nsGfxScrollFrame::InvalidateInternal, where
     // we ensure that mInvalidThebesContent is updated according to the
     // scroll position as of the most recent paint.
     if (mInvalidateAllThebesContent ||
         data->mXScale != mParameters.mXScale ||
         data->mYScale != mParameters.mYScale) {
-      nsIntRect invalidate = layer->GetValidRegion().GetBounds();
-      layer->InvalidateRegion(invalidate);
+      InvalidateEntireThebesLayer(layer, aActiveScrolledRoot);
+      didResetScrollPositionForLayerPixelAlignment = true;
     } else {
-      InvalidatePostTransformRegion(layer, mInvalidThebesContent,
-                                    GetTranslationForThebesLayer(layer));
+      nsIntRect bounds = mInvalidThebesContent.GetBounds();
+      if (!bounds.IsEmpty()) {
+        InvalidatePostTransformRegion(layer, mInvalidThebesContent,
+                                      GetTranslationForThebesLayer(layer));
+      }
     }
     // We do not need to Invalidate these areas in the widget because we
     // assume the caller of InvalidateThebesLayerContents has ensured
     // the area is invalidated in the widget.
   } else {
     // Create a new thebes layer
     layer = mManager->CreateThebesLayer();
     if (!layer)
       return nullptr;
     // Mark this layer as being used for Thebes-painting display items
     data = new ThebesDisplayItemLayerUserData();
     layer->SetUserData(&gThebesDisplayItemLayerUserData, data);
+    ResetScrollPositionForLayerPixelAlignment(aActiveScrolledRoot);
+    didResetScrollPositionForLayerPixelAlignment = true;
   }
   data->mXScale = mParameters.mXScale;
   data->mYScale = mParameters.mYScale;
   layer->SetAllowResidualTranslation(mParameters.AllowResidualTranslation());
 
   mLayerBuilder->SaveLastPaintOffset(layer);
 
   // Set up transform so that 0,0 in the Thebes layer corresponds to the
@@ -1204,20 +1221,21 @@ ContainerState::CreateOrRecycleThebesLay
   // FIXME: Temporary workaround for bug 681192 and bug 724786.
 #ifndef MOZ_JAVA_COMPOSITOR
   // Calculate exact position of the top-left of the active scrolled root.
   // This might not be 0,0 due to the snapping in ScaleToNearestPixels.
   gfxPoint activeScrolledRootTopLeft = scaledOffset - matrix.GetTranslation();
   // If it has changed, then we need to invalidate the entire layer since the
   // pixels in the layer buffer have the content at a (subpixel) offset
   // from what we need.
-  if (!SubpixelOffsetFuzzyEqual(activeScrolledRootTopLeft, data->mActiveScrolledRootPosition)) {
+  if (!activeScrolledRootTopLeft.WithinEpsilonOf(data->mActiveScrolledRootPosition, SUBPIXEL_OFFSET_EPSILON)) {
     data->mActiveScrolledRootPosition = activeScrolledRootTopLeft;
-    nsIntRect invalidate = layer->GetValidRegion().GetBounds();
-    layer->InvalidateRegion(invalidate);
+    InvalidateEntireThebesLayer(layer, aActiveScrolledRoot);
+  } else if (didResetScrollPositionForLayerPixelAlignment) {
+    data->mActiveScrolledRootPosition = activeScrolledRootTopLeft;
   }
 #endif
 
   return layer.forget();
 }
 
 /**
  * Returns the appunits per dev pixel for the item's frame. The item must
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -855,17 +855,17 @@ nsLayoutUtils::FindSiblingViewFor(nsIVie
       return insertBefore;
     }
   }
   return nullptr;
 }
 
 //static
 nsIScrollableFrame*
-nsLayoutUtils::GetScrollableFrameFor(nsIFrame *aScrolledFrame)
+nsLayoutUtils::GetScrollableFrameFor(const nsIFrame *aScrolledFrame)
 {
   nsIFrame *frame = aScrolledFrame->GetParent();
   if (!frame) {
     return nullptr;
   }
   nsIScrollableFrame *sf = do_QueryFrame(frame);
   return sf;
 }
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -318,17 +318,17 @@ public:
    * a displayport set, and aActiveScrolledRoot is scrolled by that scrollframe.
    */
   static bool IsScrolledByRootContentDocumentDisplayportScrolling(const nsIFrame* aActiveScrolledRoot,
                                                                   nsDisplayListBuilder* aBuilder);
 
   /**
     * GetScrollableFrameFor returns the scrollable frame for a scrolled frame
     */
-  static nsIScrollableFrame* GetScrollableFrameFor(nsIFrame *aScrolledFrame);
+  static nsIScrollableFrame* GetScrollableFrameFor(const nsIFrame *aScrolledFrame);
 
   /**
    * GetNearestScrollableFrameForDirection locates the first ancestor of
    * aFrame (or aFrame itself) that is scrollable with overflow:scroll or
    * overflow:auto in the given direction and where either the scrollbar for
    * that direction is visible or the frame can be scrolled by some
    * positive amount in that direction.
    * The search extends across document boundaries.
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -1588,16 +1588,17 @@ nsGfxScrollFrameInner::nsGfxScrollFrameI
   , mScrollCornerBox(nullptr)
   , mResizerBox(nullptr)
   , mOuter(aOuter)
   , mAsyncScroll(nullptr)
   , mDestination(0, 0)
   , mScrollPosAtLastPaint(0, 0)
   , mRestorePos(-1, -1)
   , mLastPos(-1, -1)
+  , mScrollPosForLayerPixelAlignment(-1, -1)
   , mNeverHasVerticalScrollbar(false)
   , mNeverHasHorizontalScrollbar(false)
   , mHasVerticalScrollbar(false)
   , mHasHorizontalScrollbar(false)
   , mFrameIsUpdatingScrollbar(false)
   , mDidHistoryRestore(false)
   , mIsRoot(aIsRoot)
   , mSupppressScrollbarUpdate(false)
@@ -2086,34 +2087,35 @@ void
 nsGfxScrollFrameInner::ScrollToImpl(nsPoint aPt, const nsRect& aRange)
 {
   nsPresContext* presContext = mOuter->PresContext();
   nscoord appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
   // 'scale' is our estimate of the scale factor that will be applied
   // when rendering the scrolled content to its own ThebesLayer.
   gfxSize scale = FrameLayerBuilder::GetThebesLayerScaleForFrame(mScrolledFrame);
   nsPoint curPos = GetScrollPosition();
+  nsPoint alignWithPos = mScrollPosForLayerPixelAlignment == nsPoint(-1,-1)
+      ? curPos : mScrollPosForLayerPixelAlignment;
   // Try to align aPt with curPos so they have an integer number of layer
   // pixels between them. This gives us the best chance of scrolling without
   // having to invalidate due to changes in subpixel rendering.
   // Note that when we actually draw into a ThebesLayer, the coordinates
   // that get mapped onto the layer buffer pixels are from the display list,
   // which are relative to the display root frame's top-left increasing down,
   // whereas here our coordinates are scroll positions which increase upward
   // and are relative to the scrollport top-left. This difference doesn't actually
   // matter since all we are about is that there be an integer number of
   // layer pixels between pt and curPos.
   nsPoint pt =
     ClampAndAlignWithLayerPixels(aPt,
                                  GetScrollRangeForClamping(),
                                  aRange,
-                                 curPos,
+                                 alignWithPos,
                                  appUnitsPerDevPixel,
                                  scale);
-
   if (pt == curPos) {
     return;
   }
 
   // notify the listeners.
   for (uint32_t i = 0; i < mListeners.Length(); i++) {
     mListeners[i]->ScrollPositionWillChange(pt.x, pt.y);
   }
@@ -2231,16 +2233,23 @@ nsGfxScrollFrameInner::BuildDisplayList(
   nsresult rv = mOuter->DisplayBorderBackgroundOutline(aBuilder, aLists);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (aBuilder->IsPaintingToWindow()) {
     mScrollPosAtLastPaint = GetScrollPosition();
     if (IsScrollingActive() && !CanScrollWithBlitting(mOuter)) {
       MarkInactive();
     }
+    if (IsScrollingActive()) {
+      if (mScrollPosForLayerPixelAlignment == nsPoint(-1,-1)) {
+        mScrollPosForLayerPixelAlignment = mScrollPosAtLastPaint;
+      }
+    } else {
+      mScrollPosForLayerPixelAlignment = nsPoint(-1,-1);
+    }
   }
 
   if (aBuilder->GetIgnoreScrollFrame() == mOuter || IsIgnoringViewportClipping()) {
     // Don't clip the scrolled child, and don't paint scrollbars/scrollcorner.
     // The scrolled frame shouldn't have its own background/border, so we
     // can just pass aLists directly.
     return mOuter->BuildDisplayListForChild(aBuilder, mScrolledFrame,
                                             aDirtyRect, aLists);
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -227,16 +227,20 @@ public:
     return (mHasVerticalScrollbar ? nsIScrollableFrame::VERTICAL : 0) |
            (mHasHorizontalScrollbar ? nsIScrollableFrame::HORIZONTAL : 0);
   }
   nsMargin GetActualScrollbarSizes() const;
   nsMargin GetDesiredScrollbarSizes(nsBoxLayoutState* aState);
   bool IsLTR() const;
   bool IsScrollbarOnRight() const;
   bool IsScrollingActive() const { return mScrollingActive || ShouldBuildLayer(); }
+  void ResetScrollPositionForLayerPixelAlignment()
+  {
+    mScrollPosForLayerPixelAlignment = GetScrollPosition();
+  }
 
   bool UpdateOverflow();
 
   // adjust the scrollbar rectangle aRect to account for any visible resizer.
   // aHasResizer specifies if there is a content resizer, however this method
   // will also check if a widget resizer is present as well.
   void AdjustScrollbarRectForResizer(nsIFrame* aFrame, nsPresContext* aPresContext,
                                      nsRect& aRect, bool aHasResizer, bool aVertical);
@@ -285,16 +289,17 @@ public:
   nsPoint mScrollPosAtLastPaint;
 
   nsPoint mRestorePos;
   nsPoint mLastPos;
 
   nsExpirationState mActivityExpirationState;
 
   nsCOMPtr<nsITimer> mScrollActivityTimer;
+  nsPoint mScrollPosForLayerPixelAlignment;
 
   bool mNeverHasVerticalScrollbar:1;
   bool mNeverHasHorizontalScrollbar:1;
   bool mHasVerticalScrollbar:1;
   bool mHasHorizontalScrollbar:1;
   bool mFrameIsUpdatingScrollbar:1;
   bool mDidHistoryRestore:1;
   // Is this the scrollframe for the document's viewport?
@@ -506,16 +511,19 @@ public:
   }
   NS_IMETHOD PostScrolledAreaEventForCurrentArea() MOZ_OVERRIDE {
     mInner.PostScrolledAreaEvent();
     return NS_OK;
   }
   virtual bool IsScrollingActive() MOZ_OVERRIDE {
     return mInner.IsScrollingActive();
   }
+  virtual void ResetScrollPositionForLayerPixelAlignment() {
+    mInner.ResetScrollPositionForLayerPixelAlignment();
+  }
   virtual bool UpdateOverflow() {
     return mInner.UpdateOverflow();
   }
 
   // nsIStatefulFrame
   NS_IMETHOD SaveState(SpecialStateID aStateID, nsPresState** aState) MOZ_OVERRIDE {
     NS_ENSURE_ARG_POINTER(aState);
     *aState = mInner.SaveState(aStateID);
@@ -754,16 +762,19 @@ public:
   }
   NS_IMETHOD PostScrolledAreaEventForCurrentArea() MOZ_OVERRIDE {
     mInner.PostScrolledAreaEvent();
     return NS_OK;
   }
   virtual bool IsScrollingActive() MOZ_OVERRIDE {
     return mInner.IsScrollingActive();
   }
+  virtual void ResetScrollPositionForLayerPixelAlignment() {
+    mInner.ResetScrollPositionForLayerPixelAlignment();
+  }
   virtual bool UpdateOverflow() {
     return mInner.UpdateOverflow();
   }
 
   // nsIStatefulFrame
   NS_IMETHOD SaveState(SpecialStateID aStateID, nsPresState** aState) MOZ_OVERRIDE {
     NS_ENSURE_ARG_POINTER(aState);
     *aState = mInner.SaveState(aStateID);
--- a/layout/generic/nsIScrollableFrame.h
+++ b/layout/generic/nsIScrollableFrame.h
@@ -199,11 +199,16 @@ public:
   NS_IMETHOD PostScrolledAreaEventForCurrentArea() = 0;
 
   /**
    * Returns true if this scrollframe is being "actively scrolled".
    * This basically means that we should allocate resources in the
    * expectation that scrolling is going to happen.
    */
   virtual bool IsScrollingActive() = 0;
+  /**
+   * Call this when the layer(s) induced by active scrolling are being
+   * completely redrawn.
+   */
+  virtual void ResetScrollPositionForLayerPixelAlignment() = 0;
 };
 
 #endif