commit 4ccd058d0fa0a5362e86002de794802ed496ebe0
parent 545cedcf48cca5034553c16acbfe731eac1e3b9b
Author: Timothy Nikkel <tnikkel@gmail.com>
Date: Fri, 12 Dec 2025 00:10:56 +0000
Bug 2003843. Handle the transform case and one-axis case of async scrolling with CSS anchor pos. r=hiro,layout-reviewers
If we have an anchor that is transformed, then if we assign the ASR of the anchor to the anchored content, that means the anchored content will get the same spatial node in web render as the anchor, that means web render will draw it as if it is also transformed. This is not correct. In order to not render incorrectly in this case we disable async scrolling with the anchor in this case. It will still scroll with the anchor, just without the benefit of async scrolling (ie the main thread updates it).
In order to avoid this we keep track on the nsDisplayListBuilder whether a frame async scrolls with its anchor or not. We don't try to do any tracking outside of painting. This seems okay because outside of painting we only use this information for activating display ports, and async scrolling with the anchor only inserts more frames in the ASR/async scrollable frame ancestor chain. We clear the information on the nsDisplayListBuilder after every frame. RDL is already disabled when anchor pos async scroll is happening so we don't have to worry about any invalidations.
This then requires an extra walk of the same frames that ActivateDisplayportOnASRAncestors will walk over later. And further, we might have to recurse if we have chained anchors and walk them again. Minimizing this walk seems too complicated. Chained anchors shouldn't be common, if we are walking these frames they should be in the cpu's cache.
We have to create yet another function that walks over ASRs (ShouldAsyncScrollWithAnchorNotCached), because we need to check each individual frame that is walked for being transformed (the existing users don't need this level of granularity).
Since it's only a bit more code this patch also fixes bug 2003845, which exists because ASRs can only scroll in both axis, but anchor pos sometimes only scrolls in one axis with its anchor. If we encounter this case, and a scroll frame for which this would actually make a difference we disable async scroll as well.
Differential Revision: https://phabricator.services.mozilla.com/D275453
Diffstat:
14 files changed, 344 insertions(+), 26 deletions(-)
diff --git a/layout/base/AnchorPositioningUtils.cpp b/layout/base/AnchorPositioningUtils.cpp
@@ -6,6 +6,7 @@
#include "AnchorPositioningUtils.h"
+#include "DisplayPortUtils.h"
#include "ScrollContainerFrame.h"
#include "mozilla/Maybe.h"
#include "mozilla/PresShell.h"
@@ -15,6 +16,7 @@
#include "mozilla/dom/Element.h"
#include "nsCanvasFrame.h"
#include "nsContainerFrame.h"
+#include "nsDisplayList.h"
#include "nsIContent.h"
#include "nsIFrame.h"
#include "nsIFrameInlines.h"
@@ -877,13 +879,20 @@ bool AnchorPositioningUtils::FitsInContainingBlock(
}
nsIFrame* AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
- nsIFrame* aFrame) {
+ nsIFrame* aFrame, nsDisplayListBuilder* aBuilder,
+ bool aSkipAsserts /* = false */) {
+#ifdef DEBUG
+ if (!aSkipAsserts) {
+ MOZ_ASSERT(!aBuilder || aBuilder->IsPaintingToWindow());
+ MOZ_ASSERT_IF(!aBuilder, aFrame->PresContext()->LayoutPhaseCount(
+ nsLayoutPhase::DisplayListBuilding) == 0);
+ }
+#endif
+
if (!StaticPrefs::apz_async_scroll_css_anchor_pos_AtStartup()) {
return nullptr;
}
- mozilla::PhysicalAxes axes = aFrame->GetAnchorPosCompensatingForScroll();
- // TODO for now we return the anchor if we are compensating in either axis.
- // This is not fully spec compliant, bug 1988034 tracks this.
+ PhysicalAxes axes = aFrame->GetAnchorPosCompensatingForScroll();
if (axes.isEmpty()) {
return nullptr;
}
@@ -902,7 +911,19 @@ nsIFrame* AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
aFrame->GetParent(), anchor)) {
return nullptr;
}
- return anchor;
+ if (!aBuilder) {
+ return anchor;
+ }
+ // TODO for now ShouldAsyncScrollWithAnchor will return false if we are
+ // compensating in only one axis and there is a scroll frame between the
+ // anchor and the positioned's containing block that can scroll in the "wrong"
+ // axis so that we don't async scroll in the wrong axis because ASRs/APZ only
+ // support scrolling in both axes. This is not fully spec compliant, bug
+ // 1988034 tracks this.
+ return DisplayPortUtils::ShouldAsyncScrollWithAnchor(aFrame, anchor, aBuilder,
+ axes)
+ ? anchor
+ : nullptr;
}
static bool TriggerFallbackReflow(PresShell* aPresShell, nsIFrame* aPositioned,
diff --git a/layout/base/AnchorPositioningUtils.h b/layout/base/AnchorPositioningUtils.h
@@ -23,6 +23,8 @@ class CopyableTArray;
namespace mozilla {
+class nsDisplayListBuilder;
+
struct AnchorPosInfo {
// Border-box of the anchor frame, offset against the positioned frame's
// absolute containing block's padding box.
@@ -315,8 +317,12 @@ struct AnchorPositioningUtils {
/**
* If aFrame is positioned using CSS anchor positioning, and it scrolls with
* its anchor this function returns the anchor. Otherwise null.
+ * Note that this function has different behaviour if it called during paint
+ * (ie aBuilder not null) or not during painting (aBuilder null).
*/
- static nsIFrame* GetAnchorThatFrameScrollsWith(nsIFrame* aFrame);
+ static nsIFrame* GetAnchorThatFrameScrollsWith(nsIFrame* aFrame,
+ nsDisplayListBuilder* aBuilder,
+ bool aSkipAsserts = false);
// Trigger a layout for positioned items that are currently overflowing their
// abs-cb and that have available fallbacks to try.
diff --git a/layout/base/DisplayPortUtils.cpp b/layout/base/DisplayPortUtils.cpp
@@ -27,6 +27,7 @@
#include "nsLayoutUtils.h"
#include "nsPlaceholderFrame.h"
#include "nsSubDocumentFrame.h"
+#include "nsIFrameInlines.h"
namespace mozilla {
@@ -1024,8 +1025,8 @@ nsIFrame* DisplayPortUtils::OneStepInAsyncScrollableAncestorChain(
return nullptr;
}
nsIFrame* anchor = nullptr;
- while ((anchor =
- AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(aFrame))) {
+ while ((anchor = AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
+ aFrame, /* aBuilder */ nullptr))) {
aFrame = anchor;
}
if (aFrame->StyleDisplay()->mPosition == StylePositionProperty::Fixed &&
@@ -1051,6 +1052,7 @@ FrameAndASRKind DisplayPortUtils::GetASRAncestorFrame(
// document in the process can find some apzc, ASRs have no such need and that
// would be incorrect). This should be kept in sync with
// OneStepInAsyncScrollableAncestorChain, OneStepInASRChain,
+ // ShouldAsyncScrollWithAnchorNotCached,
// nsLayoutUtils::GetAsyncScrollableAncestorFrame.
for (nsIFrame* f = aFrameAndASRKind.mFrame; f;
@@ -1080,8 +1082,8 @@ FrameAndASRKind DisplayPortUtils::GetASRAncestorFrame(
// scrollable ancestor chain, but rather we are moving sideways. And when
// we exit this loop the last frame might be a sticky asr, after that we
// move up (the next iteration of the outer for loop).
- while (
- (anchor = AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(f))) {
+ while ((anchor = AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
+ f, aBuilder))) {
f = anchor;
}
@@ -1102,11 +1104,13 @@ FrameAndASRKind DisplayPortUtils::GetASRAncestorFrame(
}
FrameAndASRKind DisplayPortUtils::OneStepInASRChain(
- FrameAndASRKind aFrameAndASRKind,
+ FrameAndASRKind aFrameAndASRKind, nsDisplayListBuilder* aBuilder,
nsIFrame* aLimitAncestor /* = nullptr */) {
+ MOZ_ASSERT(aBuilder->IsPaintingToWindow());
// This has the same basic structure as GetASRAncestorFrame since they are
- // meant to be used together. So this should be kept in sync with
- // GetASRAncestorFrame. See that function for more comments about the
+ // meant to be used together. As well as ShouldAsyncScrollWithAnchor, so this
+ // should be kept in sync with GetASRAncestorFrame and
+ // ShouldAsyncScrollWithAnchor. See that function for more comments about the
// structure of this code.
if (aFrameAndASRKind.mFrame->IsMenuPopupFrame()) {
return FrameAndASRKind::default_value();
@@ -1114,8 +1118,8 @@ FrameAndASRKind DisplayPortUtils::OneStepInASRChain(
if (aFrameAndASRKind.mASRKind == ActiveScrolledRoot::ASRKind::Scroll) {
nsIFrame* frame = aFrameAndASRKind.mFrame;
nsIFrame* anchor = nullptr;
- while ((anchor =
- AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(frame))) {
+ while ((anchor = AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
+ frame, aBuilder))) {
MOZ_ASSERT_IF(
aLimitAncestor,
nsLayoutUtils::IsProperAncestorFrameConsideringContinuations(
@@ -1194,7 +1198,8 @@ const ActiveScrolledRoot* DisplayPortUtils::ActivateDisplayportOnASRAncestors(
// OneStepInASRChain on the anchored frame, but this saves
// GetAnchorThatFrameScrollsWith call that we've already done.)
FrameAndASRKind frameAndASRKind{aAnchor, ActiveScrolledRoot::ASRKind::Scroll};
- frameAndASRKind = OneStepInASRChain(frameAndASRKind, aLimitAncestor);
+ frameAndASRKind =
+ OneStepInASRChain(frameAndASRKind, aBuilder, aLimitAncestor);
while (frameAndASRKind.mFrame && frameAndASRKind.mFrame != aLimitAncestor &&
(!aLimitAncestor || frameAndASRKind.mFrame->FirstContinuation() !=
aLimitAncestor->FirstContinuation())) {
@@ -1235,7 +1240,8 @@ const ActiveScrolledRoot* DisplayPortUtils::ActivateDisplayportOnASRAncestors(
break;
}
- frameAndASRKind = OneStepInASRChain(frameAndASRKind, aLimitAncestor);
+ frameAndASRKind =
+ OneStepInASRChain(frameAndASRKind, aBuilder, aLimitAncestor);
}
const ActiveScrolledRoot* asr = aASRofLimitAncestor;
@@ -1246,9 +1252,10 @@ const ActiveScrolledRoot* DisplayPortUtils::ActivateDisplayportOnASRAncestors(
MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrameConsideringContinuations(
aLimitAncestor, asrFrame.mFrame));
- MOZ_ASSERT((asr ? FrameAndASRKind{asr->mFrame, asr->mKind}
- : FrameAndASRKind::default_value()) ==
- GetASRAncestorFrame(OneStepInASRChain(asrFrame), aBuilder));
+ MOZ_ASSERT(
+ (asr ? FrameAndASRKind{asr->mFrame, asr->mKind}
+ : FrameAndASRKind::default_value()) ==
+ GetASRAncestorFrame(OneStepInASRChain(asrFrame, aBuilder), aBuilder));
asr = (asrFrame.mASRKind == ActiveScrolledRoot::ASRKind::Scroll)
? aBuilder->GetOrCreateActiveScrolledRoot(
@@ -1260,4 +1267,144 @@ const ActiveScrolledRoot* DisplayPortUtils::ActivateDisplayportOnASRAncestors(
return asr;
}
+static bool CheckAxes(ScrollContainerFrame* aScrollFrame, PhysicalAxes aAxes) {
+ if (aAxes == kPhysicalAxesBoth) {
+ return true;
+ }
+ nsRect range = aScrollFrame->GetScrollRangeForUserInputEvents();
+ if (aAxes.contains(PhysicalAxis::Vertical)) {
+ MOZ_ASSERT(!aAxes.contains(PhysicalAxis::Horizontal));
+ if (range.width > 0) {
+ // compensating in vertical axis only, but scroll frame can scroll horz
+ return false;
+ }
+ }
+ if (aAxes.contains(PhysicalAxis::Horizontal)) {
+ MOZ_ASSERT(!aAxes.contains(PhysicalAxis::Vertical));
+ if (range.height > 0) {
+ // compensating in horizontal axis only, but scroll frame can scroll vert
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool CheckForScrollFrameAndAxes(nsIFrame* aFrame, PhysicalAxes aAxes,
+ bool* aOutSawPotentialASR) {
+ ScrollContainerFrame* scrollContainerFrame = do_QueryFrame(aFrame);
+ if (!scrollContainerFrame) {
+ return true;
+ }
+ *aOutSawPotentialASR = true;
+ return CheckAxes(scrollContainerFrame, aAxes);
+}
+
+// true is good
+static bool CheckForStickyAndAxes(nsIFrame* aFrame, PhysicalAxes aAxes,
+ bool* aOutSawPotentialASR) {
+ if (aFrame->StyleDisplay()->mPosition != StylePositionProperty::Sticky) {
+ return true;
+ }
+ auto* ssc = StickyScrollContainer::GetOrCreateForFrame(aFrame);
+ if (!ssc) {
+ return true;
+ }
+ *aOutSawPotentialASR = true;
+ return CheckAxes(ssc->ScrollContainer(), aAxes);
+}
+
+static bool ShouldAsyncScrollWithAnchorNotCached(nsIFrame* aFrame,
+ nsIFrame* aAnchor,
+ nsDisplayListBuilder* aBuilder,
+ PhysicalAxes aAxes) {
+ // This has the same basic structure as GetASRAncestorFrame and
+ // OneStepInASRChain. They should all be kept in sync.
+ if (aFrame->IsMenuPopupFrame()) {
+ return false;
+ }
+ nsIFrame* limitAncestor = aFrame->GetParent();
+ MOZ_ASSERT(limitAncestor);
+ // Start from aAnchor (not aFrame) so we don't infinite loop.
+ nsIFrame* frame = aAnchor;
+ bool firstIteration = true;
+ // We want to detect if we would assign an ASR to the anchored frame that is
+ // subject to a transform that the anchored frame is not actually under
+ // because doing so would give it the same spatial node and webrender would
+ // incorrectly render it with that transform. So we track when we first see a
+ // potential ASR and then start checking for transforms.
+ bool sawPotentialASR = false;
+ while (frame && !frame->IsMenuPopupFrame() && frame != limitAncestor &&
+ (frame->FirstContinuation() != limitAncestor->FirstContinuation())) {
+ // Note that we purposely check all scroll frames in this loop because we
+ // might not have activated scroll frames yet.
+
+ // On the first iteration we don't check the scroll frame because we don't
+ // scroll with the contents of aAnchor.
+ if (!firstIteration &&
+ !CheckForScrollFrameAndAxes(frame, aAxes, &sawPotentialASR)) {
+ return false;
+ }
+
+ // On the first iteration it's okay if the anchor is transformed, we won't
+ // get rendered as transformed if we take it's ASR (even if it's sticky
+ // pos).
+ if (sawPotentialASR && !firstIteration && frame->IsTransformed()) {
+ return false;
+ }
+
+ nsIFrame* anchor = nullptr;
+ while ((anchor = AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
+ frame, aBuilder))) {
+ MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrameConsideringContinuations(
+ limitAncestor, anchor));
+ frame = anchor;
+ // Any of these anchor chain frames are okay if they are transformed, they
+ // won't affect our ASR/spatial node (even the last one, even if it's
+ // sticky).
+ }
+
+ if (!CheckForStickyAndAxes(frame, aAxes, &sawPotentialASR)) {
+ return false;
+ }
+ // If sawPotentialASR flipped from false to true in the
+ // CheckForStickyAndAxes call we don't want to check if frame is transformed
+ // because its transform will not be applied to items with an ASR equal to
+ // {frame, sticky} because the transform is inside the sticky.
+
+ frame = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame);
+ firstIteration = false;
+ }
+ return true;
+}
+
+bool DisplayPortUtils::ShouldAsyncScrollWithAnchor(
+ nsIFrame* aFrame, nsIFrame* aAnchor, nsDisplayListBuilder* aBuilder,
+ PhysicalAxes aAxes) {
+ // Note that this does not recurse because we are passing aBuilder = nullptr,
+ // but we have to skip the asserts related to aBuilder.
+ MOZ_ASSERT(aAnchor ==
+ AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
+ aFrame, /* aBuilder */ nullptr, /* aSkipAsserts */ true));
+ MOZ_ASSERT(aFrame->IsAbsolutelyPositioned());
+ MOZ_ASSERT(aBuilder->IsPaintingToWindow());
+ MOZ_ASSERT(!aAxes.isEmpty());
+
+ // ShouldAsyncScrollWithAnchorNotCached can call recursively and modify
+ // AsyncScrollsWithAnchorHashmap, so we use this to only do one hashtable
+ // lookup and only call ShouldAsyncScrollWithAnchorNotCached if not already
+ // present in the hashtable.
+ bool wasPresent = true;
+ auto& entry = aBuilder->AsyncScrollsWithAnchorHashmap().LookupOrInsertWith(
+ aFrame, [&]() {
+ wasPresent = false;
+ return true;
+ });
+ if (!wasPresent) {
+ entry =
+ ShouldAsyncScrollWithAnchorNotCached(aFrame, aAnchor, aBuilder, aAxes);
+ }
+
+ return entry;
+}
+
} // namespace mozilla
diff --git a/layout/base/DisplayPortUtils.h b/layout/base/DisplayPortUtils.h
@@ -388,6 +388,7 @@ class DisplayPortUtils {
* aLimitAncestor.
*/
static FrameAndASRKind OneStepInASRChain(FrameAndASRKind aFrameAndASRKind,
+ nsDisplayListBuilder* aBuilder,
nsIFrame* aLimitAncestor = nullptr);
/**
@@ -406,6 +407,14 @@ class DisplayPortUtils {
nsIFrame* aAnchor, nsIFrame* aLimitAncestor,
const ActiveScrolledRoot* aASRofLimitAncestor,
nsDisplayListBuilder* aBuilder);
+
+ /**
+ * aFrame is an absolutely positioned frame that is anchor positioned and
+ * compensates for scroll in at least one axis.
+ */
+ static bool ShouldAsyncScrollWithAnchor(nsIFrame* aFrame, nsIFrame* aAnchor,
+ nsDisplayListBuilder* aBuilder,
+ PhysicalAxes aAxes);
};
} // namespace mozilla
diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp
@@ -1327,8 +1327,8 @@ static nsIFrame* GetNearestScrollableOrOverflowClipFrame(
// This should be kept in sync with
// DisplayPortUtils::OneStepInAsyncScrollableAncestorChain,
- // DisplayPortUtils::OneStepInASRChain, and
- // DisplayPortUtils::GetASRAncestorFrame.
+ // DisplayPortUtils::OneStepInASRChain, DisplayPortUtils::GetASRAncestorFrame,
+ // and ShouldAsyncScrollWithAnchorNotCached.
for (nsIFrame* f = aFrame; f; f = GetNextFrame(f)) {
if (aClipFrameCheck && aClipFrameCheck(f)) {
return f;
@@ -1384,8 +1384,8 @@ static nsIFrame* GetNearestScrollableOrOverflowClipFrame(
// via the special fixed pos behaviour below or the next iteration of the
// outer for loop.
if (aFlags & nsLayoutUtils::SCROLLABLE_ONLY_ASYNC_SCROLLABLE) {
- while (
- (anchor = AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(f))) {
+ while ((anchor = AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
+ f, /* aBuilder */ nullptr))) {
f = anchor;
}
}
diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp
@@ -4452,8 +4452,8 @@ void nsIFrame::BuildDisplayListForChild(nsDisplayListBuilder* aBuilder,
// frame's BuildDisplayList, so don't bother to async scroll with an
// anchor in that case. Bug 2001861 tracks removing this check.
!PresContext()->Document()->GetActiveViewTransition()) {
- scrollsWithAnchor =
- AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(child);
+ scrollsWithAnchor = AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
+ child, aBuilder);
if (scrollsWithAnchor && aBuilder->IsRetainingDisplayList()) {
if (aBuilder->IsPartialUpdate()) {
diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp
@@ -872,6 +872,7 @@ void nsDisplayListBuilder::EndFrame() {
mActiveScrolledRoots.Clear();
FreeClipChains();
FreeTemporaryItems();
+ mAsyncScrollsWithAnchor.Clear();
nsCSSRendering::EndFrameTreesLocked();
}
diff --git a/layout/painting/nsDisplayList.h b/layout/painting/nsDisplayList.h
@@ -1848,6 +1848,11 @@ class nsDisplayListBuilder {
void SetIsDestroying() { mIsDestroying = true; }
bool IsDestroying() const { return mIsDestroying; }
+ nsTHashMap<nsPtrHashKey<const nsIFrame>, bool>&
+ AsyncScrollsWithAnchorHashmap() {
+ return mAsyncScrollsWithAnchor;
+ }
+
private:
bool MarkOutOfFlowFrameForDisplay(nsIFrame* aDirtyFrame, nsIFrame* aFrame,
const nsRect& aVisibleRect,
@@ -1995,6 +2000,12 @@ class nsDisplayListBuilder {
Preserves3DContext mPreserves3DCtx;
+ // For frames which are anchored, and compensate for scroll (according to the
+ // spec definition), whether the frame should async scroll with the anchor. It
+ // might be disabled for things that are limitations of our current
+ // implementation (one-axis only, transforms).
+ nsTHashMap<nsPtrHashKey<const nsIFrame>, bool> mAsyncScrollsWithAnchor;
+
uint8_t mBuildingPageNum = 0;
nsDisplayListBuilderMode mMode;
diff --git a/layout/reftests/async-scrolling/anchor-pos-one-axis-1-ref.html b/layout/reftests/async-scrolling/anchor-pos-one-axis-1-ref.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<style>
+.anchored {
+ position: absolute;
+ width: 50px;
+ height: 50px;
+ background: yellow;
+ left: 50px;
+ top: 50px;
+}
+</style>
+<div class="anchored"></div>
+</html>
diff --git a/layout/reftests/async-scrolling/anchor-pos-one-axis-1.html b/layout/reftests/async-scrolling/anchor-pos-one-axis-1.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html reftest-async-scroll>
+<style>
+.container {
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+.scroller {
+ position: absolute;
+ overflow: auto;
+ scrollbar-width: none;
+ width: 100px;
+ height: 100px;
+}
+.spacer {
+ width: 1px;
+ height: 500px;
+}
+.anchor {
+ width: 50px;
+ height: 50px;
+ background: blue;
+ anchor-name: --my-anchor;
+}
+.anchored {
+ position: absolute;
+ position-anchor: --my-anchor;
+ position-visibility: always;
+ width: 50px;
+ height: 50px;
+ background: yellow;
+ left: anchor(right);
+ top: 50px;
+}
+</style>
+<div class="container">
+ <div class="scroller"
+ reftest-displayport-x="0" reftest-displayport-y="0"
+ reftest-displayport-w="100" reftest-displayport-h="500"
+ reftest-async-scroll-y="50">
+ <div class="anchor"></div>
+ <div class="spacer"></div>
+ </div>
+ <div class="anchored"></div>
+</div>
+</html>
diff --git a/layout/reftests/async-scrolling/reftest.list b/layout/reftests/async-scrolling/reftest.list
@@ -196,3 +196,4 @@ pref(layout.css.anchor-positioning.enabled,true) == anchor-pos-sticky-1.html anc
pref(layout.css.anchor-positioning.enabled,true) == anchor-pos-sticky-2.html anchor-pos-sticky-2-ref.html
pref(layout.css.anchor-positioning.enabled,true) == anchor-pos-sticky-3.html anchor-pos-sticky-3-ref.html
pref(layout.css.anchor-positioning.enabled,true) == anchor-pos-sticky-4.html anchor-pos-sticky-4-ref.html
+pref(layout.css.anchor-positioning.enabled,true) == anchor-pos-one-axis-1.html anchor-pos-one-axis-1-ref.html
diff --git a/layout/reftests/transform/anchor-pos-transform-01-ref.html b/layout/reftests/transform/anchor-pos-transform-01-ref.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<style>
+.anchored {
+ position: absolute;
+ width: 50px;
+ height: 50px;
+ background: yellow;
+ left: 50px;
+ top: 50px;
+}
+</style>
+<div class="anchored"></div>
+</html>
diff --git a/layout/reftests/transform/anchor-pos-transform-01.html b/layout/reftests/transform/anchor-pos-transform-01.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<style>
+.container {
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+.scroller {
+ position: absolute;
+ overflow: auto;
+ scrollbar-width: none;
+ width: 100px;
+ height: 100px;
+ transform: rotate(-40deg);
+ transform-origin: top left;
+}
+.spacer {
+ width: 1px;
+ height: 500px;
+}
+.anchor {
+ width: 50px;
+ height: 50px;
+ anchor-name: --my-anchor;
+}
+.anchored {
+ position: absolute;
+ position-anchor: --my-anchor;
+ position-visibility: always;
+ width: 50px;
+ height: 50px;
+ background: yellow;
+ left: anchor(right);
+ top: anchor(bottom);
+}
+</style>
+<div class="container">
+ <div class="scroller">
+ <div class="anchor"></div>
+ <div class="spacer"></div>
+ </div>
+ <div class="anchored"></div>
+</div>
+</html>
diff --git a/layout/reftests/transform/reftest.list b/layout/reftests/transform/reftest.list
@@ -178,3 +178,5 @@ skip test-pref(layout.animation.prerender.partial,true) test-pref(layout.animati
test-pref(layout.animation.prerender.partial.jank,true) test-pref(layout.animation.prerender.partial,true) test-pref(layout.animation.prerender.viewport-ratio-limit,"1.125") fuzzy-if(nogpu||Android,0-255,0-400) == partial-prerender-in-svg-1.html partial-prerender-in-svg-1-ref.html
test-pref(layout.animation.prerender.partial.jank,true) test-pref(layout.animation.prerender.partial,true) test-pref(layout.animation.prerender.viewport-ratio-limit,"1.125") fuzzy-if(nogpu,0-255,0-400) == partial-prerender-in-svg-2.html partial-prerender-in-svg-1-ref.html # Reuse partial-prerender-in-svg-1-ref.html since the result should look same as partial-prerender-in-svg-1.html
test-pref(layout.animation.prerender.partial.jank,true) test-pref(layout.animation.prerender.partial,true) test-pref(layout.animation.prerender.viewport-ratio-limit,"1.125") fuzzy(0-62,0-400) fuzzy-if(nogpu||Android,0-255,0-2000) == partial-prerender-in-svg-3.html partial-prerender-in-svg-3-ref.html
+
+pref(layout.css.anchor-positioning.enabled,true) == anchor-pos-transform-01.html anchor-pos-transform-01-ref.html