Bug 1453425 - Add relative scroll offset updates using nsGkAtoms::relative. r=botond
authorRyan Hunt <rhunt@eqrion.net>
Tue, 09 Oct 2018 23:24:28 -0500
changeset 491203 0b8e2732f2a52453c4450cc22dba292f1d2ce8b6
parent 491202 214cc7e7efb60b79fda610e02f4f24c50616ba9d
child 491204 ab584824a073d21864e6aa8d571c8c5624151c73
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersbotond
bugs1453425
milestone65.0a1
Bug 1453425 - Add relative scroll offset updates using nsGkAtoms::relative. r=botond This commit adds a scroll origin, nsGkAtoms::relative, which can be used to mark main thread scrolling that can be combined with a concurrent APZ scroll. The behavior of this is controlled by a pref, apz.relative-update. This pref is initially activated and is intended as an aid to narrowing down causes of regressions for users in bug reports. Relative scroll updates work by tracking the last sent or accepted APZ scroll offset. This is sent along with every FrameMetrics. Additionally, a flag is added to FrameMetrics, mIsRelative, indicating whether the scroll offset can be combined with a potential APZ scroll. When this flag is set, AsyncPanZoomController will apply the delta between the sent base scroll offset, and sent new scroll offset. This flag is controlled by the last scroll origin on nsGfxScrollFrame. The new origin, `relative`, is marked as being able to clobber APZ updates, but can only be set if all scrolls since the last repaint request or layers transaction have been relative. Differential Revision: https://phabricator.services.mozilla.com/D8234
gfx/ipc/GfxMessageUtils.h
gfx/layers/FrameMetrics.h
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/src/AsyncPanZoomController.h
gfx/layers/ipc/LayersMessageUtils.h
gfx/layers/wr/WebRenderScrollData.cpp
gfx/thebes/gfxPrefs.h
layout/base/nsLayoutUtils.cpp
layout/generic/nsGfxScrollFrame.cpp
layout/generic/nsGfxScrollFrame.h
layout/generic/nsIScrollableFrame.h
layout/painting/FrameLayerBuilder.cpp
layout/painting/nsDisplayList.cpp
modules/libpref/init/all.js
xpcom/ds/StaticAtoms.py
--- a/gfx/ipc/GfxMessageUtils.h
+++ b/gfx/ipc/GfxMessageUtils.h
@@ -3,17 +3,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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 __GFXMESSAGEUTILS_H__
 #define __GFXMESSAGEUTILS_H__
 
 #include "FilterSupport.h"
-#include "FrameMetrics.h"
 #include "ImageTypes.h"
 #include "RegionBuilder.h"
 #include "base/process_util.h"
 #include "chrome/common/ipc_message_utils.h"
 #include "gfxFeature.h"
 #include "gfxFallback.h"
 #include "gfxPoint.h"
 #include "gfxRect.h"
--- a/gfx/layers/FrameMetrics.h
+++ b/gfx/layers/FrameMetrics.h
@@ -33,16 +33,18 @@ namespace layers {
 
 /**
  * Helper struct to hold a couple of fields that can be updated as part of
  * an empty transaction.
  */
 struct ScrollUpdateInfo {
   uint32_t mScrollGeneration;
   CSSPoint mScrollOffset;
+  CSSPoint mBaseScrollOffset;
+  bool mIsRelative;
 };
 
 /**
  * The viewport and displayport metrics for the painted frame at the
  * time of a layer-tree transaction.  These metrics are especially
  * useful for shadow layers, because the metrics values are updated
  * atomically with new pixels.
  */
@@ -73,27 +75,29 @@ public:
     , mPresShellResolution(1)
     , mCompositionBounds(0, 0, 0, 0)
     , mDisplayPort(0, 0, 0, 0)
     , mCriticalDisplayPort(0, 0, 0, 0)
     , mScrollableRect(0, 0, 0, 0)
     , mCumulativeResolution()
     , mDevPixelsPerCSSPixel(1)
     , mScrollOffset(0, 0)
+    , mBaseScrollOffset(0, 0)
     , mZoom()
     , mScrollGeneration(0)
     , mSmoothScrollOffset(0, 0)
     , mRootCompositionSize(0, 0)
     , mDisplayPortMargins(0, 0, 0, 0)
     , mPresShellId(-1)
     , mViewport(0, 0, 0, 0)
     , mExtraResolution()
     , mPaintRequestTime()
     , mScrollUpdateType(eNone)
     , mIsRootContent(false)
+    , mIsRelative(false)
     , mDoSmoothScroll(false)
     , mUseDisplayPortMargins(false)
     , mIsScrollInfoLayer(false)
   {
   }
 
   // Default copy ctor and operator= are fine
 
@@ -104,27 +108,29 @@ public:
            mPresShellResolution == aOther.mPresShellResolution &&
            mCompositionBounds.IsEqualEdges(aOther.mCompositionBounds) &&
            mDisplayPort.IsEqualEdges(aOther.mDisplayPort) &&
            mCriticalDisplayPort.IsEqualEdges(aOther.mCriticalDisplayPort) &&
            mScrollableRect.IsEqualEdges(aOther.mScrollableRect) &&
            mCumulativeResolution == aOther.mCumulativeResolution &&
            mDevPixelsPerCSSPixel == aOther.mDevPixelsPerCSSPixel &&
            mScrollOffset == aOther.mScrollOffset &&
+           mBaseScrollOffset == aOther.mBaseScrollOffset &&
            // don't compare mZoom
            mScrollGeneration == aOther.mScrollGeneration &&
            mSmoothScrollOffset == aOther.mSmoothScrollOffset &&
            mRootCompositionSize == aOther.mRootCompositionSize &&
            mDisplayPortMargins == aOther.mDisplayPortMargins &&
            mPresShellId == aOther.mPresShellId &&
            mViewport.IsEqualEdges(aOther.mViewport) &&
            mExtraResolution == aOther.mExtraResolution &&
            mPaintRequestTime == aOther.mPaintRequestTime &&
            mScrollUpdateType == aOther.mScrollUpdateType &&
            mIsRootContent == aOther.mIsRootContent &&
+           mIsRelative == aOther.mIsRelative &&
            mDoSmoothScroll == aOther.mDoSmoothScroll &&
            mUseDisplayPortMargins == aOther.mUseDisplayPortMargins &&
            mIsScrollInfoLayer == aOther.mIsScrollInfoLayer;
   }
 
   bool operator!=(const FrameMetrics& aOther) const
   {
     return !operator==(aOther);
@@ -242,34 +248,63 @@ public:
   }
 
   void ZoomBy(const gfxSize& aScale)
   {
     mZoom.xScale *= aScale.width;
     mZoom.yScale *= aScale.height;
   }
 
-  void CopyScrollInfoFrom(const FrameMetrics& aOther)
+  /*
+   * Compares an APZ frame metrics with an incoming content frame metrics
+   * to see if APZ has a scroll offset that has not been incorporated into
+   * the content frame metrics.
+   */
+  bool HasPendingScroll(const FrameMetrics& aContentFrameMetrics) const
+  {
+    return mScrollOffset != aContentFrameMetrics.mBaseScrollOffset;
+  }
+
+  void ApplyScrollUpdateFrom(const FrameMetrics& aOther)
   {
     mScrollOffset = aOther.mScrollOffset;
     mScrollGeneration = aOther.mScrollGeneration;
   }
 
-  void CopySmoothScrollInfoFrom(const FrameMetrics& aOther)
+  void ApplySmoothScrollUpdateFrom(const FrameMetrics& aOther)
   {
     mSmoothScrollOffset = aOther.mSmoothScrollOffset;
     mScrollGeneration = aOther.mScrollGeneration;
     mDoSmoothScroll = aOther.mDoSmoothScroll;
   }
 
+  void ApplyRelativeScrollUpdateFrom(const FrameMetrics& aOther)
+  {
+    MOZ_ASSERT(aOther.IsRelative());
+    CSSPoint delta = (aOther.mScrollOffset - aOther.mBaseScrollOffset);
+    ClampAndSetScrollOffset(mScrollOffset + delta);
+    mScrollGeneration = aOther.mScrollGeneration;
+  }
+
+  void ApplyRelativeSmoothScrollUpdateFrom(const FrameMetrics& aOther)
+  {
+    MOZ_ASSERT(aOther.IsRelative());
+    CSSPoint delta = (aOther.mSmoothScrollOffset - aOther.mBaseScrollOffset);
+    ClampAndSetSmoothScrollOffset(mScrollOffset + delta);
+    mScrollGeneration = aOther.mScrollGeneration;
+    mDoSmoothScroll = aOther.mDoSmoothScroll;
+  }
+
   void UpdatePendingScrollInfo(const ScrollUpdateInfo& aInfo)
   {
     mScrollOffset = aInfo.mScrollOffset;
+    mBaseScrollOffset = aInfo.mBaseScrollOffset;
     mScrollGeneration = aInfo.mScrollGeneration;
     mScrollUpdateType = ePending;
+    mIsRelative = aInfo.mIsRelative;
   }
 
 public:
   void SetPresShellResolution(float aPresShellResolution)
   {
     mPresShellResolution = aPresShellResolution;
   }
 
@@ -338,57 +373,70 @@ public:
     return mIsRootContent;
   }
 
   void SetScrollOffset(const CSSPoint& aScrollOffset)
   {
     mScrollOffset = aScrollOffset;
   }
 
+  void SetBaseScrollOffset(const CSSPoint& aScrollOffset)
+  {
+    mBaseScrollOffset = aScrollOffset;
+  }
+
   // Set scroll offset, first clamping to the scroll range.
   void ClampAndSetScrollOffset(const CSSPoint& aScrollOffset)
   {
     SetScrollOffset(CalculateScrollRange().ClampPoint(aScrollOffset));
   }
 
   const CSSPoint& GetScrollOffset() const
   {
     return mScrollOffset;
   }
 
+  const CSSPoint& GetBaseScrollOffset() const
+  {
+    return mBaseScrollOffset;
+  }
+
   void SetSmoothScrollOffset(const CSSPoint& aSmoothScrollDestination)
   {
     mSmoothScrollOffset = aSmoothScrollDestination;
   }
 
+  void ClampAndSetSmoothScrollOffset(const CSSPoint& aSmoothScrollOffset)
+  {
+    SetSmoothScrollOffset(CalculateScrollRange().ClampPoint(aSmoothScrollOffset));
+  }
+
   const CSSPoint& GetSmoothScrollOffset() const
   {
     return mSmoothScrollOffset;
   }
 
   void SetZoom(const CSSToParentLayerScale2D& aZoom)
   {
     mZoom = aZoom;
   }
 
   const CSSToParentLayerScale2D& GetZoom() const
   {
     return mZoom;
   }
 
-  void SetScrollOffsetUpdated(uint32_t aScrollGeneration)
+  void SetScrollGeneration(uint32_t aScrollGeneration)
   {
-    mScrollUpdateType = eMainThread;
     mScrollGeneration = aScrollGeneration;
   }
 
-  void SetScrollOffsetRestored(uint32_t aScrollGeneration)
+  void SetScrollOffsetUpdateType(ScrollOffsetUpdateType aScrollUpdateType)
   {
-    mScrollUpdateType = eRestore;
-    mScrollGeneration = aScrollGeneration;
+    mScrollUpdateType = aScrollUpdateType;
   }
 
   void SetSmoothScrollOffsetUpdated(int32_t aScrollGeneration)
   {
     mDoSmoothScroll = true;
     mScrollGeneration = aScrollGeneration;
   }
 
@@ -397,16 +445,26 @@ public:
     return mScrollUpdateType;
   }
 
   bool GetScrollOffsetUpdated() const
   {
     return mScrollUpdateType != eNone;
   }
 
+  void SetIsRelative(bool aIsRelative)
+  {
+    mIsRelative = aIsRelative;
+  }
+
+  bool IsRelative() const
+  {
+    return mIsRelative;
+  }
+
   bool GetDoSmoothScroll() const
   {
     return mDoSmoothScroll;
   }
 
   uint32_t GetScrollGeneration() const
   {
     return mScrollGeneration;
@@ -637,16 +695,20 @@ private:
   //   width = mCompositionBounds.x / mResolution.scale,
   //   height = mCompositionBounds.y / mResolution.scale }
   // Be within |mScrollableRect|.
   //
   // This is valid for any layer, but is always relative to this frame and
   // not any parents, regardless of parent transforms.
   CSSPoint mScrollOffset;
 
+  // The base scroll offset to use for calculating a relative update to a
+  // scroll offset.
+  CSSPoint mBaseScrollOffset;
+
   // The "user zoom". Content is painted by gecko at mResolution * mDevPixelsPerCSSPixel,
   // but will be drawn to the screen at mZoom. In the steady state, the
   // two will be the same, but during an async zoom action the two may
   // diverge. This information is initialized in Gecko but updated in the APZC.
   CSSToParentLayerScale2D mZoom;
 
   // The scroll generation counter used to acknowledge the scroll offset update.
   uint32_t mScrollGeneration;
@@ -684,16 +746,20 @@ private:
 
   // Whether mScrollOffset was updated by something other than the APZ code, and
   // if the APZC receiving this metrics should update its local copy.
   ScrollOffsetUpdateType mScrollUpdateType;
 
   // Whether or not this is the root scroll frame for the root content document.
   bool mIsRootContent:1;
 
+  // When mIsRelative, the scroll offset was updated using a relative API,
+  // such as `ScrollBy`, and can combined with an async scroll.
+  bool mIsRelative:1;
+
   // When mDoSmoothScroll, the scroll offset should be animated to
   // smoothly transition to mScrollOffset rather than be updated instantly.
   bool mDoSmoothScroll:1;
 
   // If this is true then we use the display port margins on this metrics,
   // otherwise use the display port rect.
   bool mUseDisplayPortMargins:1;
 
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -486,16 +486,21 @@ typedef PlatformSpecificStateBase Platfo
  * This controls how long the zoom-to-rect animation takes.\n
  * Units: ms
  *
  * \li\b apz.scale_repaint_delay_ms
  * How long to delay between repaint requests during a scale.
  * A negative number prevents repaint requests during a scale.\n
  * Units: ms
  *
+ * \li\b apz.relative-update.enabled
+ * Whether to enable relative scroll offset updates or not. Relative scroll
+ * offset updates allow APZ and the main thread to concurrently update
+ * the scroll offset and merge the result.
+ *
  */
 
 /**
  * Computed time function used for sampling frames of a zoom to animation.
  */
 StaticAutoPtr<ComputedTimingFunction> gZoomAnimationFunction;
 
 /**
@@ -3373,21 +3378,16 @@ void AsyncPanZoomController::ClampAndSet
 void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
   SetScrollOffset(Metrics().GetScrollOffset() + aOffset);
 }
 
 void AsyncPanZoomController::ScrollByAndClamp(const CSSPoint& aOffset) {
   ClampAndSetScrollOffset(Metrics().GetScrollOffset() + aOffset);
 }
 
-void AsyncPanZoomController::CopyScrollInfoFrom(const FrameMetrics& aFrameMetrics) {
-  Metrics().CopyScrollInfoFrom(aFrameMetrics);
-  Metrics().RecalculateViewportOffset();
-}
-
 void AsyncPanZoomController::ScaleWithFocus(float aScale,
                                             const CSSPoint& aFocus) {
   Metrics().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.
   SetScrollOffset((Metrics().GetScrollOffset() + aFocus) - (aFocus / aScale));
@@ -4251,16 +4251,17 @@ void AsyncPanZoomController::NotifyLayer
 
   bool smoothScrollRequested = aLayerMetrics.GetDoSmoothScroll()
        && (aLayerMetrics.GetScrollGeneration() != Metrics().GetScrollGeneration());
 
   // TODO if we're in a drag and scrollOffsetUpdated is set then we want to
   // ignore it
 
   bool needContentRepaint = false;
+  bool userAction = false;
   bool viewportUpdated = false;
 
   // We usually don't entertain viewport updates on the same transaction as
   // a composition bounds update, but we make an exception for Android
   // to avoid the composition bounds and the viewport diverging during
   // orientation changes and dynamic toolbar transitions.
   // TODO: Do this on all platforms.
   bool entertainViewportUpdates =
@@ -4367,28 +4368,47 @@ void AsyncPanZoomController::NotifyLayer
                       aScrollMetadata.IsAutoDirRootContentRTL());
     mScrollMetadata.SetUsesContainerScrolling(aScrollMetadata.UsesContainerScrolling());
     Metrics().SetIsScrollInfoLayer(aLayerMetrics.IsScrollInfoLayer());
     mScrollMetadata.SetForceDisableApz(aScrollMetadata.IsApzForceDisabled());
     mScrollMetadata.SetDisregardedDirection(aScrollMetadata.GetDisregardedDirection());
     mScrollMetadata.SetOverscrollBehavior(aScrollMetadata.GetOverscrollBehavior());
 
     if (scrollOffsetUpdated) {
-      APZC_LOG("%p updating scroll offset from %s to %s\n", this,
-        ToString(Metrics().GetScrollOffset()).c_str(),
-        ToString(aLayerMetrics.GetScrollOffset()).c_str());
-
       // 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.
-      CopyScrollInfoFrom(aLayerMetrics);
+      if (gfxPrefs::APZRelativeUpdate() && aLayerMetrics.IsRelative()) {
+        APZC_LOG("%p relative updating scroll offset from %s by %s\n", this,
+          ToString(Metrics().GetScrollOffset()).c_str(),
+          ToString(aLayerMetrics.GetScrollOffset() - aLayerMetrics.GetBaseScrollOffset()).c_str());
+
+        // It's possible that the main thread has ignored an APZ scroll offset
+        // update for the pending relative scroll that we have just received.
+        // When this happens, we need to send a new scroll offset update with
+        // the combined scroll offset or else the main thread may have an
+        // incorrect scroll offset for a period of time.
+        if (Metrics().HasPendingScroll(aLayerMetrics)) {
+          needContentRepaint = true;
+          userAction = true;
+        }
+
+        Metrics().ApplyRelativeScrollUpdateFrom(aLayerMetrics);
+      } else {
+        APZC_LOG("%p updating scroll offset from %s to %s\n", this,
+          ToString(Metrics().GetScrollOffset()).c_str(),
+          ToString(aLayerMetrics.GetScrollOffset()).c_str());
+        Metrics().ApplyScrollUpdateFrom(aLayerMetrics);
+      }
+      Metrics().RecalculateViewportOffset();
+
       mCompositedLayoutViewport = Metrics().GetViewport();
       mCompositedScrollOffset = Metrics().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();
@@ -4419,17 +4439,21 @@ void AsyncPanZoomController::NotifyLayer
 
     APZC_LOG("%p smooth scrolling from %s to %s in state %d\n", this,
       Stringify(Metrics().GetScrollOffset()).c_str(),
       Stringify(aLayerMetrics.GetSmoothScrollOffset()).c_str(),
       mState);
 
     // See comment on the similar code in the |if (scrollOffsetUpdated)| block
     // above.
-    Metrics().CopySmoothScrollInfoFrom(aLayerMetrics);
+    if (gfxPrefs::APZRelativeUpdate() && aLayerMetrics.IsRelative()) {
+      Metrics().ApplyRelativeSmoothScrollUpdateFrom(aLayerMetrics);
+    } else {
+      Metrics().ApplySmoothScrollUpdateFrom(aLayerMetrics);
+    }
     needContentRepaint = true;
     mExpectedGeckoMetrics = aLayerMetrics;
 
     SmoothScrollTo(Metrics().GetSmoothScrollOffset());
   }
 
   if (viewportUpdated) {
     // While we want to accept the main thread's layout viewport _size_,
@@ -4438,17 +4462,21 @@ void AsyncPanZoomController::NotifyLayer
     // visual viewport.
     // Note: it's important to do this _after_ we've accepted any
     // updated composition bounds.
     Metrics().RecalculateViewportOffset();
   }
 
   if (needContentRepaint) {
     // This repaint request is not driven by a user action on the APZ side
-    RequestContentRepaint(RepaintUpdateType::eNone);
+    RepaintUpdateType updateType = RepaintUpdateType::eNone;
+    if (userAction) {
+      updateType = RepaintUpdateType::eUserAction;
+    }
+    RequestContentRepaint(updateType);
   }
   UpdateSharedCompositorFrameMetrics();
 }
 
 FrameMetrics& AsyncPanZoomController::Metrics() {
   return mScrollMetadata.GetMetrics();
 }
 
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -705,21 +705,16 @@ protected:
 
   /**
    * Scroll the scroll frame by an X,Y offset, clamping the resulting
    * 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);
 
   /**
--- a/gfx/layers/ipc/LayersMessageUtils.h
+++ b/gfx/layers/ipc/LayersMessageUtils.h
@@ -169,54 +169,58 @@ struct ParamTraits<mozilla::layers::Fram
     WriteParam(aMsg, aParam.mPresShellResolution);
     WriteParam(aMsg, aParam.mCompositionBounds);
     WriteParam(aMsg, aParam.mDisplayPort);
     WriteParam(aMsg, aParam.mCriticalDisplayPort);
     WriteParam(aMsg, aParam.mScrollableRect);
     WriteParam(aMsg, aParam.mCumulativeResolution);
     WriteParam(aMsg, aParam.mDevPixelsPerCSSPixel);
     WriteParam(aMsg, aParam.mScrollOffset);
+    WriteParam(aMsg, aParam.mBaseScrollOffset);
     WriteParam(aMsg, aParam.mZoom);
     WriteParam(aMsg, aParam.mScrollGeneration);
     WriteParam(aMsg, aParam.mSmoothScrollOffset);
     WriteParam(aMsg, aParam.mRootCompositionSize);
     WriteParam(aMsg, aParam.mDisplayPortMargins);
     WriteParam(aMsg, aParam.mPresShellId);
     WriteParam(aMsg, aParam.mViewport);
     WriteParam(aMsg, aParam.mExtraResolution);
     WriteParam(aMsg, aParam.mPaintRequestTime);
     WriteParam(aMsg, aParam.mScrollUpdateType);
     WriteParam(aMsg, aParam.mIsRootContent);
+    WriteParam(aMsg, aParam.mIsRelative);
     WriteParam(aMsg, aParam.mDoSmoothScroll);
     WriteParam(aMsg, aParam.mUseDisplayPortMargins);
     WriteParam(aMsg, aParam.mIsScrollInfoLayer);
   }
 
   static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
   {
     return (ReadParam(aMsg, aIter, &aResult->mScrollId) &&
             ReadParam(aMsg, aIter, &aResult->mPresShellResolution) &&
             ReadParam(aMsg, aIter, &aResult->mCompositionBounds) &&
             ReadParam(aMsg, aIter, &aResult->mDisplayPort) &&
             ReadParam(aMsg, aIter, &aResult->mCriticalDisplayPort) &&
             ReadParam(aMsg, aIter, &aResult->mScrollableRect) &&
             ReadParam(aMsg, aIter, &aResult->mCumulativeResolution) &&
             ReadParam(aMsg, aIter, &aResult->mDevPixelsPerCSSPixel) &&
             ReadParam(aMsg, aIter, &aResult->mScrollOffset) &&
+            ReadParam(aMsg, aIter, &aResult->mBaseScrollOffset) &&
             ReadParam(aMsg, aIter, &aResult->mZoom) &&
             ReadParam(aMsg, aIter, &aResult->mScrollGeneration) &&
             ReadParam(aMsg, aIter, &aResult->mSmoothScrollOffset) &&
             ReadParam(aMsg, aIter, &aResult->mRootCompositionSize) &&
             ReadParam(aMsg, aIter, &aResult->mDisplayPortMargins) &&
             ReadParam(aMsg, aIter, &aResult->mPresShellId) &&
             ReadParam(aMsg, aIter, &aResult->mViewport) &&
             ReadParam(aMsg, aIter, &aResult->mExtraResolution) &&
             ReadParam(aMsg, aIter, &aResult->mPaintRequestTime) &&
             ReadParam(aMsg, aIter, &aResult->mScrollUpdateType) &&
             ReadBoolForBitfield(aMsg, aIter, aResult, &paramType::SetIsRootContent) &&
+            ReadBoolForBitfield(aMsg, aIter, aResult, &paramType::SetIsRelative) &&
             ReadBoolForBitfield(aMsg, aIter, aResult, &paramType::SetDoSmoothScroll) &&
             ReadBoolForBitfield(aMsg, aIter, aResult, &paramType::SetUseDisplayPortMargins) &&
             ReadBoolForBitfield(aMsg, aIter, aResult, &paramType::SetIsScrollInfoLayer));
   }
 };
 
 template <>
 struct ParamTraits<mozilla::layers::RepaintRequest>
--- a/gfx/layers/wr/WebRenderScrollData.cpp
+++ b/gfx/layers/wr/WebRenderScrollData.cpp
@@ -63,16 +63,17 @@ WebRenderLayerScrollData::Initialize(Web
     MOZ_ASSERT(aOwner.GetManager());
     FrameMetrics::ViewID scrollId = asr->GetViewId();
     if (Maybe<size_t> index = aOwner.HasMetadataFor(scrollId)) {
       mScrollIds.AppendElement(index.ref());
     } else {
       Maybe<ScrollMetadata> metadata = asr->mScrollableFrame->ComputeScrollMetadata(
           aOwner.GetManager(), aItem->ReferenceFrame(),
           Nothing(), nullptr);
+      asr->mScrollableFrame->NotifyApzTransaction();
       MOZ_ASSERT(metadata);
       MOZ_ASSERT(metadata->GetMetrics().GetScrollId() == scrollId);
       mScrollIds.AppendElement(aOwner.AddMetadata(metadata.ref()));
     }
     asr = asr->mParent;
   }
 
   // aAncestorTransform, if present, is the transform from an ancestor
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -360,16 +360,17 @@ private:
   DECL_GFX_PREF(Live, "apz.x_skate_highmem_adjust",            APZXSkateHighMemAdjust, float, 0.0f);
   DECL_GFX_PREF(Live, "apz.x_skate_size_multiplier",           APZXSkateSizeMultiplier, float, 1.5f);
   DECL_GFX_PREF(Live, "apz.x_stationary_size_multiplier",      APZXStationarySizeMultiplier, float, 3.0f);
   DECL_GFX_PREF(Live, "apz.y_skate_highmem_adjust",            APZYSkateHighMemAdjust, float, 0.0f);
   DECL_GFX_PREF(Live, "apz.y_skate_size_multiplier",           APZYSkateSizeMultiplier, float, 2.5f);
   DECL_GFX_PREF(Live, "apz.y_stationary_size_multiplier",      APZYStationarySizeMultiplier, float, 3.5f);
   DECL_GFX_PREF(Live, "apz.zoom_animation_duration_ms",        APZZoomAnimationDuration, int32_t, 250);
   DECL_GFX_PREF(Live, "apz.scale_repaint_delay_ms",            APZScaleRepaintDelay, int32_t, 500);
+  DECL_GFX_PREF(Live, "apz.relative-update.enabled",           APZRelativeUpdate, bool, false);
 
   DECL_GFX_PREF(Live, "browser.ui.scroll-toolbar-threshold",   ToolbarScrollThreshold, int32_t, 10);
   DECL_GFX_PREF(Live, "browser.ui.zoom.force-user-scalable",   ForceUserScalable, bool, false);
   DECL_GFX_PREF(Live, "browser.viewport.desktopWidth",         DesktopViewportWidth, int32_t, 980);
 
   DECL_GFX_PREF(Live, "dom.ipc.plugins.asyncdrawing.enabled",  PluginAsyncDrawingEnabled, bool, false);
   DECL_GFX_PREF(Live, "dom.meta-viewport.enabled",             MetaViewportEnabled, bool, false);
   DECL_GFX_PREF(Live, "dom.visualviewport.enabled",            VisualViewportEnabled, bool, false);
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -9224,36 +9224,43 @@ nsLayoutUtils::ComputeScrollMetadata(nsI
   if (aScrollFrame)
     scrollableFrame = aScrollFrame->GetScrollTargetFrame();
 
   metrics.SetScrollableRect(CSSRect::FromAppUnits(
     nsLayoutUtils::CalculateScrollableRectForFrame(scrollableFrame, aForFrame)));
 
   if (scrollableFrame) {
     CSSPoint scrollPosition = CSSPoint::FromAppUnits(scrollableFrame->GetScrollPosition());
+    CSSPoint apzScrollPosition = CSSPoint::FromAppUnits(scrollableFrame->GetApzScrollPosition());
     metrics.SetScrollOffset(scrollPosition);
+    metrics.SetBaseScrollOffset(apzScrollPosition);
 
     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
     // not overwrite a user-driven scroll.
-    if (scrollableFrame->LastScrollOrigin() == nsGkAtoms::restore) {
-      metrics.SetScrollOffsetRestored(scrollableFrame->CurrentScrollGeneration());
-    } else if (CanScrollOriginClobberApz(scrollableFrame->LastScrollOrigin())) {
-      metrics.SetScrollOffsetUpdated(scrollableFrame->CurrentScrollGeneration());
-    }
-    scrollableFrame->AllowScrollOriginDowngrade();
+    nsAtom* lastOrigin = scrollableFrame->LastScrollOrigin();
+    if (lastOrigin == nsGkAtoms::restore) {
+      metrics.SetScrollGeneration(scrollableFrame->CurrentScrollGeneration());
+      metrics.SetScrollOffsetUpdateType(FrameMetrics::eRestore);
+    } else if (CanScrollOriginClobberApz(lastOrigin)) {
+      if (lastOrigin == nsGkAtoms::relative) {
+        metrics.SetIsRelative(true);
+      }
+      metrics.SetScrollGeneration(scrollableFrame->CurrentScrollGeneration());
+      metrics.SetScrollOffsetUpdateType(FrameMetrics::eMainThread);
+    }
 
     nsAtom* lastSmoothScrollOrigin = scrollableFrame->LastSmoothScrollOrigin();
     if (lastSmoothScrollOrigin) {
       metrics.SetSmoothScrollOffsetUpdated(scrollableFrame->CurrentScrollGeneration());
     }
 
     nsSize lineScrollAmount = scrollableFrame->GetLineScrollAmount();
     LayoutDeviceIntSize lineScrollAmountInDevPixels =
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -2035,17 +2035,17 @@ ComputeBezierAnimationSettingsForOrigin(
 void
 ScrollFrameHelper::AsyncScroll::InitSmoothScroll(TimeStamp aTime,
                                                  nsPoint aInitialPosition,
                                                  nsPoint aDestination,
                                                  nsAtom *aOrigin,
                                                  const nsRect& aRange,
                                                  const nsSize& aCurrentVelocity)
 {
-  if (!aOrigin || aOrigin == nsGkAtoms::restore) {
+  if (!aOrigin || aOrigin == nsGkAtoms::restore || aOrigin == nsGkAtoms::relative) {
     // We don't have special prefs for "restore", just treat it as "other".
     // "restore" scrolls are (for now) always instant anyway so unless something
     // changes we should never have aOrigin == nsGkAtoms::restore here.
     aOrigin = nsGkAtoms::other;
   }
   // Likewise we should never get APZ-triggered scrolls here, and if that changes
   // something is likely broken somewhere.
   MOZ_ASSERT(aOrigin != nsGkAtoms::apz);
@@ -2117,16 +2117,17 @@ ScrollFrameHelper::ScrollFrameHelper(nsC
   , mAsyncSmoothMSDScroll(nullptr)
   , mLastScrollOrigin(nsGkAtoms::other)
   , mAllowScrollOriginDowngrade(false)
   , mLastSmoothScrollOrigin(nullptr)
   , mScrollGeneration(++sScrollGenerationCounter)
   , mDestination(0, 0)
   , mRestorePos(-1, -1)
   , mLastPos(-1, -1)
+  , mApzScrollPos(0, 0)
   , mScrollPosForLayerPixelAlignment(-1, -1)
   , mLastUpdateFramesPos(-1, -1)
   , mHadDisplayPortAtLastFrameUpdate(false)
   , mDisplayPortAtLastFrameUpdate()
   , mScrollParentID(mozilla::layers::FrameMetrics::NULL_SCROLL_ID)
   , mNeverHasVerticalScrollbar(false)
   , mNeverHasHorizontalScrollbar(false)
   , mHasVerticalScrollbar(false)
@@ -2918,32 +2919,47 @@ ScrollFrameHelper::ScrollToImpl(nsPoint 
   nsRect oldDisplayPort;
   nsIContent* content = mOuter->GetContent();
   nsLayoutUtils::GetHighResolutionDisplayPort(content, &oldDisplayPort);
   oldDisplayPort.MoveBy(-mScrolledFrame->GetPosition());
 
   // Update frame position for scrolling
   mScrolledFrame->SetPosition(mScrollPort.TopLeft() - pt);
 
+  // If this scroll is |relative|, but we've already had a user scroll that
+  // was not relative, promote this origin to |other|. This ensures that we
+  // may only transmit a relative update to APZ if all scrolls since the last
+  // transaction or repaint request have been relative.
+  if (aOrigin == nsGkAtoms::relative &&
+      (mLastScrollOrigin &&
+       mLastScrollOrigin != nsGkAtoms::relative &&
+       mLastScrollOrigin != nsGkAtoms::apz)) {
+    aOrigin = nsGkAtoms::other;
+  }
+
   // If |mLastScrollOrigin| is already set to something that can clobber APZ's
   // scroll offset, then we don't want to change it to something that can't.
   // If we allowed this, then we could end up in a state where APZ ignores
   // legitimate scroll offset updates because the origin has been masked by
   // a later change within the same refresh driver tick.
   bool isScrollOriginDowngrade =
     nsLayoutUtils::CanScrollOriginClobberApz(mLastScrollOrigin) &&
     !nsLayoutUtils::CanScrollOriginClobberApz(aOrigin);
   bool allowScrollOriginChange = mAllowScrollOriginDowngrade ||
     !isScrollOriginDowngrade;
+
   if (allowScrollOriginChange) {
     mLastScrollOrigin = aOrigin;
     mAllowScrollOriginDowngrade = false;
   }
   mLastSmoothScrollOrigin = nullptr;
   mScrollGeneration = ++sScrollGenerationCounter;
+  if (mLastScrollOrigin == nsGkAtoms::apz) {
+    mApzScrollPos = GetScrollPosition();
+  }
 
   // If the new scroll offset is going to clobber APZ's scroll offset, for
   // the RCD-RSF this will have the effect of resetting the visual viewport
   // offset to be the same as the new scroll (= layout viewport) offset.
   // The APZ callback transform, which reflects the difference between these
   // offsets, will subsequently be cleared. However, it we wait for APZ to
   // clear it, the main thread could end up using the old value and get
   // incorrect results, so just clear it now.
@@ -3001,17 +3017,22 @@ ScrollFrameHelper::ScrollToImpl(nsPoint 
             mozilla::layers::FrameMetrics::ViewID id;
             bool success = nsLayoutUtils::FindIDFor(content, &id);
             MOZ_ASSERT(success); // we have a displayport, we better have an ID
 
             // Schedule an empty transaction to carry over the scroll offset update,
             // instead of a full transaction. This empty transaction might still get
             // squashed into a full transaction if something happens to trigger one.
             success = manager->SetPendingScrollUpdateForNextTransaction(id,
-                { mScrollGeneration, CSSPoint::FromAppUnits(GetScrollPosition()) });
+                {
+                  mScrollGeneration,
+                  CSSPoint::FromAppUnits(GetScrollPosition()),
+                  CSSPoint::FromAppUnits(GetApzScrollPosition()),
+                  mLastScrollOrigin == nsGkAtoms::relative
+                });
             if (success) {
               schedulePaint = false;
               mOuter->SchedulePaint(nsIFrame::PAINT_COMPOSITE_ONLY);
               PAINT_SKIP_LOG("Skipping due to APZ-forwarded main-thread scroll\n");
             } else {
               PAINT_SKIP_LOG("Failed to set pending scroll update on layer manager\n");
             }
           }
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -208,16 +208,19 @@ public:
   nsPoint GetLogicalScrollPosition() const {
     nsPoint pt;
     pt.x = IsPhysicalLTR() ?
       mScrollPort.x - mScrolledFrame->GetPosition().x :
       mScrollPort.XMost() - mScrolledFrame->GetRect().XMost();
     pt.y = mScrollPort.y - mScrolledFrame->GetPosition().y;
     return pt;
   }
+  nsPoint GetApzScrollPosition() const {
+    return mApzScrollPos;
+  }
   nsRect GetScrollRange() const;
   // Get the scroll range assuming the viewport has size (aWidth, aHeight).
   nsRect GetScrollRange(nscoord aWidth, nscoord aHeight) const;
   nsSize GetVisualViewportSize() const;
   nsPoint GetVisualViewportOffset() const;
   void ScrollSnap(nsIScrollableFrame::ScrollMode aMode = nsIScrollableFrame::SMOOTH_MSD);
   void ScrollSnap(const nsPoint &aDestination,
                   nsIScrollableFrame::ScrollMode aMode = nsIScrollableFrame::SMOOTH_MSD);
@@ -438,30 +441,33 @@ public:
 
   ScrollSnapInfo GetScrollSnapInfo() const;
 
   bool DecideScrollableLayer(nsDisplayListBuilder* aBuilder,
                              nsRect* aVisibleRect,
                              nsRect* aDirtyRect,
                              bool aSetBase,
                              bool* aDirtyRectHasBeenOverriden = nullptr);
+  void NotifyApzTransaction() {
+    mAllowScrollOriginDowngrade = true;
+    mApzScrollPos = GetScrollPosition();
+  }
   void NotifyApproximateFrameVisibilityUpdate(bool aIgnoreDisplayPort);
   bool GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort);
 
   bool AllowDisplayPortExpiration();
   void TriggerDisplayPortExpiration();
   void ResetDisplayPortExpiryTimer();
 
   void ScheduleSyntheticMouseMove();
   static void ScrollActivityCallback(nsITimer *aTimer, void* anInstance);
 
   void HandleScrollbarStyleSwitching();
 
   nsAtom* LastScrollOrigin() const { return mLastScrollOrigin; }
-  void AllowScrollOriginDowngrade() { mAllowScrollOriginDowngrade = true; }
   nsAtom* LastSmoothScrollOrigin() const { return mLastSmoothScrollOrigin; }
   uint32_t CurrentScrollGeneration() const { return mScrollGeneration; }
   nsPoint LastScrollDestination() const { return mDestination; }
   void ResetScrollInfoIfGeneration(uint32_t aGeneration) {
     if (aGeneration == mScrollGeneration) {
       mLastScrollOrigin = nullptr;
       mLastSmoothScrollOrigin = nullptr;
     }
@@ -545,16 +551,21 @@ public:
   // after every reflow --- because after each time content is loaded/added to the
   // scrollable element, there will be a reflow.
   nsPoint mRestorePos;
   // The last logical position we scrolled to while trying to restore mRestorePos, or
   // 0,0 when this is a new frame. Set to -1,-1 once we've scrolled for any reason
   // other than trying to restore mRestorePos.
   nsPoint mLastPos;
 
+  // The latest scroll position we've sent or received from APZ. This
+  // represents the main thread's best knowledge of the APZ scroll position,
+  // and is used to calculate relative scroll offset updates.
+  nsPoint mApzScrollPos;
+
   nsExpirationState mActivityExpirationState;
 
   nsCOMPtr<nsITimer> mScrollActivityTimer;
   nsPoint mScrollPosForLayerPixelAlignment;
 
   // The scroll position where we last updated frame visibility.
   nsPoint mLastUpdateFramesPos;
   bool mHadDisplayPortAtLastFrameUpdate;
@@ -852,16 +863,19 @@ public:
     return mHelper.GetScrollPortRect();
   }
   virtual nsPoint GetScrollPosition() const override {
     return mHelper.GetScrollPosition();
   }
   virtual nsPoint GetLogicalScrollPosition() const override {
     return mHelper.GetLogicalScrollPosition();
   }
+  virtual nsPoint GetApzScrollPosition() const override {
+    return mHelper.GetApzScrollPosition();
+  }
   virtual nsRect GetScrollRange() const override {
     return mHelper.GetScrollRange();
   }
   virtual nsSize GetVisualViewportSize() const override {
     return mHelper.GetVisualViewportSize();
   }
   virtual nsPoint GetVisualViewportOffset() const override {
     return mHelper.GetVisualViewportOffset();
@@ -968,19 +982,16 @@ public:
     return mHelper.IsRectNearlyVisible(aRect);
   }
   virtual nsRect ExpandRectToNearlyVisible(const nsRect& aRect) const override {
     return mHelper.ExpandRectToNearlyVisible(aRect);
   }
   virtual nsAtom* LastScrollOrigin() override {
     return mHelper.LastScrollOrigin();
   }
-  virtual void AllowScrollOriginDowngrade() override {
-    mHelper.AllowScrollOriginDowngrade();
-  }
   virtual nsAtom* LastSmoothScrollOrigin() override {
     return mHelper.LastSmoothScrollOrigin();
   }
   virtual uint32_t CurrentScrollGeneration() override {
     return mHelper.CurrentScrollGeneration();
   }
   virtual nsPoint LastScrollDestination() override {
     return mHelper.LastScrollDestination();
@@ -1015,16 +1026,19 @@ public:
     return mHelper.UsesContainerScrolling();
   }
   virtual bool DecideScrollableLayer(nsDisplayListBuilder* aBuilder,
                                      nsRect* aVisibleRect,
                                      nsRect* aDirtyRect,
                                      bool aSetBase) override {
     return mHelper.DecideScrollableLayer(aBuilder, aVisibleRect, aDirtyRect, aSetBase);
   }
+  virtual void NotifyApzTransaction() override {
+    mHelper.NotifyApzTransaction();
+  }
   virtual void NotifyApproximateFrameVisibilityUpdate(bool aIgnoreDisplayPort) override {
     mHelper.NotifyApproximateFrameVisibilityUpdate(aIgnoreDisplayPort);
   }
   virtual bool GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort) override {
     return mHelper.GetDisplayPortAtLastApproximateFrameVisibilityUpdate(aDisplayPort);
   }
   void TriggerDisplayPortExpiration() override {
     mHelper.TriggerDisplayPortExpiration();
@@ -1314,16 +1328,19 @@ public:
     return mHelper.GetScrollPortRect();
   }
   virtual nsPoint GetScrollPosition() const override {
     return mHelper.GetScrollPosition();
   }
   virtual nsPoint GetLogicalScrollPosition() const override {
     return mHelper.GetLogicalScrollPosition();
   }
+  virtual nsPoint GetApzScrollPosition() const override {
+    return mHelper.GetApzScrollPosition();
+  }
   virtual nsRect GetScrollRange() const override {
     return mHelper.GetScrollRange();
   }
   virtual nsSize GetVisualViewportSize() const override {
     return mHelper.GetVisualViewportSize();
   }
   virtual nsPoint GetVisualViewportOffset() const override {
     return mHelper.GetVisualViewportOffset();
@@ -1426,19 +1443,16 @@ public:
     return mHelper.IsRectNearlyVisible(aRect);
   }
   virtual nsRect ExpandRectToNearlyVisible(const nsRect& aRect) const override {
     return mHelper.ExpandRectToNearlyVisible(aRect);
   }
   virtual nsAtom* LastScrollOrigin() override {
     return mHelper.LastScrollOrigin();
   }
-  virtual void AllowScrollOriginDowngrade() override {
-    mHelper.AllowScrollOriginDowngrade();
-  }
   virtual nsAtom* LastSmoothScrollOrigin() override {
     return mHelper.LastSmoothScrollOrigin();
   }
   virtual uint32_t CurrentScrollGeneration() override {
     return mHelper.CurrentScrollGeneration();
   }
   virtual nsPoint LastScrollDestination() override {
     return mHelper.LastScrollDestination();
@@ -1548,16 +1562,19 @@ public:
     mHelper.SetHasOutOfFlowContentInsideFilter();
   }
   virtual bool DecideScrollableLayer(nsDisplayListBuilder* aBuilder,
                                      nsRect* aVisibleRect,
                                      nsRect* aDirtyRect,
                                      bool aSetBase) override {
     return mHelper.DecideScrollableLayer(aBuilder, aVisibleRect, aDirtyRect, aSetBase);
   }
+  virtual void NotifyApzTransaction() override {
+    mHelper.NotifyApzTransaction();
+  }
   virtual void NotifyApproximateFrameVisibilityUpdate(bool aIgnoreDisplayPort) override {
     mHelper.NotifyApproximateFrameVisibilityUpdate(aIgnoreDisplayPort);
   }
   virtual bool GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort) override {
     return mHelper.GetDisplayPortAtLastApproximateFrameVisibilityUpdate(aDisplayPort);
   }
   void TriggerDisplayPortExpiration() override {
     mHelper.TriggerDisplayPortExpiration();
--- a/layout/generic/nsIScrollableFrame.h
+++ b/layout/generic/nsIScrollableFrame.h
@@ -134,16 +134,22 @@ public:
    * This will always be a multiple of device pixels.
    */
   virtual nsPoint GetScrollPosition() const = 0;
   /**
    * As GetScrollPosition(), but uses the top-right as origin for RTL frames.
    */
   virtual nsPoint GetLogicalScrollPosition() const = 0;
   /**
+   * Get the latest scroll position that the main thread has sent or received
+   * from APZ.
+   */
+  virtual nsPoint GetApzScrollPosition() const = 0;
+
+  /**
    * Get the area that must contain the scroll position. Typically
    * (but not always, e.g. for RTL content) x and y will be 0, and
    * width or height will be nonzero if the content can be scrolled in
    * that direction. Since scroll positions must be a multiple of
    * device pixels, the range extrema will also be a multiple of
    * device pixels.
    */
   virtual nsRect GetScrollRange() const = 0;
@@ -376,25 +382,16 @@ public:
   virtual nsRect ExpandRectToNearlyVisible(const nsRect& aRect) const = 0;
   /**
    * Returns the origin that triggered the last instant scroll. Will equal
    * nsGkAtoms::apz when the compositor's replica frame metrics includes the
    * latest instant scroll.
    */
   virtual nsAtom* LastScrollOrigin() = 0;
   /**
-   * Sets a flag on the scrollframe that indicates the current scroll origin
-   * has been sent over in a layers transaction, and subsequent changes to
-   * the scroll position by "weaker" origins are permitted to overwrite the
-   * the scroll origin. Scroll origins that nsLayoutUtils::CanScrollOriginClobberApz
-   * returns false for are considered "weaker" than scroll origins for which
-   * that function returns true.
-   */
-  virtual void AllowScrollOriginDowngrade() = 0;
-  /**
    * Returns the origin that triggered the last smooth scroll.
    * Will equal nsGkAtoms::apz when the compositor's replica frame
    * metrics includes the latest smooth scroll.  The compositor will always
    * perform an instant scroll prior to instantiating any smooth scrolls
    * if LastScrollOrigin and LastSmoothScrollOrigin indicate that
    * an instant scroll and a smooth scroll have occurred since the last
    * replication of the frame metrics.
    *
@@ -490,16 +487,32 @@ public:
    * set to true before on the same paint.
    */
   virtual bool DecideScrollableLayer(nsDisplayListBuilder* aBuilder,
                                      nsRect* aVisibleRect,
                                      nsRect* aDirtyRect,
                                      bool aSetBase) = 0;
 
   /**
+   * Notify the scrollframe that the current scroll offset and origin have been
+   * sent over in a layers transaction.
+   *
+   * This sets a flag on the scrollframe that indicates subsequent changes
+   * to the scroll position by "weaker" origins are permitted to overwrite the
+   * the scroll origin. Scroll origins that nsLayoutUtils::CanScrollOriginClobberApz
+   * returns false for are considered "weaker" than scroll origins for which
+   * that function returns true.
+   *
+   * This function must be called for a scrollframe after all calls to
+   * ComputeScrollMetadata in a layers transaction have been completed.
+   *
+   */
+  virtual void NotifyApzTransaction() = 0;
+
+  /**
    * Notification that this scroll frame is getting its frame visibility updated.
    * aIgnoreDisplayPort indicates that the display port was ignored (because
    * there was no suitable base rect)
    */
   virtual void NotifyApproximateFrameVisibilityUpdate(bool aIgnoreDisplayPort) = 0;
 
   /**
    * Returns true if this scroll frame had a display port at the last frame
--- a/layout/painting/FrameLayerBuilder.cpp
+++ b/layout/painting/FrameLayerBuilder.cpp
@@ -6156,16 +6156,17 @@ ContainerState::SetupScrollingMetadata(N
 
     Maybe<ScrollMetadata> metadata;
     if (mCachedScrollMetadata.mASR == asr &&
         mCachedScrollMetadata.mClip == clip) {
       metadata = mCachedScrollMetadata.mMetadata;
     } else {
       metadata = scrollFrame->ComputeScrollMetadata(
         aEntry->mLayer->Manager(), mContainerReferenceFrame, Some(mParameters), clip);
+      scrollFrame->NotifyApzTransaction();
       mCachedScrollMetadata.mASR = asr;
       mCachedScrollMetadata.mClip = clip;
       mCachedScrollMetadata.mMetadata = metadata;
     }
 
     if (!metadata) {
       continue;
     }
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -7224,27 +7224,33 @@ nsDisplaySubDocument::ComputeScrollMetad
     aContainerParameters.mXScale * presShell->GetResolution(),
     aContainerParameters.mYScale * presShell->GetResolution(),
     nsIntPoint(),
     aContainerParameters);
 
   nsRect viewport = mFrame->GetRect() - mFrame->GetPosition() +
                     mFrame->GetOffsetToCrossDoc(ReferenceFrame());
 
-  return MakeUnique<ScrollMetadata>(
+  UniquePtr<ScrollMetadata> metadata = MakeUnique<ScrollMetadata>(
     nsLayoutUtils::ComputeScrollMetadata(mFrame,
                                          rootScrollFrame,
                                          rootScrollFrame->GetContent(),
                                          ReferenceFrame(),
                                          aLayerManager,
                                          mScrollParentId,
                                          viewport,
                                          Nothing(),
                                          isRootContentDocument,
                                          Some(params)));
+  nsIScrollableFrame* scrollableFrame = rootScrollFrame->GetScrollTargetFrame();
+  if (scrollableFrame) {
+    scrollableFrame->NotifyApzTransaction();
+  }
+
+  return metadata;
 }
 
 static bool
 UseDisplayPortForViewport(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame)
 {
   return aBuilder->IsPaintingToWindow() &&
          nsLayoutUtils::ViewportHasDisplayPort(aFrame->PresContext());
 }
@@ -7927,16 +7933,20 @@ nsDisplayScrollInfoLayer::ComputeScrollM
                                          ReferenceFrame(),
                                          aLayerManager,
                                          mScrollParentId,
                                          viewport,
                                          Nothing(),
                                          false,
                                          Some(aContainerParameters));
   metadata.GetMetrics().SetIsScrollInfoLayer(true);
+  nsIScrollableFrame* scrollableFrame = mScrollFrame->GetScrollTargetFrame();
+  if (scrollableFrame) {
+    scrollableFrame->NotifyApzTransaction();
+  }
 
   return UniquePtr<ScrollMetadata>(new ScrollMetadata(metadata));
 }
 
 bool
 nsDisplayScrollInfoLayer::UpdateScrollData(
   mozilla::layers::WebRenderScrollData* aData,
   mozilla::layers::WebRenderLayerScrollData* aLayerData)
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -732,16 +732,19 @@ pref("apz.overscroll.stretch_factor", "0
 pref("apz.paint_skipping.enabled", true);
 // Fetch displayport updates early from the message queue
 pref("apz.peek_messages.enabled", true);
 pref("apz.pinch_lock.mode", 1);
 pref("apz.pinch_lock.scoll_lock_threshold", "0.03125");  // 1/32 inches
 pref("apz.pinch_lock.span_breakout_threshold", "0.03125");  // 1/32 inches
 pref("apz.pinch_lock.span_lock_threshold", "0.03125");  // 1/32 inches
 pref("apz.popups.enabled", false);
+#ifdef NIGHTLY_BUILD
+pref("apz.relative-update.enabled", true);
+#endif
 
 // Whether to print the APZC tree for debugging
 pref("apz.printtree", false);
 
 #ifdef NIGHTLY_BUILD
 pref("apz.record_checkerboarding", true);
 #else
 pref("apz.record_checkerboarding", false);
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -2065,16 +2065,17 @@ STATIC_ATOMS = [
     Atom("pixels",     "pixels"),
     Atom("lines",      "lines"),
     Atom("pages",      "pages"),
     Atom("scrollbars", "scrollbars"),
     # Atom("other",      "other"),  # "other" is present above
     # Scroll origins without smooth-scrolling prefs
     Atom("apz",        "apz"),
     Atom("restore",    "restore"),
+    Atom("relative",    "relative"),
 
     Atom("alert", "alert"),
     Atom("alertdialog", "alertdialog"),
     Atom("application", "application"),
     Atom("aria_colcount", "aria-colcount"),
     Atom("aria_colindex", "aria-colindex"),
     Atom("aria_details", "aria-details"),
     Atom("aria_errormessage", "aria-errormessage"),