Merge mozilla-central to mozilla-inbound. a=merge
authorCosmin Sabou <csabou@mozilla.com>
Mon, 20 Aug 2018 00:52:55 +0300
changeset 487414 b16c258543bf941e8d4264b2fa9c41117e9065c7
parent 487413 16e87606763681d91dc92d6013f3698c36450c9c (current diff)
parent 487402 93d0e291f45883765df384c070e56e4e0517db4b (diff)
child 487415 a2ad987a8861b30f011c4406a7b69494a0af4497
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. a=merge
--- a/dom/media/webaudio/ScriptProcessorNode.cpp
+++ b/dom/media/webaudio/ScriptProcessorNode.cpp
@@ -114,20 +114,44 @@ public:
       MutexAutoLock lock(mOutputQueue.Lock());
       amount += mOutputQueue.SizeOfExcludingThis(aMallocSizeOf);
     }
 
     return amount;
   }
 
   // main thread
+
+  // NotifyNodeIsConnected() may be called even when the state has not
+  // changed.
+  void NotifyNodeIsConnected(bool aIsConnected)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    if (!aIsConnected) {
+      // Reset main thread state for FinishProducingOutputBuffer().
+      mLatency = 0.0f;
+      mLastEventTime = TimeStamp();
+      mDroppingBuffers = false;
+      // Don't flush the output buffer here because the graph thread may be
+      // using it now.  The graph thread will flush when it knows it is
+      // disconnected.
+    }
+    mNodeIsConnected = aIsConnected;
+  }
+
   void FinishProducingOutputBuffer(const AudioChunk& aBuffer)
   {
     MOZ_ASSERT(NS_IsMainThread());
 
+    if (!mNodeIsConnected) {
+      // The output buffer is not used, and mLastEventTime will not be
+      // initialized until the node is re-connected.
+      return;
+    }
+
     TimeStamp now = TimeStamp::Now();
 
     if (mLastEventTime.IsNull()) {
       mLastEventTime = now;
     } else {
       // When main thread blocking has built up enough so
       // |mLatency > MAX_LATENCY_S|, frame dropping starts. It continues until
       // the output buffer is completely empty, at which point the accumulated
@@ -160,16 +184,17 @@ public:
          offset += WEBAUDIO_BLOCK_SIZE) {
       AudioChunk& chunk = mOutputQueue.Produce();
       chunk = aBuffer;
       chunk.SliceTo(offset, offset + WEBAUDIO_BLOCK_SIZE);
     }
   }
 
   // graph thread
+
   AudioChunk GetOutputBuffer()
   {
     MOZ_ASSERT(!NS_IsMainThread());
     AudioChunk buffer;
 
     {
       MutexAutoLock lock(mOutputQueue.Lock());
       if (mOutputQueue.ReadyToConsume() > 0) {
@@ -191,44 +216,46 @@ public:
   }
 
   StreamTime DelaySoFar() const
   {
     MOZ_ASSERT(!NS_IsMainThread());
     return mDelaySoFar == STREAM_TIME_MAX ? 0 : mDelaySoFar;
   }
 
-  void Reset()
+  void Flush()
   {
     MOZ_ASSERT(!NS_IsMainThread());
     mDelaySoFar = STREAM_TIME_MAX;
-    mLatency = 0.0f;
     {
       MutexAutoLock lock(mOutputQueue.Lock());
       mOutputQueue.Clear();
     }
-    mLastEventTime = TimeStamp();
   }
 
 private:
   OutputQueue mOutputQueue;
   // How much delay we've seen so far.  This measures the amount of delay
   // caused by the main thread lagging behind in producing output buffers.
   // STREAM_TIME_MAX means that we have not received our first buffer yet.
+  // Graph thread only.
   StreamTime mDelaySoFar;
   // The samplerate of the context.
-  float mSampleRate;
+  const float mSampleRate;
+  // The remaining members are main thread only.
   // This is the latency caused by the buffering. If this grows too high, we
   // will drop buffers until it is acceptable.
   float mLatency;
   // This is the time at which we last produced a buffer, to detect if the main
   // thread has been blocked.
   TimeStamp mLastEventTime;
   // True if we should be dropping buffers.
   bool mDroppingBuffers;
+  // True iff the AudioNode has at least one input or output connected.
+  bool mNodeIsConnected;
 };
 
 class ScriptProcessorNodeEngine final : public AudioNodeEngine
 {
 public:
   ScriptProcessorNodeEngine(ScriptProcessorNode* aNode,
                             AudioDestinationNode* aDestination,
                             uint32_t aBufferSize,
@@ -268,17 +295,17 @@ public:
                     AudioBlock* aOutput,
                     bool* aFinished) override
   {
     // This node is not connected to anything. Per spec, we don't fire the
     // onaudioprocess event. We also want to clear out the input and output
     // buffer queue, and output a null buffer.
     if (!mIsConnected) {
       aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
-      mSharedBuffers->Reset();
+      mSharedBuffers->Flush();
       mInputWriteIndex = 0;
       return;
     }
 
     // The input buffer is allocated lazily when non-null input is received.
     if (!aInput.IsNull() && !mInputBuffer) {
       mInputBuffer = ThreadSharedFloatArrayBufferList::
         Create(mInputChannelCount, mBufferSize, fallible);
@@ -552,13 +579,16 @@ ScriptProcessorNode::UpdateConnectedStat
   SendInt32ParameterToStream(ScriptProcessorNodeEngine::IS_CONNECTED,
                              isConnected);
 
   if (isConnected && HasListenersFor(nsGkAtoms::onaudioprocess)) {
     MarkActive();
   } else {
     MarkInactive();
   }
+
+  auto engine = static_cast<ScriptProcessorNodeEngine*>(mStream->Engine());
+  engine->GetSharedBuffers()->NotifyNodeIsConnected(isConnected);
 }
 
 } // namespace dom
 } // namespace mozilla
 
--- a/gfx/layers/apz/public/APZSampler.h
+++ b/gfx/layers/apz/public/APZSampler.h
@@ -6,16 +6,17 @@
 
 #ifndef mozilla_layers_APZSampler_h
 #define mozilla_layers_APZSampler_h
 
 #include <unordered_map>
 
 #include "base/platform_thread.h" // for PlatformThreadId
 #include "mozilla/layers/AsyncCompositionManager.h" // for AsyncTransform
+#include "mozilla/layers/APZUtils.h"
 #include "mozilla/StaticMutex.h"
 #include "mozilla/StaticPtr.h"
 #include "nsTArray.h"
 #include "Units.h"
 
 namespace mozilla {
 
 class TimeStamp;
@@ -79,23 +80,31 @@ public:
       const LayerMetricsWrapper& aContent,
       const ScrollbarData& aThumbData,
       bool aScrollbarIsDescendant,
       AsyncTransformComponentMatrix* aOutClipTransform);
 
   CSSRect GetCurrentAsyncLayoutViewport(const LayerMetricsWrapper& aLayer);
   ParentLayerPoint GetCurrentAsyncScrollOffset(const LayerMetricsWrapper& aLayer);
   AsyncTransform GetCurrentAsyncTransform(const LayerMetricsWrapper& aLayer);
+  AsyncTransform GetCurrentAsyncTransformForFixedAdjustment(const LayerMetricsWrapper& aLayer);
   AsyncTransformComponentMatrix GetOverscrollTransform(const LayerMetricsWrapper& aLayer);
   AsyncTransformComponentMatrix GetCurrentAsyncTransformWithOverscroll(const LayerMetricsWrapper& aLayer);
 
   void MarkAsyncTransformAppliedToContent(const LayerMetricsWrapper& aLayer);
   bool HasUnusedAsyncTransform(const LayerMetricsWrapper& aLayer);
 
   /**
+   * Return a new AutoApplyAsyncTestAttributes instance. This should be called
+   * whenever we need to apply async attributes for test purposes (namely
+   * reftest-async-scroll-{x,y} and reftest-async-zoom).
+   */
+  UniquePtr<AutoApplyAsyncTestAttributes> ApplyAsyncTestAttributes(const LayerMetricsWrapper& aLayer);
+
+  /**
    * This can be used to assert that the current thread is the
    * sampler thread (which samples the async transform).
    * This does nothing if thread assertions are disabled.
    */
   void AssertOnSamplerThread() const;
 
   /**
    * Returns true if currently on the APZSampler's "sampler thread".
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -596,16 +596,21 @@ APZCTreeManager::SampleForWebRender(wr::
                                     const TimeStamp& aSampleTime)
 {
   AssertOnSamplerThread();
   MutexAutoLock lock(mMapLock);
 
   bool activeAnimations = false;
   for (const auto& mapping : mApzcMap) {
     AsyncPanZoomController* apzc = mapping.second;
+
+    // Apply any additional async scrolling for testing purposes (used for
+    // reftest-async-scroll and reftest-async-zoom).
+    auto _ = MakeUnique<AutoApplyAsyncTestAttributes>(apzc);
+
     ParentLayerPoint layerTranslation = apzc->GetCurrentAsyncTransform(
         AsyncPanZoomController::eForCompositing).mTranslation;
 
     // The positive translation means the painted content is supposed to
     // move down (or to the right), and that corresponds to a reduction in
     // the scroll offset. Since we are effectively giving WR the async
     // scroll delta here, we want to negate the translation.
     ParentLayerPoint asyncScrollDelta = -layerTranslation;
@@ -3041,16 +3046,19 @@ APZCTreeManager::CommonAncestor(AsyncPan
   return ancestor.forget();
 }
 
 LayerToParentLayerMatrix4x4
 APZCTreeManager::ComputeTransformForNode(const HitTestingTreeNode* aNode) const
 {
   mTreeLock.AssertCurrentThreadIn();
   if (AsyncPanZoomController* apzc = aNode->GetApzc()) {
+    // Apply any additional async scrolling for testing purposes (used for
+    // reftest-async-scroll and reftest-async-zoom).
+    auto _ = MakeUnique<AutoApplyAsyncTestAttributes>(apzc);
     // If the node represents scrollable content, apply the async transform
     // from its APZC.
     return aNode->GetTransform() *
         CompleteAsyncTransform(
           apzc->GetCurrentAsyncTransformWithOverscroll(AsyncPanZoomController::eForHitTesting));
   } else if (aNode->IsScrollThumbNode()) {
     // If the node represents a scrollbar thumb, compute and apply the
     // transformation that will be applied to the thumb in
@@ -3130,16 +3138,20 @@ APZCTreeManager::ComputeTransformForScro
   // layer has been built for a scroll frame, as this would result in a
   // disparity between scrollbars and visible content.
   if (aMetrics.IsScrollInfoLayer()) {
     return LayerToParentLayerMatrix4x4{};
   }
 
   MOZ_RELEASE_ASSERT(aApzc);
 
+  // Apply any additional async scrolling for testing purposes (used for
+  // reftest-async-scroll and reftest-async-zoom).
+  auto _ = MakeUnique<AutoApplyAsyncTestAttributes>(aApzc);
+
   AsyncTransformComponentMatrix asyncTransform =
     aApzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);
 
   // |asyncTransform| represents the amount by which we have scrolled and
   // zoomed since the last paint. Because the scrollbar was sized and positioned based
   // on the painted content, we need to adjust it based on asyncTransform so that
   // it reflects what the user is actually seeing now.
   AsyncTransformComponentMatrix scrollbarTransform;
--- a/gfx/layers/apz/src/APZSampler.cpp
+++ b/gfx/layers/apz/src/APZSampler.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/layers/APZSampler.h"
 
 #include "APZCTreeManager.h"
 #include "AsyncPanZoomController.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/APZUtils.h"
 #include "mozilla/layers/CompositorThread.h"
 #include "mozilla/layers/LayerMetricsWrapper.h"
 #include "mozilla/layers/SynchronousTask.h"
 #include "TreeTraversal.h"
 #include "mozilla/webrender/WebRenderAPI.h"
 
 namespace mozilla {
 namespace layers {
@@ -175,16 +176,26 @@ APZSampler::GetCurrentAsyncTransform(con
 {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   AssertOnSamplerThread();
 
   MOZ_ASSERT(aLayer.GetApzc());
   return aLayer.GetApzc()->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);
 }
 
+AsyncTransform
+APZSampler::GetCurrentAsyncTransformForFixedAdjustment(const LayerMetricsWrapper& aLayer)
+{
+  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+  AssertOnSamplerThread();
+
+  MOZ_ASSERT(aLayer.GetApzc());
+  return aLayer.GetApzc()->GetCurrentAsyncTransformForFixedAdjustment(AsyncPanZoomController::eForCompositing);
+}
+
 AsyncTransformComponentMatrix
 APZSampler::GetOverscrollTransform(const LayerMetricsWrapper& aLayer)
 {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   AssertOnSamplerThread();
 
   MOZ_ASSERT(aLayer.GetApzc());
   return aLayer.GetApzc()->GetOverscrollTransform(AsyncPanZoomController::eForCompositing);
@@ -217,16 +228,26 @@ APZSampler::HasUnusedAsyncTransform(cons
   AssertOnSamplerThread();
 
   AsyncPanZoomController* apzc = aLayer.GetApzc();
   return apzc
       && !apzc->GetAsyncTransformAppliedToContent()
       && !AsyncTransformComponentMatrix(apzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing)).IsIdentity();
 }
 
+UniquePtr<AutoApplyAsyncTestAttributes>
+APZSampler::ApplyAsyncTestAttributes(const LayerMetricsWrapper& aLayer)
+{
+  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+  AssertOnSamplerThread();
+
+  MOZ_ASSERT(aLayer.GetApzc());
+  return MakeUnique<AutoApplyAsyncTestAttributes>(aLayer.GetApzc());
+}
+
 void
 APZSampler::AssertOnSamplerThread() const
 {
   if (APZThreadUtils::GetThreadAssertionsEnabled()) {
     MOZ_ASSERT(IsSamplerThread());
   }
 }
 
--- a/gfx/layers/apz/src/APZUtils.cpp
+++ b/gfx/layers/apz/src/APZUtils.cpp
@@ -5,16 +5,29 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/layers/APZUtils.h"
 
 #include "AsyncPanZoomController.h"
 
 namespace mozilla {
 namespace layers {
+
+AutoApplyAsyncTestAttributes::AutoApplyAsyncTestAttributes(AsyncPanZoomController* aApzc)
+  : mApzc(aApzc)
+  , mPrevFrameMetrics(aApzc->Metrics())
+{
+  mApzc->ApplyAsyncTestAttributes();
+}
+
+AutoApplyAsyncTestAttributes::~AutoApplyAsyncTestAttributes()
+{
+  mApzc->UnapplyAsyncTestAttributes(mPrevFrameMetrics);
+}
+
 namespace apz {
 
 /*static*/ void
 InitializeGlobalState()
 {
   MOZ_ASSERT(NS_IsMainThread());
   AsyncPanZoomController::InitializeGlobalState();
 }
--- a/gfx/layers/apz/src/APZUtils.h
+++ b/gfx/layers/apz/src/APZUtils.h
@@ -15,16 +15,18 @@
 #include "mozilla/gfx/Point.h"
 #include "mozilla/DefineEnum.h"
 #include "mozilla/EnumSet.h"
 #include "mozilla/FloatingPoint.h"
 
 namespace mozilla {
 namespace layers {
 
+class AsyncPanZoomController;
+
 enum CancelAnimationFlags : uint32_t {
   Default = 0x0,             /* Cancel all animations */
   ExcludeOverscroll = 0x1,   /* Don't clear overscroll */
   ScrollSnap = 0x2,          /* Snap to snap points */
   ExcludeWheel = 0x4,        /* Don't stop wheel smooth-scroll animations */
   TriggeredExternally = 0x8, /* Cancellation was not triggered by APZ in
                                 response to an input event */
 };
@@ -93,16 +95,29 @@ struct TargetConfirmationFlags {
                        !(aHitTestInfo & gfx::CompositorHitTestInfo::eDispatchToContent))
     , mRequiresTargetConfirmation(aHitTestInfo & gfx::CompositorHitTestInfo::eRequiresTargetConfirmation)
   {}
 
   bool mTargetConfirmed : 1;
   bool mRequiresTargetConfirmation : 1;
 };
 
+/**
+ * An RAII class to temporarily apply async test attributes to the provided
+ * AsyncPanZoomController.
+ */
+class AutoApplyAsyncTestAttributes {
+public:
+  explicit AutoApplyAsyncTestAttributes(AsyncPanZoomController*);
+  ~AutoApplyAsyncTestAttributes();
+private:
+  AsyncPanZoomController* mApzc;
+  FrameMetrics mPrevFrameMetrics;
+};
+
 namespace apz {
 
 /**
  * Initializes the global state used in AsyncPanZoomController.
  * This is normally called when it is first needed in the constructor
  * of APZCTreeManager, but can be called manually to force it to be
  * initialized earlier.
  */
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -3794,47 +3794,101 @@ ParentLayerPoint
 AsyncPanZoomController::GetCurrentAsyncScrollOffset(AsyncTransformConsumer aMode) const
 {
   RecursiveMutexAutoLock lock(mRecursiveMutex);
 
   if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) {
     return mLastContentPaintMetrics.GetScrollOffset() * mLastContentPaintMetrics.GetZoom();
   }
 
-  return (GetEffectiveScrollOffset(aMode) + mTestAsyncScrollOffset)
-      * GetEffectiveZoom(aMode) * mTestAsyncZoom.scale;
+  return GetEffectiveScrollOffset(aMode) * GetEffectiveZoom(aMode);
 }
 
 CSSPoint
 AsyncPanZoomController::GetCurrentAsyncScrollOffsetInCssPixels(AsyncTransformConsumer aMode) const {
   RecursiveMutexAutoLock lock(mRecursiveMutex);
 
   if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) {
     return mLastContentPaintMetrics.GetScrollOffset();
   }
 
-  return GetEffectiveScrollOffset(aMode) + mTestAsyncScrollOffset;
+  return GetEffectiveScrollOffset(aMode);
+}
+
+AsyncTransform
+AsyncPanZoomController::GetCurrentAsyncViewportTransform(AsyncTransformConsumer aMode) const
+{
+  RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+  if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) {
+    return AsyncTransform();
+  }
+
+  CSSRect lastPaintViewport;
+  if (mLastContentPaintMetrics.IsScrollable()) {
+    lastPaintViewport = mLastContentPaintMetrics.GetViewport();
+  }
+
+  CSSRect currentViewport = GetEffectiveLayoutViewport(aMode);
+  CSSPoint currentViewportOffset = currentViewport.TopLeft();
+
+  // If checkerboarding has been disallowed, clamp the scroll position to stay
+  // within rendered content.
+  //
+  // TODO: This calculation is slightly inaccurate and this function will
+  // likely report an inconsistent transformation if apz.allow_checkerboarding
+  // is disabled. The correct calculation requires the following:
+  //
+  //   * Calculating a clamped visual scroll offset, like in
+  //     AsyncPanZoomController::GetCurrentAsyncTransform.
+  //   * Calling FrameMetrics::RecalculateViewportOffset to compute the layout
+  //     viewport offset corresponding to that visual offset and using that as
+  //     the clamped layout viewport offset.
+  //
+  if (!gfxPrefs::APZAllowCheckerboarding() &&
+      !mLastContentPaintMetrics.GetDisplayPort().IsEmpty()) {
+    CSSSize viewportSize = currentViewport.Size();
+    CSSPoint maxViewportOffset = lastPaintViewport.TopLeft() +
+      CSSPoint(mLastContentPaintMetrics.GetDisplayPort().XMost() - viewportSize.width,
+               mLastContentPaintMetrics.GetDisplayPort().YMost() - viewportSize.height);
+    CSSPoint minViewportOffset = lastPaintViewport.TopLeft() +
+      mLastContentPaintMetrics.GetDisplayPort().TopLeft();
+
+    if (minViewportOffset.x < maxViewportOffset.x) {
+      currentViewportOffset.x = clamped(currentViewportOffset.x, minViewportOffset.x, maxViewportOffset.x);
+    }
+    if (minViewportOffset.y < maxViewportOffset.y) {
+      currentViewportOffset.y = clamped(currentViewportOffset.y, minViewportOffset.y, maxViewportOffset.y);
+    }
+  }
+
+  CSSToParentLayerScale2D effectiveZoom = GetEffectiveZoom(aMode);
+  ParentLayerPoint translation =
+    (currentViewportOffset - lastPaintViewport.TopLeft()) * effectiveZoom;
+  LayerToParentLayerScale compositedAsyncZoom =
+    (effectiveZoom / Metrics().LayersPixelsPerCSSPixel()).ToScaleFactor();
+
+  return AsyncTransform(compositedAsyncZoom, -translation);
 }
 
 AsyncTransform
 AsyncPanZoomController::GetCurrentAsyncTransform(AsyncTransformConsumer aMode) const
 {
   RecursiveMutexAutoLock lock(mRecursiveMutex);
 
   if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) {
     return AsyncTransform();
   }
 
   CSSPoint lastPaintScrollOffset;
   if (mLastContentPaintMetrics.IsScrollable()) {
     lastPaintScrollOffset = mLastContentPaintMetrics.GetScrollOffset();
   }
 
-  CSSPoint currentScrollOffset = GetEffectiveScrollOffset(aMode) +
-    mTestAsyncScrollOffset;
+  CSSPoint currentScrollOffset = GetEffectiveScrollOffset(aMode);
 
   // If checkerboarding has been disallowed, clamp the scroll position to stay
   // within rendered content.
   if (!gfxPrefs::APZAllowCheckerboarding() &&
       !mLastContentPaintMetrics.GetDisplayPort().IsEmpty()) {
     CSSSize compositedSize = mLastContentPaintMetrics.CalculateCompositedSizeInCssPixels();
     CSSPoint maxScrollOffset = lastPaintScrollOffset +
       CSSPoint(mLastContentPaintMetrics.GetDisplayPort().XMost() - compositedSize.width,
@@ -3845,25 +3899,45 @@ AsyncPanZoomController::GetCurrentAsyncT
       currentScrollOffset.x = clamped(currentScrollOffset.x, minScrollOffset.x, maxScrollOffset.x);
     }
     if (minScrollOffset.y < maxScrollOffset.y) {
       currentScrollOffset.y = clamped(currentScrollOffset.y, minScrollOffset.y, maxScrollOffset.y);
     }
   }
 
   CSSToParentLayerScale2D effectiveZoom = GetEffectiveZoom(aMode);
-
-  ParentLayerPoint translation = (currentScrollOffset - lastPaintScrollOffset)
-                               * effectiveZoom * mTestAsyncZoom.scale;
-
+  ParentLayerPoint translation =
+    (currentScrollOffset - lastPaintScrollOffset) * effectiveZoom;
   LayerToParentLayerScale compositedAsyncZoom =
-      (effectiveZoom / Metrics().LayersPixelsPerCSSPixel()).ToScaleFactor();
-  return AsyncTransform(
-    LayerToParentLayerScale(compositedAsyncZoom.scale * mTestAsyncZoom.scale),
-    -translation);
+    (effectiveZoom / Metrics().LayersPixelsPerCSSPixel()).ToScaleFactor();
+
+  return AsyncTransform(compositedAsyncZoom, -translation);
+}
+
+AsyncTransform
+AsyncPanZoomController::GetCurrentAsyncTransformForFixedAdjustment(AsyncTransformConsumer aMode) const
+{
+  RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+  // Use the layout viewport to adjust fixed position elements if and only if
+  // it's larger than the visual viewport (assuming we're scrolling the RCD-RSF
+  // with apz.allow_zooming enabled).
+  return (
+      gfxPrefs::APZAllowZooming() &&
+      Metrics().IsRootContent() &&
+      Metrics().GetVisualViewport().Size() <= Metrics().GetViewport().Size()
+  ) ? GetCurrentAsyncViewportTransform(aMode)
+    : GetCurrentAsyncTransform(aMode);
+}
+
+AsyncTransformComponentMatrix
+AsyncPanZoomController::GetCurrentAsyncTransformWithOverscroll(AsyncTransformConsumer aMode) const
+{
+  return AsyncTransformComponentMatrix(GetCurrentAsyncTransform(aMode))
+       * GetOverscrollTransform(aMode);
 }
 
 CSSRect
 AsyncPanZoomController::GetEffectiveLayoutViewport(AsyncTransformConsumer aMode) const
 {
   if (gfxPrefs::APZFrameDelayEnabled() && aMode == eForCompositing) {
     return mCompositedLayoutViewport;
   }
@@ -3887,31 +3961,50 @@ AsyncPanZoomController::GetEffectiveZoom
   }
   return Metrics().GetZoom();
 }
 
 bool
 AsyncPanZoomController::SampleCompositedAsyncTransform()
 {
   RecursiveMutexAutoLock lock(mRecursiveMutex);
-  if (mCompositedScrollOffset != Metrics().GetScrollOffset() ||
+  if (!mCompositedLayoutViewport.IsEqualEdges(Metrics().GetViewport()) ||
+      mCompositedScrollOffset != Metrics().GetScrollOffset() ||
       mCompositedZoom != Metrics().GetZoom()) {
     mCompositedLayoutViewport = Metrics().GetViewport();
     mCompositedScrollOffset = Metrics().GetScrollOffset();
     mCompositedZoom = Metrics().GetZoom();
     return true;
   }
   return false;
 }
 
-AsyncTransformComponentMatrix
-AsyncPanZoomController::GetCurrentAsyncTransformWithOverscroll(AsyncTransformConsumer aMode) const
-{
-  return AsyncTransformComponentMatrix(GetCurrentAsyncTransform(aMode))
-       * GetOverscrollTransform(aMode);
+bool
+AsyncPanZoomController::ApplyAsyncTestAttributes() {
+  RecursiveMutexAutoLock lock(mRecursiveMutex);
+  if (mTestAsyncScrollOffset == CSSPoint() &&
+      mTestAsyncZoom == LayerToParentLayerScale()) {
+    return false;
+  }
+  Metrics().ZoomBy(mTestAsyncZoom.scale);
+  ScrollBy(mTestAsyncScrollOffset);
+  SampleCompositedAsyncTransform();
+  return true;
+}
+
+bool
+AsyncPanZoomController::UnapplyAsyncTestAttributes(const FrameMetrics& aPrevFrameMetrics) {
+  RecursiveMutexAutoLock lock(mRecursiveMutex);
+  if (mTestAsyncScrollOffset == CSSPoint() &&
+      mTestAsyncZoom == LayerToParentLayerScale()) {
+    return false;
+  }
+  Metrics() = aPrevFrameMetrics;
+  SampleCompositedAsyncTransform();
+  return true;
 }
 
 Matrix4x4 AsyncPanZoomController::GetTransformToLastDispatchedPaint() const {
   RecursiveMutexAutoLock lock(mRecursiveMutex);
 
   LayerPoint scrollChange =
     (mLastContentPaintMetrics.GetScrollOffset() - mExpectedGeckoMetrics.GetScrollOffset())
     * mLastContentPaintMetrics.GetDevPixelsPerCSSPixel()
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -1043,24 +1043,39 @@ public:
 
   /**
    * Return a visual effect that reflects this apzc's
    * overscrolled state, if any.
    */
   AsyncTransformComponentMatrix GetOverscrollTransform(AsyncTransformConsumer aMode) const;
 
   /**
+   * Returns the incremental transformation corresponding to the async
+   * panning/zooming of the layout viewport (unlike GetCurrentAsyncTransform,
+   * which deals with async movement of the visual viewport). That is, when
+   * this transform is multiplied with the layer's existing transform, it will
+   * make the layer appear with the desired pan/zoom amount.
+   */
+  AsyncTransform GetCurrentAsyncViewportTransform(AsyncTransformConsumer aMode) const;
+
+  /**
    * Returns the incremental transformation corresponding to the async pan/zoom
    * in progress. That is, when this transform is multiplied with the layer's
    * existing transform, it will make the layer appear with the desired pan/zoom
    * amount.
    */
   AsyncTransform GetCurrentAsyncTransform(AsyncTransformConsumer aMode) const;
 
   /**
+   * Returns the incremental transformation corresponding to the async
+   * panning/zooming of the larger of the visual or layout viewport.
+   */
+  AsyncTransform GetCurrentAsyncTransformForFixedAdjustment(AsyncTransformConsumer aMode) const;
+
+  /**
    * Returns the same transform as GetCurrentAsyncTransform(), but includes
    * any transform due to axis over-scroll.
    */
   AsyncTransformComponentMatrix GetCurrentAsyncTransformWithOverscroll(AsyncTransformConsumer aMode) const;
 
 private:
   /**
    * Samples the composited async transform, making the result of
@@ -1081,16 +1096,38 @@ private:
    * zoom either directly from |Metrics()|, or from cached variables that
    * store the required value from the last time it was sampled by calling
    * SampleCompositedAsyncTransform(), depending on who is asking.
    */
   CSSRect GetEffectiveLayoutViewport(AsyncTransformConsumer aMode) const;
   CSSPoint GetEffectiveScrollOffset(AsyncTransformConsumer aMode) const;
   CSSToParentLayerScale2D GetEffectiveZoom(AsyncTransformConsumer aMode) const;
 
+private:
+  friend class AutoApplyAsyncTestAttributes;
+
+  /**
+   * Applies |mTestAsyncScrollOffset| and |mTestAsyncZoom| to this
+   * AsyncPanZoomController. Calls |SampleCompositedAsyncTransform| to ensure
+   * that the GetCurrentAsync* functions consider the test offset and zoom in
+   * their computations.
+   *
+   * Returns false if neither test value is set, and true otherwise.
+   */
+  bool ApplyAsyncTestAttributes();
+
+  /**
+   * Sets this AsyncPanZoomController's FrameMetrics to |aPrevFrameMetrics| and
+   * calls |SampleCompositedAsyncTransform| to unapply any test values applied
+   * by |ApplyAsyncTestAttributes|.
+   *
+   * Returns false if neither test value is set, and true otherwise.
+   */
+  bool UnapplyAsyncTestAttributes(const FrameMetrics& aPrevFrameMetrics);
+
   /* ===================================================================
    * The functions and members in this section are used to manage
    * the state that tracks what this APZC is doing with the input events.
    */
 protected:
   enum PanZoomState {
     NOTHING,                  /* no touch-start events received */
     FLING,                    /* all touches removed, but we're still scrolling page */
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width; initial-scale=2.0">
+  <title>Hittest position:fixed zoomed scroll</title>
+  <script type="application/javascript" src="apz_test_utils.js"></script>
+  <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <style>
+    body {
+      margin: 0;
+    }
+    #fixed {
+      position: fixed;
+      height: 30px;
+      width: 100%;
+      background: linear-gradient(135deg, white, black);
+    }
+    #fixed > input {
+      position: absolute;
+      top: 0;
+      right: 0;
+      height: 100%;
+    }
+  </style>
+</head>
+<body>
+  <div id="fixed"><input type="button" value="Button" /></div>
+  <script>
+    const input = document.querySelector("input");
+    function* test(testDriver) {
+      SpecialPowers.Services.obs.addObserver(testDriver, "APZ:TransformEnd");
+      yield synthesizeNativeTouchDrag(document.body, 10, 10, -2000, 0);
+      SpecialPowers.Services.obs.removeObserver(testDriver, "APZ:TransformEnd", false);
+
+      yield waitForApzFlushedRepaints(testDriver);
+
+      // TODO: We're unsure why, but adding this observer substantially
+      // decreases the rate of intermittents that we see. We should figure out
+      // why and/or rewrite this test entirely.
+      SpecialPowers.Services.obs.addObserver(testDriver, "mouseevent");
+      yield synthesizeNativeClick(input, 0, 0, testDriver);
+      SpecialPowers.Services.obs.removeObserver(testDriver, "mouseevent", false);
+    }
+    window.addEventListener("click", (e) => {
+      is(e.target, input, "got click");
+      subtestDone();
+    });
+    waitUntilApzStable().then(runContinuation(test));
+  </script>
+</body>
+</html>
+
--- a/gfx/layers/apz/test/mochitest/test_group_zoom.html
+++ b/gfx/layers/apz/test/mochitest/test_group_zoom.html
@@ -35,16 +35,17 @@ var doubletap_prefs = [
   ["ui.click_hold_context_menus.delay", 10000],
   ["apz.max_tap_time", 10000],
 ];
 
 var subtests = [
   {'file': 'helper_bug1280013.html', 'prefs': prefs},
   {'file': 'helper_basic_zoom.html', 'prefs': prefs},
   {'file': 'helper_zoomed_pan.html', 'prefs': prefs},
+  {'file': 'helper_fixed_position_scroll_hittest.html', 'prefs': prefs},
   {'file': 'helper_basic_doubletap_zoom.html', 'prefs': doubletap_prefs},
 ];
 
 if (isApzEnabled()) {
   // This has a lot of subtests, and Android emulators are slow.
   SimpleTest.requestLongerTimeout(2);
   SimpleTest.waitForExplicitFinish();
   window.onload = function() {
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=1.5">
+  <style>
+    body {
+      margin: 0;
+      height: 2000px;
+      overflow: hidden;
+    }
+    div {
+      position: absolute;
+      bottom: 0;
+      width: 100%;
+      height: 500px;
+      background: repeating-linear-gradient(90deg, transparent, transparent 20px, black 20px, black 40px);
+    }
+  </style>
+</head>
+<body>
+  <div></div>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=1.5">
+  <style>
+    body {
+      margin: 0;
+      height: 2000px;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      bottom: 0;
+      width: 100%;
+      height: 500px;
+      background: repeating-linear-gradient(90deg, transparent, transparent 20px, black 20px, black 40px);
+    }
+  </style>
+</head>
+<body onload="scrollTo(0, 500); document.documentElement.classList.remove('reftest-wait');">
+  <!-- Test that fixed position elements are attached to the layout viewport
+       instead of the visual viewport.
+
+       The reference page has a position:absolute element in place of a
+       position:fixed element, both positioned at the bottom of the page.
+
+       After zooming in, the top edge of the visual viewport will coincide with
+       the top edge of the layout viewport, but their bottom edges will
+       diverge.
+
+       Since absolute elements are attached to the initial containing block,
+       which coincides with the layout viewport on page load, the rendering of
+       the fixed element will only match if it is being attached to the layout
+       viewport. -->
+  <div></div>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=1.5">
+  <style>
+    body {
+      margin: 0;
+      height: 2000px;
+      overflow: hidden;
+    }
+    #tall {
+      height: 100vh;
+    }
+    #sticky {
+      position: absolute;
+      bottom: 0;
+      width: 100%;
+      height: 500px;
+      background: repeating-linear-gradient(90deg, transparent, transparent 20px, black 20px, black 40px);
+    }
+  </style>
+</head>
+<body>
+  <div id="tall"></div>
+  <div id="sticky"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=1.5">
+  <style>
+    body {
+      margin: 0;
+      height: 2000px;
+      overflow: hidden;
+    }
+    #tall {
+      height: 100vh;
+    }
+    #sticky {
+      position: sticky;
+      bottom: 0;
+      width: 100%;
+      height: 500px;
+      background: repeating-linear-gradient(90deg, transparent, transparent 20px, black 20px, black 40px);
+    }
+  </style>
+</head>
+<body onload="scrollTo(0, 500); document.documentElement.classList.remove('reftest-wait');">
+  <!-- This is similar to the pinch-zoom-position-fixed test, but we add a tall
+       element before the sticky element to ensure that the sticky element is
+       in its "fixed" configuration on page load. -->
+  <div id="tall"></div>
+  <div id="sticky"></div>
+</body>
+</html>
+
--- a/gfx/layers/apz/test/reftest/reftest.list
+++ b/gfx/layers/apz/test/reftest/reftest.list
@@ -17,8 +17,17 @@ fuzzy-if(Android,0-45,0-26) skip-if(!And
 # As above, the end of the scrollthumb won't match perfectly, but the bulk of the scrollbar should be present and identical.
 fuzzy-if(Android,0-54,0-14) skip-if(!Android) pref(apz.allow_zooming,true) == scrollbar-zoom-resolution-1.html scrollbar-zoom-resolution-1-ref.html
 fuzzy-if(Android,0-51,0-22) skip-if(!Android) pref(apz.allow_zooming,true) == scrollbar-zoom-resolution-2.html scrollbar-zoom-resolution-2-ref.html
 
 # Meta-viewport tag support
 skip-if(!Android) pref(apz.allow_zooming,true) == initial-scale-1.html initial-scale-1-ref.html
 
 skip-if(!asyncPan) == frame-reconstruction-scroll-clamping.html frame-reconstruction-scroll-clamping-ref.html
+
+# Test that position:fixed and position:sticky elements are attached to the
+# layout viewport.
+#
+# We skip these tests on Desktop platforms since they require container
+# scrolling, which is enabled by default on Android, but behind a "Once" pref
+# and cannot be enabled for individual reftests.
+skip-if(!Android) pref(apz.allow_zooming,true) == pinch-zoom-position-fixed.html pinch-zoom-position-fixed-ref.html
+skip-if(!Android) pref(apz.allow_zooming,true) == pinch-zoom-position-sticky.html pinch-zoom-position-sticky-ref.html
--- a/gfx/layers/composite/AsyncCompositionManager.cpp
+++ b/gfx/layers/composite/AsyncCompositionManager.cpp
@@ -972,16 +972,21 @@ AsyncCompositionManager::ApplyAsyncConte
         }
 
         if (RefPtr<APZSampler> sampler = mCompositorBridge->GetAPZSampler()) {
           for (uint32_t i = 0; i < layer->GetScrollMetadataCount(); i++) {
             LayerMetricsWrapper wrapper(layer, i);
             if (!wrapper.GetApzc()) {
               continue;
             }
+
+            // Apply any additional async scrolling for testing purposes (used
+            // for reftest-async-scroll and reftest-async-zoom).
+            auto _ = sampler->ApplyAsyncTestAttributes(wrapper);
+
             const FrameMetrics& metrics = wrapper.Metrics();
             MOZ_ASSERT(metrics.IsScrollable());
 
             hasAsyncTransform = true;
 
             AsyncTransform asyncTransformWithoutOverscroll =
                 sampler->GetCurrentAsyncTransform(wrapper);
             AsyncTransformComponentMatrix overscrollTransform =
@@ -1058,21 +1063,21 @@ AsyncCompositionManager::ApplyAsyncConte
 
             // For the purpose of aligning fixed and sticky layers, we disregard
             // the overscroll transform as well as any OMTA transform when computing the
             // 'aCurrentTransformForRoot' parameter. This ensures that the overscroll
             // and OMTA transforms are not unapplied, and therefore that the visual
             // effects apply to fixed and sticky layers. We do this by using
             // GetTransform() as the base transform rather than GetLocalTransform(),
             // which would include those factors.
-            LayerToParentLayerMatrix4x4 transformWithoutOverscrollOrOmta =
-                layer->GetTransformTyped()
-              * CompleteAsyncTransform(
-                  AdjustForClip(asyncTransformWithoutOverscroll, layer));
-
+            AsyncTransform asyncTransformForFixedAdjustment
+              = sampler->GetCurrentAsyncTransformForFixedAdjustment(wrapper);
+            LayerToParentLayerMatrix4x4 transformWithoutOverscrollOrOmta
+              = layer->GetTransformTyped()
+              * CompleteAsyncTransform(AdjustForClip(asyncTransformForFixedAdjustment, layer));
             AlignFixedAndStickyLayers(layer, layer, metrics.GetScrollId(), oldTransform,
                                       transformWithoutOverscrollOrOmta, fixedLayerMargins,
                                       &clipPartsCache);
 
             // Combine the local clip with the ancestor scrollframe clip. This is not
             // included in the async transform above, since the ancestor clip should not
             // move with this APZC.
             if (scrollMetadata.HasScrollClip()) {
--- a/layout/generic/ViewportFrame.cpp
+++ b/layout/generic/ViewportFrame.cpp
@@ -276,24 +276,24 @@ ViewportFrame::AdjustReflowInputAsContai
 #endif
     AdjustReflowInputForScrollbars(aReflowInput);
 
   NS_ASSERTION(GetAbsoluteContainingBlock()->GetChildList().IsEmpty() ||
                (offset.x == 0 && offset.y == 0),
                "We don't handle correct positioning of fixed frames with "
                "scrollbars in odd positions");
 
-  // If a scroll position clamping viewport size has been set, layout
-  // fixed position elements to this size instead of the computed size.
+  // Layout fixed position elements to the visual viewport size if and only if
+  // it has been set and it is larger than the computed size, otherwise use the
+  // computed size.
   nsRect rect(0, 0, aReflowInput->ComputedWidth(), aReflowInput->ComputedHeight());
   nsIPresShell* ps = PresShell();
-  if (ps->IsVisualViewportSizeSet()) {
+  if (ps->IsVisualViewportSizeSet() && rect.Size() < ps->GetVisualViewportSize()) {
     rect.SizeTo(ps->GetVisualViewportSize());
   }
-
   return rect;
 }
 
 void
 ViewportFrame::Reflow(nsPresContext*           aPresContext,
                       ReflowOutput&     aDesiredSize,
                       const ReflowInput& aReflowInput,
                       nsReflowStatus&          aStatus)
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -1469,33 +1469,35 @@ public:
     static nsRect ComputeVisibleRectForFrame(nsDisplayListBuilder* aBuilder,
                                              nsIFrame* aFrame,
                                              const nsRect& aVisibleRect,
                                              const nsRect& aDirtyRect,
                                              nsRect* aOutDirtyRect) {
       nsRect visible = aVisibleRect;
       nsRect dirtyRectRelativeToDirtyFrame = aDirtyRect;
 
-      if (nsLayoutUtils::IsFixedPosFrameInDisplayPort(aFrame) &&
-          aBuilder->IsPaintingToWindow()) {
-        // position: fixed items are reflowed into and only drawn inside the
-        // viewport, or the visual viewport size, if one is set.
-        nsIPresShell* ps = aFrame->PresShell();
-        if (ps->IsVisualViewportSizeSet()) {
-          dirtyRectRelativeToDirtyFrame =
-            nsRect(nsPoint(0, 0), ps->GetVisualViewportSize());
-          visible = dirtyRectRelativeToDirtyFrame;
 #ifdef MOZ_WIDGET_ANDROID
-        } else {
-          dirtyRectRelativeToDirtyFrame =
-            nsRect(nsPoint(0, 0), aFrame->GetParent()->GetSize());
-          visible = dirtyRectRelativeToDirtyFrame;
+        if (nsLayoutUtils::IsFixedPosFrameInDisplayPort(aFrame) &&
+            aBuilder->IsPaintingToWindow()) {
+            // We want to ensure that fixed position elements are visible when
+            // being async scrolled, so we paint them at the size of the larger
+            // viewport.
+            dirtyRectRelativeToDirtyFrame =
+              nsRect(nsPoint(0, 0), aFrame->GetParent()->GetSize());
+
+            nsIPresShell* ps = aFrame->PresShell();
+            if (ps->IsVisualViewportSizeSet() &&
+                dirtyRectRelativeToDirtyFrame.Size() < ps->GetVisualViewportSize()) {
+                dirtyRectRelativeToDirtyFrame.SizeTo(ps->GetVisualViewportSize());
+            }
+
+            visible = dirtyRectRelativeToDirtyFrame;
+        }
 #endif
-        }
-      }
+
       *aOutDirtyRect = dirtyRectRelativeToDirtyFrame - aFrame->GetPosition();
       visible -= aFrame->GetPosition();
 
       nsRect overflowRect = aFrame->GetVisualOverflowRect();
 
       if (aFrame->IsTransformed() &&
           mozilla::EffectCompositor::HasAnimationsForCompositor(aFrame,
                                                                 eCSSProperty_transform)) {
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-fixed-async-zoom-1-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=2.0">
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      top: -50px;
+      width: 100px;
+      height: 100px;
+      background: green;
+    }
+  </style>
+</head>
+<body>
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-fixed-async-zoom-1.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html reftest-async-scroll
+      reftest-displayport-x="0" reftest-displayport-y="0"
+      reftest-displayport-w="800" reftest-displayport-h="1000"
+      reftest-async-scroll-x="0" reftest-async-scroll-y="50"
+      reftest-async-zoom="2.0">
+<head>
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      top: 0;
+      width: 100px;
+      height: 100px;
+      background: green;
+    }
+  </style>
+</head>
+<body>
+  <!-- Test that position:fixed elements are attached to the layout viewport
+       instead of the visual viewport.
+
+       An async scroll of 50 CSS pixels will scroll the visual viewport by 100
+       screen pixels (since async zoom is set to 2.0) and result in the top
+       half of the div being scrolled out of view. -->
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-fixed-async-zoom-2-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=2.0">
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: absolute;
+      top: 0;
+      width: 100px;
+      height: 100px;
+      background: green;
+    }
+  </style>
+</head>
+<body>
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-fixed-async-zoom-2.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+      reftest-async-scroll
+      reftest-displayport-x="0" reftest-displayport-y="0"
+      reftest-displayport-w="800" reftest-displayport-h="1000"
+      reftest-async-scroll-x="0" reftest-async-scroll-y="-50"
+      reftest-async-zoom="2.0">
+<head>
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      top: 0;
+      width: 100px;
+      height: 100px;
+      background: green;
+    }
+  </style>
+</head>
+<body onload="scrollTo(0, 1000); document.documentElement.classList.remove('reftest-wait');">
+  <!-- Test that position:fixed elements scroll with the layout viewport.
+
+       Scroll the window (i.e., the layout viewport) to (0, 1000). An async
+       scroll of -50 CSS pixels will move both the layout and visual viewport
+       up by 100 screen pixels (since async zoom is set to 2.0). The div should
+       remain at top-left corner of the layout viewport. -->
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-fixed-async-zoom-3-ref.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=2.0">
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      top: 20px;
+      right: 20px;
+      height: 100px;
+      width: 100px;
+      background: blue;
+    }
+  </style>
+</head>
+<body>
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-fixed-async-zoom-3.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html reftest-async-scroll
+      reftest-displayport-x="0" reftest-displayport-y="0"
+      reftest-displayport-w="800" reftest-displayport-h="1000"
+      reftest-async-zoom="2.0">
+<head>
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      top: 20px;
+      right: 20px;
+      height: 100px;
+      width: 100px;
+      background: blue;
+    }
+  </style>
+</head>
+<body>
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-fixed-async-zoom-4-ref.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=2.0">
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      height: 100px;
+      width: 100px;
+    }
+    div.top-left {
+      top: 20px;
+      left: 20px;
+      background: red;
+    }
+    div.top-right {
+      top: 20px;
+      right: 20px;
+      background: blue;
+    }
+    div.bottom-left {
+      bottom: 20px;
+      left: 20px;
+      background: green;
+    }
+    div.bottom-right {
+      bottom: 20px;
+      right: 20px;
+      background: purple;
+    }
+  </style>
+</head>
+<body>
+  <div class="top-left"></div>
+  <div class="top-right"></div>
+  <div class="bottom-left"></div>
+  <div class="bottom-right"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-fixed-async-zoom-4.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html reftest-async-scroll
+      reftest-displayport-x="0" reftest-displayport-y="0"
+      reftest-displayport-w="800" reftest-displayport-h="1000"
+      reftest-async-zoom="2.0">
+<head>
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      height: 100px;
+      width: 100px;
+    }
+    div.top-left {
+      top: 20px;
+      left: 20px;
+      background: red;
+    }
+    div.top-right {
+      top: 20px;
+      right: 20px;
+      background: blue;
+    }
+    div.bottom-left {
+      bottom: 20px;
+      left: 20px;
+      background: green;
+    }
+    div.bottom-right {
+      bottom: 20px;
+      right: 20px;
+      background: purple;
+    }
+  </style>
+</head>
+<body>
+  <div class="top-left"></div>
+  <div class="top-right"></div>
+  <div class="bottom-left"></div>
+  <div class="bottom-right"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-sticky-async-zoom-1-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=2.0">
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: fixed;
+      top: -50px;
+      width: 100px;
+      height: 100px;
+      background: green;
+    }
+  </style>
+</head>
+<body>
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-sticky-async-zoom-1.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html reftest-async-scroll
+      reftest-displayport-x="0" reftest-displayport-y="0"
+      reftest-displayport-w="800" reftest-displayport-h="1000"
+      reftest-async-scroll-x="0" reftest-async-scroll-y="50"
+      reftest-async-zoom="2.0">
+<head>
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: sticky;
+      top: 0;
+      width: 100px;
+      height: 100px;
+      background: green;
+    }
+  </style>
+</head>
+<body>
+  <!-- This test is identical to the position-fixed-async-zoom-1 test, but for
+       position:sticky elements. -->
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-sticky-async-zoom-2-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=2.0">
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: absolute;
+      top: 0;
+      width: 100px;
+      height: 100px;
+      background: green;
+    }
+  </style>
+</head>
+<body>
+  <div></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/async-scrolling/position-sticky-async-zoom-2.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+      reftest-async-scroll
+      reftest-displayport-x="0" reftest-displayport-y="0"
+      reftest-displayport-w="800" reftest-displayport-h="1000"
+      reftest-async-scroll-x="0" reftest-async-scroll-y="-50"
+      reftest-async-zoom="2.0">
+<head>
+  <style>
+    body {
+      height: 3000px;
+      margin: 0;
+      overflow: hidden;
+    }
+    div {
+      position: sticky;
+      top: 0;
+      width: 100px;
+      height: 100px;
+      background: green;
+    }
+  </style>
+</head>
+<body onload="scrollTo(0, 1000); document.documentElement.classList.remove('reftest-wait');">
+  <!-- This test is identical to the position-fixed-async-zoom-2 test, but for
+       position:sticky elements. -->
+  <div></div>
+</body>
+</html>
--- a/layout/reftests/async-scrolling/reftest.list
+++ b/layout/reftests/async-scrolling/reftest.list
@@ -71,8 +71,15 @@ fuzzy-if(Android,0-2,0-4) skip-if(!async
 # as it will expand the displayport beyond what the test specifies in
 # its reftest-displayport attributes, and interfere with where we expect
 # checkerboarding to occur
 default-preferences pref(layers.low-precision-buffer,false)
 skip-if(!asyncPan) == checkerboard-1.html checkerboard-1-ref.html
 skip-if(!asyncPan) == checkerboard-2.html checkerboard-2-ref.html
 skip-if(!asyncPan) == checkerboard-3.html checkerboard-3-ref.html
 default-preferences
+
+skip-if(!Android) pref(apz.allow_zooming,true) == position-fixed-async-zoom-1.html position-fixed-async-zoom-1-ref.html
+skip-if(!Android) pref(apz.allow_zooming,true) == position-fixed-async-zoom-2.html position-fixed-async-zoom-2-ref.html
+fails skip-if(!Android) pref(apz.allow_zooming,true) == position-fixed-async-zoom-3.html position-fixed-async-zoom-3-ref.html
+fails skip-if(!Android) pref(apz.allow_zooming,true) == position-fixed-async-zoom-4.html position-fixed-async-zoom-4-ref.html
+skip-if(!Android) pref(apz.allow_zooming,true) == position-sticky-async-zoom-1.html position-sticky-async-zoom-1-ref.html
+skip-if(!Android) pref(apz.allow_zooming,true) == position-sticky-async-zoom-2.html position-sticky-async-zoom-2-ref.html