Bug 1423011 - Part 1: Allow APZ to async-scroll the layout viewport. r?botond draft
authorKashav Madan <kmadan@mozilla.com>, Tanushree Podder <tpodder@mozilla.com>
Thu, 07 Jun 2018 17:01:36 -0400
changeset 812573 93c2f4c13891e2aef44357a7be9ac8e2dadd1d38
parent 812548 a009b5249a4b78a889fdc5ffcf55ad51715cc686
child 812574 3f2de3d877d5328e94bb753a8f4f3ea1eba3f23d
push id114599
push userbmo:kmadan@mozilla.com
push dateFri, 29 Jun 2018 16:55:56 +0000
reviewersbotond
bugs1423011
milestone63.0a1
Bug 1423011 - Part 1: Allow APZ to async-scroll the layout viewport. r?botond There are 3 main components to this change: a) Store the origin of the layout viewport in APZ (until now we only stored it's size). This required updating the offset stored in mViewport, which was previously (0, 0). b) Adjust the layout viewport in APZ each time the visual viewport exceeds the bounds of the layout viewport. c) Update the main thread to store the layout viewport offset for the RCD-RSF (currently it uses the layout viewport offset for overflow:hidden pages, and the visual viewport offset otherwise). MozReview-Commit-ID: 7AD8wvthh2m
gfx/layers/FrameMetrics.cpp
gfx/layers/FrameMetrics.h
gfx/layers/apz/src/AndroidAPZ.cpp
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/src/AsyncPanZoomController.h
gfx/layers/apz/src/GenericFlingAnimation.h
gfx/layers/apz/src/GenericScrollAnimation.cpp
gfx/layers/apz/util/APZCCallbackHelper.cpp
layout/base/nsLayoutUtils.cpp
--- a/gfx/layers/FrameMetrics.cpp
+++ b/gfx/layers/FrameMetrics.cpp
@@ -9,16 +9,49 @@
 #include "nsStyleConsts.h"
 
 namespace mozilla {
 namespace layers {
 
 const FrameMetrics::ViewID FrameMetrics::NULL_SCROLL_ID = 0;
 
 void
+FrameMetrics::RecalculateViewportOffset()
+{
+  if (!mIsRootContent) {
+    return;
+  }
+  CSSRect visualViewport = GetVisualViewport();
+  // If the visual viewport is contained within the layout viewport, we don't
+  // need to make any adjustments, so we can exit early.
+  //
+  // Additionally, if the composition bounds changes (due to an orientation
+  // change, window resize, etc.), it may take a few frames for mViewport to
+  // update and during that time, the visual viewport may be larger than the
+  // layout viewport, so don't attempt to make any adjustments.
+  if (mViewport.Contains(visualViewport) ||
+      (mViewport.Width() < visualViewport.Width() &&
+       !FuzzyEqualsMultiplicative(mViewport.Width(), visualViewport.Width())) ||
+      (mViewport.Height() < visualViewport.Height() &&
+       !FuzzyEqualsMultiplicative(mViewport.Height(), visualViewport.Height()))) {
+    return;
+  }
+  if (visualViewport.X() < mViewport.X()) {
+    mViewport.MoveToX(visualViewport.X());
+  } else if (mViewport.XMost() < visualViewport.XMost()) {
+    mViewport.MoveByX(visualViewport.XMost() - mViewport.XMost());
+  }
+  if (visualViewport.Y() < mViewport.Y()) {
+    mViewport.MoveToY(visualViewport.Y());
+  } else if (mViewport.YMost() < visualViewport.YMost()) {
+    mViewport.MoveByY(visualViewport.YMost() - mViewport.YMost());
+  }
+}
+
+void
 ScrollMetadata::SetUsesContainerScrolling(bool aValue) {
   MOZ_ASSERT_IF(aValue, gfxPrefs::LayoutUseContainersForRootFrames());
   mUsesContainerScrolling = aValue;
 }
 
 static OverscrollBehavior
 ToOverscrollBehavior(StyleOverscrollBehavior aBehavior)
 {
--- a/gfx/layers/FrameMetrics.h
+++ b/gfx/layers/FrameMetrics.h
@@ -476,16 +476,21 @@ public:
     mViewport = aViewport;
   }
 
   const CSSRect& GetViewport() const
   {
     return mViewport;
   }
 
+  CSSRect GetVisualViewport() const
+  {
+    return CSSRect(mScrollOffset, CalculateCompositedSizeInCssPixels());
+  }
+
   void SetExtraResolution(const ScreenToLayerScale2D& aExtraResolution)
   {
     mExtraResolution = aExtraResolution;
   }
 
   const ScreenToLayerScale2D& GetExtraResolution() const
   {
     return mExtraResolution;
@@ -521,16 +526,23 @@ public:
 
   void SetIsScrollInfoLayer(bool aIsScrollInfoLayer) {
     mIsScrollInfoLayer = aIsScrollInfoLayer;
   }
   bool IsScrollInfoLayer() const {
     return mIsScrollInfoLayer;
   }
 
+  // Determine if the visual viewport is outside of the layout viewport and
+  // adjust the x,y-offset in mViewport accordingly. This is necessary to
+  // allow APZ to async-scroll the layout viewport.
+  //
+  // This is a no-op if mIsRootContent is false.
+  void RecalculateViewportOffset();
+
 private:
   // A unique ID assigned to each scrollable frame.
   ViewID mScrollId;
 
   // The pres-shell resolution that has been induced on the document containing
   // this scroll frame as a result of zooming this scroll frame (whether via
   // user action, or choosing an initial zoom level on page load). This can
   // only be different from 1.0 for frames that are zoomable, which currently
--- a/gfx/layers/apz/src/AndroidAPZ.cpp
+++ b/gfx/layers/apz/src/AndroidAPZ.cpp
@@ -237,17 +237,17 @@ StackScrollerFlingAnimation::DoSample(Fr
       DeferHandleFlingOverscroll(velocity);
     }
     return false;
   }
 
   mPreviousOffset = offset;
 
   mApzc.SetVelocityVector(velocity);
-  aFrameMetrics.SetScrollOffset(offset / aFrameMetrics.GetZoom());
+  mApzc.SetScrollOffset(offset / aFrameMetrics.GetZoom());
 
   // If we hit a bounds while flinging, send the velocity so that the bounce
   // animation can play.
   if (hitBoundX || hitBoundY) {
     ParentLayerPoint bounceVelocity = velocity;
 
     if (!mSentBounceX && hitBoundX && fabsf(offset.x - mStartOffset.x) > BOUNDS_EPSILON) {
       mSentBounceX = true;
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -576,67 +576,71 @@ public:
 
 private:
   AsyncPanZoomController* mApzc;
   AsyncPanZoomController::PanZoomState mInitialState;
 };
 
 class ZoomAnimation: public AsyncPanZoomAnimation {
 public:
-  ZoomAnimation(const CSSPoint& aStartOffset,
+  ZoomAnimation(AsyncPanZoomController& aApzc,
+                const CSSPoint& aStartOffset,
                 const CSSToParentLayerScale2D& aStartZoom,
                 const CSSPoint& aEndOffset,
                 const CSSToParentLayerScale2D& aEndZoom)
-    : mTotalDuration(
+    : mApzc(aApzc)
+    , mTotalDuration(
         TimeDuration::FromMilliseconds(gfxPrefs::APZZoomAnimationDuration()))
     , mStartOffset(aStartOffset)
     , mStartZoom(aStartZoom)
     , mEndOffset(aEndOffset)
     , mEndZoom(aEndZoom)
   {
   }
 
   virtual bool DoSample(FrameMetrics& aFrameMetrics,
                         const TimeDuration& aDelta) override
   {
     mDuration += aDelta;
     double animPosition = mDuration / mTotalDuration;
 
     if (animPosition >= 1.0) {
       aFrameMetrics.SetZoom(mEndZoom);
-      aFrameMetrics.SetScrollOffset(mEndOffset);
+      mApzc.SetScrollOffset(mEndOffset);
       return false;
     }
 
     // Sample the zoom at the current time point.  The sampled zoom
     // will affect the final computed resolution.
     float sampledPosition =
       gZoomAnimationFunction->GetValue(animPosition,
         ComputedTimingFunction::BeforeFlag::Unset);
 
     // We scale the scrollOffset linearly with sampledPosition, so the zoom
     // needs to scale inversely to match.
     aFrameMetrics.SetZoom(CSSToParentLayerScale2D(
       1 / (sampledPosition / mEndZoom.xScale + (1 - sampledPosition) / mStartZoom.xScale),
       1 / (sampledPosition / mEndZoom.yScale + (1 - sampledPosition) / mStartZoom.yScale)));
 
-    aFrameMetrics.SetScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point(
+    mApzc.SetScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point(
       mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition),
       mEndOffset.y * sampledPosition + mStartOffset.y * (1 - sampledPosition)
     )));
 
     return true;
   }
 
   virtual bool WantsRepaints() override
   {
     return false;
   }
 
 private:
+  AsyncPanZoomController& mApzc;
+
   TimeDuration mDuration;
   const TimeDuration mTotalDuration;
 
   // Old metrics from before we started a zoom animation. This is only valid
   // when we are in the "ANIMATED_ZOOM" state. This is used so that we can
   // interpolate between the start and end frames. We only use the
   // |mViewportScrollOffset| and |mResolution| fields on this.
   CSSPoint mStartOffset;
@@ -675,17 +679,17 @@ public:
     nsPoint oneParentLayerPixel =
       CSSPoint::ToAppUnits(ParentLayerPoint(1, 1) / aFrameMetrics.GetZoom());
     if (mXAxisModel.IsFinished(oneParentLayerPixel.x) &&
         mYAxisModel.IsFinished(oneParentLayerPixel.y)) {
       // Set the scroll offset to the exact destination. If we allow the scroll
       // offset to end up being a bit off from the destination, we can get
       // artefacts like "scroll to the next snap point in this direction"
       // scrolling to the snap point we're already supposed to be at.
-      aFrameMetrics.ClampAndSetScrollOffset(
+      mApzc.ClampAndSetScrollOffset(
           CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetDestination(),
                                          mYAxisModel.GetDestination())));
       return false;
     }
 
     mXAxisModel.Simulate(aDelta);
     mYAxisModel.Simulate(aDelta);
 
@@ -714,17 +718,17 @@ public:
     CSSToParentLayerScale2D zoom = aFrameMetrics.GetZoom();
     ParentLayerPoint displacement = (position - aFrameMetrics.GetScrollOffset()) * zoom;
 
     ParentLayerPoint overscroll;
     ParentLayerPoint adjustedOffset;
     mApzc.mX.AdjustDisplacement(displacement.x, adjustedOffset.x, overscroll.x);
     mApzc.mY.AdjustDisplacement(displacement.y, adjustedOffset.y, overscroll.y);
 
-    aFrameMetrics.ScrollBy(adjustedOffset / zoom);
+    mApzc.ScrollBy(adjustedOffset / zoom);
 
     // The smooth scroll may have caused us to reach the end of our scroll range.
     // This can happen if either the layout.css.scroll-behavior.damping-ratio
     // preference is set to less than 1 (underdamped) or if a smooth scroll
     // inherits velocity from a fling gesture.
     if (!IsZero(overscroll)) {
       // Hand off a fling with the remaining momentum to the next APZC in the
       // overscroll handoff chain.
@@ -1070,17 +1074,17 @@ nsEventStatus AsyncPanZoomController::Ha
 
   CSSPoint scrollOffset = mFrameMetrics.GetScrollOffset();
   if (direction == ScrollDirection::eHorizontal) {
     scrollOffset.x = scrollPosition;
   } else {
     scrollOffset.y = scrollPosition;
   }
   APZC_LOG("%p set scroll offset to %s from scrollbar drag\n", this, Stringify(scrollOffset).c_str());
-  mFrameMetrics.SetScrollOffset(scrollOffset);
+  SetScrollOffset(scrollOffset);
   ScheduleCompositeAndMaybeRepaint();
   UpdateSharedCompositorFrameMetrics();
 
   return nsEventStatus_eConsumeNoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent,
                                                        const ScreenToParentLayerMatrix4x4& aTransformToApzc) {
@@ -3305,42 +3309,57 @@ void AsyncPanZoomController::AdjustScrol
   RecursiveMutexAutoLock lock(mRecursiveMutex);
   CSSPoint adjustment =
     ViewAs<ParentLayerPixel>(aShift, PixelCastJustification::ScreenIsParentLayerForRoot)
     / mFrameMetrics.GetZoom();
   APZC_LOG("%p adjusting scroll position by %s for surface shift\n",
     this, Stringify(adjustment).c_str());
   CSSRect scrollRange = mFrameMetrics.CalculateScrollRange();
   // Apply shift to mFrameMetrics.mScrollOffset.
-  mFrameMetrics.SetScrollOffset(scrollRange.ClampPoint(
+  SetScrollOffset(scrollRange.ClampPoint(
       mFrameMetrics.GetScrollOffset() + adjustment));
   // Apply shift to mCompositedScrollOffset, since the dynamic toolbar expects
   // the shift to take effect right away, without the usual frame delay.
   mCompositedScrollOffset = scrollRange.ClampPoint(
       mCompositedScrollOffset + adjustment);
   RequestContentRepaint();
   UpdateSharedCompositorFrameMetrics();
 }
 
+void AsyncPanZoomController::SetScrollOffset(const CSSPoint& aOffset) {
+  mFrameMetrics.SetScrollOffset(aOffset);
+  mFrameMetrics.RecalculateViewportOffset();
+}
+
+void AsyncPanZoomController::ClampAndSetScrollOffset(const CSSPoint& aOffset) {
+  mFrameMetrics.ClampAndSetScrollOffset(aOffset);
+  mFrameMetrics.RecalculateViewportOffset();
+}
+
 void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
-  mFrameMetrics.ScrollBy(aOffset);
+  SetScrollOffset(mFrameMetrics.GetScrollOffset() + aOffset);
 }
 
 void AsyncPanZoomController::ScrollByAndClamp(const CSSPoint& aOffset) {
-  mFrameMetrics.ClampAndSetScrollOffset(mFrameMetrics.GetScrollOffset() + aOffset);
+  ClampAndSetScrollOffset(mFrameMetrics.GetScrollOffset() + aOffset);
+}
+
+void AsyncPanZoomController::CopyScrollInfoFrom(const FrameMetrics& aFrameMetrics) {
+  mFrameMetrics.CopyScrollInfoFrom(aFrameMetrics);
+  mFrameMetrics.RecalculateViewportOffset();
 }
 
 void AsyncPanZoomController::ScaleWithFocus(float aScale,
                                             const CSSPoint& aFocus) {
   mFrameMetrics.ZoomBy(aScale);
   // We want to adjust the scroll offset such that the CSS point represented by aFocus remains
   // at the same position on the screen before and after the change in zoom. The below code
   // accomplishes this; see https://bugzilla.mozilla.org/show_bug.cgi?id=923431#c6 for an
   // in-depth explanation of how.
-  mFrameMetrics.SetScrollOffset((mFrameMetrics.GetScrollOffset() + aFocus) - (aFocus / aScale));
+  SetScrollOffset((mFrameMetrics.GetScrollOffset() + aFocus) - (aFocus / aScale));
 }
 
 /**
  * Enlarges the displayport along both axes based on the velocity.
  */
 static CSSSize
 CalculateDisplayPortSize(const CSSSize& aCompositionSize,
                          const CSSPoint& aVelocity)
@@ -3453,19 +3472,18 @@ const ScreenMargin AsyncPanZoomControlle
                       displayPortSize.width, displayPortSize.height);
 
   // Offset the displayport, depending on how fast we're moving and the
   // estimated time it takes to paint, to try to minimise checkerboarding.
   float paintFactor = kDefaultEstimatedPaintDurationMs;
   displayPort.MoveBy(velocity * paintFactor * gfxPrefs::APZVelocityBias());
 
   APZC_LOG_FM(aFrameMetrics,
-    "Calculated displayport as (%f %f %f %f) from velocity %s paint time %f metrics",
-    displayPort.x, displayPort.y, displayPort.Width(), displayPort.Height(),
-    ToString(aVelocity).c_str(), paintFactor);
+    "Calculated displayport as %s from velocity %s paint time %f metrics",
+    Stringify(displayPort).c_str(), ToString(aVelocity).c_str(), paintFactor);
 
   CSSMargin cssMargins;
   cssMargins.left = -displayPort.X();
   cssMargins.top = -displayPort.Y();
   cssMargins.right = displayPort.Width() - compositionSize.width - cssMargins.left;
   cssMargins.bottom = displayPort.Height() - compositionSize.height - cssMargins.top;
 
   return cssMargins * aFrameMetrics.DisplayportPixelsPerCSSPixel();
@@ -3605,16 +3623,20 @@ AsyncPanZoomController::RequestContentRe
       fabsf(mLastPaintRequestMetrics.GetScrollOffset().y -
             aFrameMetrics.GetScrollOffset().y) < EPSILON &&
       aFrameMetrics.GetPresShellResolution() == mLastPaintRequestMetrics.GetPresShellResolution() &&
       aFrameMetrics.GetZoom() == mLastPaintRequestMetrics.GetZoom() &&
       fabsf(aFrameMetrics.GetViewport().Width() -
             mLastPaintRequestMetrics.GetViewport().Width()) < EPSILON &&
       fabsf(aFrameMetrics.GetViewport().Height() -
             mLastPaintRequestMetrics.GetViewport().Height()) < EPSILON &&
+      fabsf(aFrameMetrics.GetViewport().X() -
+            mLastPaintRequestMetrics.GetViewport().X()) < EPSILON &&
+      fabsf(aFrameMetrics.GetViewport().Y() -
+            mLastPaintRequestMetrics.GetViewport().Y()) < EPSILON &&
       aFrameMetrics.GetScrollGeneration() ==
             mLastPaintRequestMetrics.GetScrollGeneration() &&
       aFrameMetrics.GetScrollUpdateType() ==
             mLastPaintRequestMetrics.GetScrollUpdateType()) {
     return;
   }
 
   APZC_LOG_FM(aFrameMetrics, "%p requesting content repaint", this);
@@ -4063,30 +4085,16 @@ void AsyncPanZoomController::NotifyLayer
       if (!aLayerMetrics.GetCriticalDisplayPort().IsEmpty()) {
         mCheckerboardEvent->UpdateRendertraceProperty(
             CheckerboardEvent::PaintedCriticalDisplayPort,
             aLayerMetrics.GetCriticalDisplayPort() + aLayerMetrics.GetScrollOffset());
       }
     }
   }
 
-  bool needContentRepaint = false;
-  bool viewportUpdated = false;
-  if (FuzzyEqualsAdditive(aLayerMetrics.GetCompositionBounds().Width(), mFrameMetrics.GetCompositionBounds().Width()) &&
-      FuzzyEqualsAdditive(aLayerMetrics.GetCompositionBounds().Height(), mFrameMetrics.GetCompositionBounds().Height())) {
-    // Remote content has sync'd up to the composition geometry
-    // change, so we can accept the viewport it's calculated.
-    if (mFrameMetrics.GetViewport().Width() != aLayerMetrics.GetViewport().Width() ||
-        mFrameMetrics.GetViewport().Height() != aLayerMetrics.GetViewport().Height()) {
-      needContentRepaint = true;
-      viewportUpdated = true;
-    }
-    mFrameMetrics.SetViewport(aLayerMetrics.GetViewport());
-  }
-
   // If the layers update was not triggered by our own repaint request, then
   // we want to take the new scroll offset. Check the scroll generation as well
   // to filter duplicate calls to NotifyLayersUpdated with the same scroll offset
   // update message.
   bool scrollOffsetUpdated = aLayerMetrics.GetScrollOffsetUpdated()
         && (aLayerMetrics.GetScrollGeneration() != mFrameMetrics.GetScrollGeneration());
 
   if (scrollOffsetUpdated && userScrolled &&
@@ -4096,16 +4104,32 @@ void AsyncPanZoomController::NotifyLayer
   }
 
   bool smoothScrollRequested = aLayerMetrics.GetDoSmoothScroll()
        && (aLayerMetrics.GetScrollGeneration() != mFrameMetrics.GetScrollGeneration());
 
   // TODO if we're in a drag and scrollOffsetUpdated is set then we want to
   // ignore it
 
+  bool needContentRepaint = false;
+  bool viewportUpdated = false;
+  if (FuzzyEqualsAdditive(aLayerMetrics.GetCompositionBounds().Width(), mFrameMetrics.GetCompositionBounds().Width()) &&
+      FuzzyEqualsAdditive(aLayerMetrics.GetCompositionBounds().Height(), mFrameMetrics.GetCompositionBounds().Height())) {
+    // Remote content has sync'd up to the composition geometry
+    // change, so we can accept the viewport it's calculated.
+    if (mFrameMetrics.GetViewport().Width() != aLayerMetrics.GetViewport().Width() ||
+        mFrameMetrics.GetViewport().Height() != aLayerMetrics.GetViewport().Height()) {
+      needContentRepaint = true;
+      viewportUpdated = true;
+    }
+    if (viewportUpdated || scrollOffsetUpdated) {
+      mFrameMetrics.SetViewport(aLayerMetrics.GetViewport());
+    }
+  }
+
 #if defined(MOZ_WIDGET_ANDROID)
   if (aLayerMetrics.IsRootContent()) {
     if (APZCTreeManager* manager = GetApzcTreeManager()) {
       AndroidDynamicToolbarAnimator* animator = manager->GetAndroidDynamicToolbarAnimator();
       MOZ_ASSERT(animator);
       animator->MaybeUpdateCompositionSizeAndRootFrameMetrics(aLayerMetrics);
     }
   }
@@ -4199,17 +4223,17 @@ void AsyncPanZoomController::NotifyLayer
 
       // Send an acknowledgement with the new scroll generation so that any
       // repaint requests later in this function go through.
       // Because of the scroll generation update, any inflight paint requests are
       // going to be ignored by layout, and so mExpectedGeckoMetrics
       // becomes incorrect for the purposes of calculating the LD transform. To
       // correct this we need to update mExpectedGeckoMetrics to be the
       // last thing we know was painted by Gecko.
-      mFrameMetrics.CopyScrollInfoFrom(aLayerMetrics);
+      CopyScrollInfoFrom(aLayerMetrics);
       mCompositedLayoutViewport = mFrameMetrics.GetViewport();
       mCompositedScrollOffset = mFrameMetrics.GetScrollOffset();
       mExpectedGeckoMetrics = aLayerMetrics;
 
       // Cancel the animation (which might also trigger a repaint request)
       // after we update the scroll offset above. Otherwise we can be left
       // in a state where things are out of sync.
       CancelAnimation();
@@ -4224,17 +4248,17 @@ void AsyncPanZoomController::NotifyLayer
       needContentRepaint = true;
       // Since the main-thread scroll offset changed we should trigger a
       // recomposite to make sure it becomes user-visible.
       ScheduleComposite();
     } else if (scrollableRectChanged) {
       // Even if we didn't accept a new scroll offset from content, the
       // scrollable rect may have changed in a way that makes our local
       // scroll offset out of bounds, so re-clamp it.
-      mFrameMetrics.ClampAndSetScrollOffset(mFrameMetrics.GetScrollOffset());
+      ClampAndSetScrollOffset(mFrameMetrics.GetScrollOffset());
     }
   }
 
   if (smoothScrollRequested) {
     // A smooth scroll has been requested for animation on the compositor
     // thread.  This flag will be reset by the main thread when it receives
     // the scroll update acknowledgement.
 
@@ -4402,18 +4426,20 @@ void AsyncPanZoomController::ZoomToRect(
     if (aRect.Y() + sizeAfterZoom.height > cssPageRect.Height()) {
       aRect.MoveToY(std::max(0.f, cssPageRect.Height() - sizeAfterZoom.height));
     }
     if (aRect.X() + sizeAfterZoom.width > cssPageRect.Width()) {
       aRect.MoveToX(std::max(0.f, cssPageRect.Width() - sizeAfterZoom.width));
     }
 
     endZoomToMetrics.SetScrollOffset(aRect.TopLeft());
+    endZoomToMetrics.RecalculateViewportOffset();
 
     StartAnimation(new ZoomAnimation(
+        *this,
         mFrameMetrics.GetScrollOffset(),
         mFrameMetrics.GetZoom(),
         endZoomToMetrics.GetScrollOffset(),
         endZoomToMetrics.GetZoom()));
 
     // Schedule a repaint now, so the new displayport will be painted before the
     // animation finishes.
     ParentLayerPoint velocity(0, 0);
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -661,29 +661,51 @@ protected:
    * Helper method to cancel any gesture currently going to Gecko. Used
    * primarily when a user taps the screen over some clickable content but then
    * pans down instead of letting go (i.e. to cancel a previous touch so that a
    * new one can properly take effect.
    */
   nsEventStatus OnCancelTap(const TapGestureInput& aEvent);
 
   /**
+   * The following five methods modify the scroll offset. For the APZC
+   * representing the RCD-RSF, they also recalculate the offset of the layout
+   * viewport.
+   */
+
+  /**
+   * Scroll the scroll frame to an X,Y offset.
+   */
+  void SetScrollOffset(const CSSPoint& aOffset);
+
+  /**
+   * Scroll the scroll frame to an X,Y offset, clamping the resulting scroll
+   * offset to the scroll range.
+   */
+  void ClampAndSetScrollOffset(const CSSPoint& aOffset);
+
+  /**
    * Scroll the scroll frame by an X,Y offset.
    * The resulting scroll offset is not clamped to the scrollable rect;
    * the caller must ensure it stays within range.
    */
   void ScrollBy(const CSSPoint& aOffset);
 
   /**
    * Scroll the scroll frame by an X,Y offset, clamping the resulting
-   * scroll offset to the scrollable rect.
+   * scroll offset to the scroll range.
    */
   void ScrollByAndClamp(const CSSPoint& aOffset);
 
   /**
+   * Copy the scroll offset and scroll generation from |aFrameMetrics|.
+   */
+  void CopyScrollInfoFrom(const FrameMetrics& aFrameMetrics);
+
+  /**
    * Scales the viewport by an amount (note that it multiplies this scale in to
    * the current scale, it doesn't set it to |aScale|). Also considers a focus
    * point so that the page zooms inward/outward from that point.
    */
   void ScaleWithFocus(float aScale,
                       const CSSPoint& aFocus);
 
   /**
@@ -1180,16 +1202,17 @@ private:
   template <typename FlingPhysics> friend class GenericFlingAnimation;
   friend class AndroidFlingPhysics;
   friend class DesktopFlingPhysics;
   friend class OverscrollAnimation;
   friend class SmoothScrollAnimation;
   friend class GenericScrollAnimation;
   friend class WheelScrollAnimation;
   friend class KeyboardScrollAnimation;
+  friend class ZoomAnimation;
 
   friend class GenericOverscrollEffect;
   friend class WidgetOverscrollEffect;
 
   // The initial velocity of the most recent fling.
   ParentLayerPoint mLastFlingVelocity;
   // The time at which the most recent fling started.
   TimeStamp mLastFlingTime;
--- a/gfx/layers/apz/src/GenericFlingAnimation.h
+++ b/gfx/layers/apz/src/GenericFlingAnimation.h
@@ -150,17 +150,17 @@ public:
     // the following AdjustDisplacement calls returns true, but this
     // is already running as part of a FlingAnimation, so we'll be compositing
     // per frame of animation anyway.
     ParentLayerPoint overscroll;
     ParentLayerPoint adjustedOffset;
     mApzc.mX.AdjustDisplacement(offset.x, adjustedOffset.x, overscroll.x);
     mApzc.mY.AdjustDisplacement(offset.y, adjustedOffset.y, overscroll.y);
 
-    aFrameMetrics.ScrollBy(adjustedOffset / aFrameMetrics.GetZoom());
+    mApzc.ScrollBy(adjustedOffset / aFrameMetrics.GetZoom());
 
     // The fling may have caused us to reach the end of our scroll range.
     if (!IsZero(overscroll)) {
       // Hand off the fling to the next APZC in the overscroll handoff chain.
 
       // We may have reached the end of the scroll range along one axis but
       // not the other. In such a case we only want to hand off the relevant
       // component of the fling.
--- a/gfx/layers/apz/src/GenericScrollAnimation.cpp
+++ b/gfx/layers/apz/src/GenericScrollAnimation.cpp
@@ -100,14 +100,14 @@ GenericScrollAnimation::DoSample(FrameMe
   // then end the animation early. Note that the initial displacement could be 0
   // if the compositor ran very quickly (<1ms) after the animation was created.
   // When that happens we want to make sure the animation continues.
   if (!IsZero(displacement) && IsZero(adjustedOffset)) {
     // Nothing more to do - end the animation.
     return false;
   }
 
-  aFrameMetrics.ScrollBy(adjustedOffset / zoom);
+  mApzc.ScrollBy(adjustedOffset / zoom);
   return !finished;
 }
 
 } // namespace layers
 } // namespace mozilla
--- a/gfx/layers/apz/util/APZCCallbackHelper.cpp
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -73,43 +73,47 @@ RecenterDisplayPort(mozilla::layers::Fra
   margins.top = margins.bottom = margins.TopBottom() / 2;
   aFrameMetrics.SetDisplayPortMargins(margins);
 }
 
 static CSSPoint
 ScrollFrameTo(nsIScrollableFrame* aFrame, const FrameMetrics& aMetrics, bool& aSuccessOut)
 {
   aSuccessOut = false;
-  CSSPoint targetScrollPosition = aMetrics.GetScrollOffset();
+  CSSPoint targetScrollPosition = aMetrics.IsRootContent()
+    ? aMetrics.GetViewport().TopLeft()
+    : aMetrics.GetScrollOffset();
 
   if (!aFrame) {
     return targetScrollPosition;
   }
 
   CSSPoint geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
 
   // If the repaint request was triggered due to a previous main-thread scroll
   // offset update sent to the APZ, then we don't need to do another scroll here
   // and we can just return.
   if (!aMetrics.GetScrollOffsetUpdated()) {
     return geckoScrollPosition;
   }
 
-  // If the frame is overflow:hidden on a particular axis, we don't want to allow
-  // user-driven scroll on that axis. Simply set the scroll position on that axis
-  // to whatever it already is. Note that this will leave the APZ's async scroll
-  // position out of sync with the gecko scroll position, but APZ can deal with that
-  // (by design). Note also that when we run into this case, even if both axes
-  // have overflow:hidden, we want to set aSuccessOut to true, so that the displayport
-  // follows the async scroll position rather than the gecko scroll position.
+  // If this is the root content with overflow:hidden, then APZ should not
+  // allow scrolling in such a way that moves the layout viewport.
+  //
+  // If this is overflow:hidden, but not the root content, then
+  // nsLayoutUtils::CalculateScrollableRectForFrame should have sized the
+  // scrollable rect in a way that prevents APZ from scrolling it at all.
+  //
+  // In either case, targetScrollPosition should be the same as
+  // geckoScrollPosition here.
   if (aFrame->GetScrollbarStyles().mVertical == NS_STYLE_OVERFLOW_HIDDEN) {
-    targetScrollPosition.y = geckoScrollPosition.y;
+    MOZ_ASSERT(targetScrollPosition.y == geckoScrollPosition.y);
   }
   if (aFrame->GetScrollbarStyles().mHorizontal == NS_STYLE_OVERFLOW_HIDDEN) {
-    targetScrollPosition.x = geckoScrollPosition.x;
+    MOZ_ASSERT(targetScrollPosition.x == geckoScrollPosition.x);
   }
 
   // If the scrollable frame is currently in the middle of an async or smooth
   // scroll then we don't want to interrupt it (see bug 961280).
   // Also if the scrollable frame got a scroll request from a higher priority origin
   // since the last layers update, then we don't want to push our scroll request
   // because we'll clobber that one, which is bad.
   bool scrollInProgress = APZCCallbackHelper::IsScrollInProgress(aFrame);
@@ -157,16 +161,25 @@ ScrollFrame(nsIContent* aContent,
       if (nsIFrame* frame = aContent->GetPrimaryFrame()) {
         frame->SchedulePaint();
       }
     } else {
       // Correct the display port due to the difference between mScrollOffset and the
       // actual scroll offset.
       APZCCallbackHelper::AdjustDisplayPortForScrollDelta(aMetrics, actualScrollOffset);
     }
+  } else if (aMetrics.IsRootContent() &&
+             aMetrics.GetScrollOffset() != aMetrics.GetViewport().TopLeft()) {
+    // APZ uses the visual viewport's offset to calculate where to place the
+    // display port, so the display port is misplaced when a pinch zoom occurs.
+    //
+    // We need to force a display port adjustment in the following paint to
+    // account for a difference between mScrollOffset and the actual scroll
+    // offset in repaints requested by AsyncPanZoomController::NotifyLayersUpdated.
+    APZCCallbackHelper::AdjustDisplayPortForScrollDelta(aMetrics, actualScrollOffset);
   } else {
     // For whatever reason we couldn't update the scroll offset on the scroll frame,
     // which means the data APZ used for its displayport calculation is stale. Fall
     // back to a sane default behaviour. Note that we don't tile-align the recentered
     // displayport because tile-alignment depends on the scroll position, and the
     // scroll position here is out of our control. See bug 966507 comment 21 for a
     // more detailed explanation.
     RecenterDisplayPort(aMetrics);
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -9128,18 +9128,22 @@ nsLayoutUtils::ComputeScrollMetadata(nsI
   nsIScrollableFrame* scrollableFrame = nullptr;
   if (aScrollFrame)
     scrollableFrame = aScrollFrame->GetScrollTargetFrame();
 
   metrics.SetScrollableRect(CSSRect::FromAppUnits(
     nsLayoutUtils::CalculateScrollableRectForFrame(scrollableFrame, aForFrame)));
 
   if (scrollableFrame) {
-    nsPoint scrollPosition = scrollableFrame->GetScrollPosition();
-    metrics.SetScrollOffset(CSSPoint::FromAppUnits(scrollPosition));
+    CSSPoint scrollPosition = CSSPoint::FromAppUnits(scrollableFrame->GetScrollPosition());
+    metrics.SetScrollOffset(scrollPosition);
+
+    CSSRect viewport = metrics.GetViewport();
+    viewport.MoveTo(scrollPosition);
+    metrics.SetViewport(viewport);
 
     nsPoint smoothScrollPosition = scrollableFrame->LastScrollDestination();
     metrics.SetSmoothScrollOffset(CSSPoint::FromAppUnits(smoothScrollPosition));
 
     // If the frame was scrolled since the last layers update, and by something
     // that is higher priority than APZ, we want to tell the APZ to update
     // its scroll offset. We want to distinguish the case where the scroll offset
     // was "restored" because in that case the restored scroll position should