Bug 1621007 - Rewrite the checking for PrerenderDecision and reset mAllowAsyncAnimation if needed in 3d context. r=mattwoodrow

The basic idea is:
  In the same 3d context (note: we use AutoPreserves3DContext to keep
  this info), and we block the async animations of the ancestors of the ones
  who don't prerender. Of course, and its later silbing frames and child
  frames.

In order to to this, we keep a flag in Preserves3DContext, and set it
to false if the decision is No prerender, so we could force to block
async animations on its ancestors and descendants (and later silbings).

Differential Revision: https://phabricator.services.mozilla.com/D67686

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Boris Chiou 2020-03-27 18:47:16 +00:00
parent 3c7aa1ba1c
commit 2eaf9ee039
7 changed files with 191 additions and 34 deletions

View File

@ -1377,7 +1377,7 @@ promise_test(async t => {
promise_test(async t => {
const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
const targetA = addDiv(t, { style: 'transform-style: preserve-3d' });
const targetB = addDiv(t, { style: 'transform-style: preserve-3d '});
const targetB = addDiv(t, { style: 'transform-style: preserve-3d' });
const targetC = addDiv(t);
container.appendChild(targetA);
targetA.append(targetB);
@ -1402,5 +1402,93 @@ promise_test(async t => {
}, 'Transform-like animations in the 3d rendering context should runs on the ' +
'compositor');
promise_test(async t => {
const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
const target = addDiv(t, { style: 'transform-style: preserve-3d;' });
const innerA = addDiv(t, { style: 'width: 50px; height: 50px;' });
// The frame of innerB is too large, so this makes its ancenstors and children
// in the 3d context be not allowed the async animations.
const innerB = addDiv(t, { style: 'rotate: 0 1 1 100deg; ' +
'transform-style: preserve-3d; ' +
'text-indent: -9999em' });
const innerB2 = addDiv(t, { style: 'rotate: 0 1 1 45deg;' });
const innerBText = document.createTextNode("innerB");
container.appendChild(target);
target.appendChild(innerA);
target.appendChild(innerB);
innerB.appendChild(innerBText);
innerB.appendChild(innerB2);
const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
100 * MS_PER_SEC);
const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
100 * MS_PER_SEC);
const animation3 = innerB2.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] },
100 * MS_PER_SEC);
await waitForAnimationReadyToRestyle(animation1);
await waitForAnimationReadyToRestyle(animation2);
await waitForAnimationReadyToRestyle(animation3);
await waitForPaints();
assert_animation_is_not_running_on_compositor(animation1,
'rotate animation in the 3d rendering context should not be running on ' +
'the compositor because one of its inner frames is too large');
assert_animation_is_running_on_compositor(animation2,
'rotate animation in the 3d rendering context is still running on ' +
'the compositor because its display item is created earlier');
assert_animation_is_not_running_on_compositor(animation3,
'rotate animation in the 3d rendering context should not be running on ' +
'the compositor because one of its parent frames is too large');
innerBText.remove();
}, 'Transform-like animations in the 3d rendering context should not runs on ' +
'the compositor if it is the ancestor of ones whose frames are too large ' +
'or its ancestor is not allowed to run on the compositor');
promise_test(async t => {
const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
const target = addDiv(t, { style: 'transform-style: preserve-3d;' });
// The frame of innerA is too large, so this makes its ancenstors and children
// in the 3d context be not allowed the async animations.
const innerA = addDiv(t, { style: 'transform-style: preserve-3d; ' +
'text-indent: -9999em' });
const innerAText = document.createTextNode("innerA");
const innerB = addDiv(t, { style: 'width: 50px; height: 50px;' });
container.appendChild(target);
target.appendChild(innerA);
target.appendChild(innerB);
innerA.appendChild(innerAText);
const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
100 * MS_PER_SEC);
const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
100 * MS_PER_SEC);
const animation3 = innerB.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] },
100 * MS_PER_SEC);
await waitForAnimationReadyToRestyle(animation1);
await waitForAnimationReadyToRestyle(animation2);
await waitForAnimationReadyToRestyle(animation3);
await waitForPaints();
assert_animation_is_not_running_on_compositor(animation1,
'rotate animation in the 3d rendering context should not be running on ' +
'the compositor because one of its inner frames is too large');
assert_animation_is_not_running_on_compositor(animation2,
'rotate animation in the 3d rendering context should not be running on ' +
'the compositor because its frame size is too large');
assert_animation_is_not_running_on_compositor(animation3,
'rotate animation in the 3d rendering context should not be running on ' +
'the compositor because its previous sibling frame is too large');
innerAText.remove();
}, 'Transform-like animations in the 3d rendering context should not runs on ' +
'the compositor if its previous sibling frame size is too large');
</script>
</body>

View File

@ -3210,10 +3210,11 @@ void nsIFrame::BuildDisplayListForStackingContext(
bool allowAsyncAnimation = false;
bool inTransform = aBuilder->IsInTransform();
if (isTransformed) {
nsDisplayTransform::PrerenderDecision decision =
nsDisplayTransform::PrerenderInfo decision =
nsDisplayTransform::ShouldPrerenderTransformedContent(aBuilder, this,
&dirtyRect);
switch (decision) {
switch (decision.mDecision) {
case nsDisplayTransform::PrerenderDecision::Full:
allowAsyncAnimation = true;
visibleRect = dirtyRect;
@ -3224,6 +3225,15 @@ void nsIFrame::BuildDisplayListForStackingContext(
[[fallthrough]];
// fall through to the PrerenderDecision::No case
case nsDisplayTransform::PrerenderDecision::No: {
// If we didn't prerender an animated frame in a preserve-3d context,
// then we want disable async animations for the rest of the preserve-3d
// (especially ancestors).
if ((extend3DContext || combines3DTransformWithAncestors) &&
decision.mDecision == nsDisplayTransform::PrerenderDecision::No &&
decision.mHasAnimations) {
aBuilder->SavePreserves3DAllowAsyncAnimation(false);
}
const nsRect overflow = GetVisualOverflowRectRelativeToSelf();
if (overflow.IsEmpty() && !extend3DContext) {
return;
@ -3231,8 +3241,6 @@ void nsIFrame::BuildDisplayListForStackingContext(
// If we're in preserve-3d then grab the dirty rect that was given to
// the root and transform using the combined transform.
// FIXME: Could we remove this after making transform animations with
// preserve-3d run on the compositor?
if (combines3DTransformWithAncestors) {
visibleRect = dirtyRect = aBuilder->GetPreserves3DRect();
}
@ -3703,6 +3711,22 @@ void nsIFrame::BuildDisplayListForStackingContext(
buildingDisplayList.SetReferenceFrameAndCurrentOffset(
outerReferenceFrame, GetOffsetToCrossDoc(outerReferenceFrame));
// We would like to block async animations for ancestors of ones not
// prerendered in the preserve-3d tree. Now that we've finished processing
// all descendants, update allowAsyncAnimation to take their prerender
// state into account
// FIXME: We don't block async animations for previous siblings because
// their prerender decisions have been made. We may have to figure out a
// better way to rollback their prerender decisions.
// Alternatively we could not block animations for later siblings, and only
// block them for ancestors of a blocked one.
if ((extend3DContext || combines3DTransformWithAncestors) &&
allowAsyncAnimation) {
// aBuilder->GetPreserves3DAllowAsyncAnimation() means the inner or
// previous silbing frames are allowed/disallowed for async animations.
allowAsyncAnimation = aBuilder->GetPreserves3DAllowAsyncAnimation();
}
nsDisplayTransform* transformItem = MakeDisplayItem<nsDisplayTransform>(
aBuilder, this, &resultList, visibleRect, 0, allowAsyncAnimation);
if (transformItem) {

View File

@ -8293,7 +8293,16 @@ bool nsDisplayBackgroundColor::CanUseAsyncAnimations(
/* static */
auto nsDisplayTransform::ShouldPrerenderTransformedContent(
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nsRect* aDirtyRect)
-> PrerenderDecision {
-> PrerenderInfo {
PrerenderInfo result;
// If we are in a preserve-3d tree, and we've disallowed async animations, we
// return No prerender decision directly.
if ((aFrame->Extend3DContext() ||
aFrame->Combines3DTransformWithAncestors()) &&
!aBuilder->GetPreserves3DAllowAsyncAnimation()) {
return result;
}
// Elements whose transform has been modified recently, or which
// have a compositor-animated transform, can be prerendered. An element
// might have only just had its transform animated in which case
@ -8308,7 +8317,13 @@ auto nsDisplayTransform::ShouldPrerenderTransformedContent(
AnimationPerformanceWarning(
AnimationPerformanceWarning::Type::TransformFrameInactive));
return PrerenderDecision::No;
// This case happens when we're sure that the frame is not animated and its
// preserve-3d ancestors are not, either. So we don't need to pre-render.
// However, this decision shouldn't affect the decisions for other frames in
// the preserve-3d context. We need this flag to determine whether we should
// block async animations on other frames in the current preserve-3d tree.
result.mHasAnimations = false;
return result;
}
// We should not allow prerender if any ancestor container element has
@ -8334,7 +8349,7 @@ auto nsDisplayTransform::ShouldPrerenderTransformedContent(
container = nsLayoutUtils::GetCrossDocParentFrame(container)) {
const nsStyleSVGReset* svgReset = container->StyleSVGReset();
if (svgReset->HasMask() || svgReset->HasClipPath()) {
return PrerenderDecision::No;
return result;
}
}
@ -8342,7 +8357,8 @@ auto nsDisplayTransform::ShouldPrerenderTransformedContent(
// we are already rendering the entire content.
nsRect overflow = aFrame->GetVisualOverflowRectRelativeToSelf();
if (aDirtyRect->Contains(overflow)) {
return PrerenderDecision::Full;
result.mDecision = PrerenderDecision::Full;
return result;
}
float viewportRatioX =
@ -8376,29 +8392,15 @@ auto nsDisplayTransform::ShouldPrerenderTransformedContent(
uint64_t frameArea = uint64_t(frameSize.width) * frameSize.height;
if (frameArea <= maxLimitArea && frameSize <= absoluteLimit) {
*aDirtyRect = overflow;
return PrerenderDecision::Full;
}
// Frames participating any preserve-3d rendering context are force to
// full prerender here if we failed the previous if-conditions.
// If the current frame doesn't have animations and any of its parent has
// animations, we still hit this path because
// ActiveLayerTracker::IsTransformMaybeAnimated() catches the existing
// animations on the parent frames in the preserve-3d rendering context.
// FIXME: For now, we force all the frame in the 3D rendering context to use
// FullPrerender, so this may have an issue if one of the frame is too large
// and it has animations (which may make the calculation of visible and dirty
// rects not correct.).
if (aFrame->Extend3DContext() || aFrame->Combines3DTransformWithAncestors()) {
// Use overflow as dirty and force to use full prerender.
*aDirtyRect = overflow;
return PrerenderDecision::Full;
result.mDecision = PrerenderDecision::Full;
return result;
}
if (StaticPrefs::layout_animation_prerender_partial()) {
*aDirtyRect = nsLayoutUtils::ComputePartialPrerenderArea(*aDirtyRect,
overflow, maxSize);
return PrerenderDecision::Partial;
result.mDecision = PrerenderDecision::Partial;
return result;
}
if (frameArea > maxLimitArea) {
@ -8426,7 +8428,7 @@ auto nsDisplayTransform::ShouldPrerenderTransformedContent(
}));
}
return PrerenderDecision::No;
return result;
}
/* If the matrix is singular, or a hidden backface is shown, the frame won't be

View File

@ -402,13 +402,15 @@ class nsDisplayListBuilder {
public:
typedef mozilla::gfx::Matrix4x4 Matrix4x4;
Preserves3DContext() : mAccumulatedRectLevels(0) {}
Preserves3DContext()
: mAccumulatedRectLevels(0), mAllowAsyncAnimation(true) {}
Preserves3DContext(const Preserves3DContext& aOther)
: mAccumulatedTransform(),
mAccumulatedRect(),
mAccumulatedRectLevels(0),
mVisibleRect(aOther.mVisibleRect) {}
mVisibleRect(aOther.mVisibleRect),
mAllowAsyncAnimation(aOther.mAllowAsyncAnimation) {}
// Accmulate transforms of ancestors on the preserves-3d chain.
Matrix4x4 mAccumulatedTransform;
@ -417,6 +419,8 @@ class nsDisplayListBuilder {
// How far this frame is from the root of the current 3d context.
int mAccumulatedRectLevels;
nsRect mVisibleRect;
// Allow async animation for this 3D context.
bool mAllowAsyncAnimation;
};
/**
@ -1716,6 +1720,14 @@ class nsDisplayListBuilder {
void SavePreserves3DRect() { mPreserves3DCtx.mVisibleRect = mVisibleRect; }
void SavePreserves3DAllowAsyncAnimation(bool aValue) {
mPreserves3DCtx.mAllowAsyncAnimation = aValue;
}
bool GetPreserves3DAllowAsyncAnimation() const {
return mPreserves3DCtx.mAllowAsyncAnimation;
}
bool IsBuildingInvisibleItems() const { return mBuildingInvisibleItems; }
void SetBuildingInvisibleItems(bool aBuildingInvisibleItems) {
@ -7105,16 +7117,25 @@ class nsDisplayTransform : public nsDisplayHitTestInfoBase {
static Matrix4x4 GetResultingTransformMatrix(
const FrameTransformProperties& aProperties, TransformReferenceBox&,
const nsPoint& aOrigin, float aAppUnitsPerPixel, uint32_t aFlags);
struct PrerenderInfo {
PrerenderDecision mDecision = PrerenderDecision::No;
bool mHasAnimations = true;
};
/**
* Decide whether we should prerender some or all of the contents of the
* transformed frame even when it's not completely visible (yet).
* Return FullPrerender if the entire contents should be prerendered,
* PartialPrerender if some but not all of the contents should be prerendered,
* or NoPrerender if only the visible area should be rendered.
* Return PrerenderDecision::Full if the entire contents should be
* prerendered, PrerenderDecision::Partial if some but not all of the
* contents should be prerendered, or PrerenderDecision::No if only the
* visible area should be rendered.
* |mNoAffectDecisionInPreserve3D| is set if the prerender decision should not
* affect the decision on other frames in the preserve 3d tree.
* |aDirtyRect| is updated to the area that should be prerendered.
*/
static PrerenderDecision ShouldPrerenderTransformedContent(
static PrerenderInfo ShouldPrerenderTransformedContent(
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nsRect* aDirtyRect);
bool CanUseAsyncAnimations(nsDisplayListBuilder* aBuilder) override;
bool MayBeAnimated(nsDisplayListBuilder* aBuilder,

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<style>
.transform {
will-change: transform;
transform-style: preserve-3d;
}
</style>
<body>
<div class="transform">
<div>TEST PAGE</div>
<div style="text-indent: -9999em">TEST PAGE</div>
</div>
</body>

View File

@ -0,0 +1,7 @@
<!DOCTYPE html>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<body>
<div style="transform-style: preserve-3d">
<div>TEST PAGE</div>
</div>
</body>

View File

@ -96,3 +96,4 @@ fuzzy-if(winWidget,0-150,0-120) == component-alpha-1.html component-alpha-1-ref.
!= preserve3d-scale.html about:blank
fuzzy-if(webrender,0-1,0-5) == perspective-overflow-1.html perspective-overflow-1-ref.html
== 1544995-1.html 1544995-1-ref.html
== preserve3d-will-change-large-frame.html preserve3d-will-change-ref.html