Bug 1164227 - Don't allow invalid region simplification to invalidate unchanged scrolled contents. r=roc

This commit is contained in:
Markus Stange 2015-05-13 17:50:45 -04:00
parent a022f9015f
commit e9a8ee958d
6 changed files with 303 additions and 13 deletions

View File

@ -1295,6 +1295,16 @@ public:
nsPoint mLastAnimatedGeometryRootOrigin;
nsPoint mAnimatedGeometryRootOrigin;
// If mIgnoreInvalidationsOutsideRect is set, this contains the bounds of the
// layer's old visible region, in layer pixels.
nsIntRect mOldVisibleBounds;
// If set, invalidations that fall outside of this rect should not result in
// calls to layer->InvalidateRegion during DLBI. Instead, the parts outside
// this rectangle will be invalidated in InvalidateVisibleBoundsChangesForScrolledLayer.
// See the comment in ComputeAndSetIgnoreInvalidationRect for more information.
Maybe<nsIntRect> mIgnoreInvalidationsOutsideRect;
nsRefPtr<ColorLayer> mColorLayer;
nsRefPtr<ImageLayer> mImageLayer;
};
@ -1471,23 +1481,29 @@ AppendToString(nsACString& s, const nsIntRegion& r,
* *after* aTranslation has been applied, so we need to
* apply the inverse of that transform before calling InvalidateRegion.
*/
static void
InvalidatePostTransformRegion(PaintedLayer* aLayer, const nsIntRegion& aRegion,
const nsIntPoint& aTranslation)
template<typename RegionOrRect> void
InvalidatePostTransformRegion(PaintedLayer* aLayer, const RegionOrRect& aRegion,
const nsIntPoint& aTranslation,
PaintedDisplayItemLayerUserData* aData)
{
// Convert the region from the coordinates of the container layer
// (relative to the snapped top-left of the display list reference frame)
// to the PaintedLayer's own coordinates
nsIntRegion rgn = aRegion;
RegionOrRect rgn = aRegion;
rgn.MoveBy(-aTranslation);
aLayer->InvalidateRegion(rgn);
#ifdef MOZ_DUMP_PAINTING
if (nsLayoutUtils::InvalidationDebuggingIsEnabled()) {
nsAutoCString str;
AppendToString(str, rgn);
printf_stderr("Invalidating layer %p: %s\n", aLayer, str.get());
if (aData->mIgnoreInvalidationsOutsideRect) {
rgn = rgn.Intersect(*aData->mIgnoreInvalidationsOutsideRect);
}
if (!rgn.IsEmpty()) {
aLayer->InvalidateRegion(rgn);
#ifdef MOZ_DUMP_PAINTING
if (nsLayoutUtils::InvalidationDebuggingIsEnabled()) {
nsAutoCString str;
AppendToString(str, rgn);
printf_stderr("Invalidating layer %p: %s\n", aLayer, str.get());
}
#endif
}
}
static void
@ -1501,7 +1517,7 @@ InvalidatePostTransformRegion(PaintedLayer* aLayer, const nsRect& aRect,
nsRect rect = aClip.ApplyNonRoundedIntersection(aRect);
nsIntRect pixelRect = rect.ScaleToOutsidePixels(data->mXScale, data->mYScale, data->mAppUnitsPerDevPixel);
InvalidatePostTransformRegion(aLayer, pixelRect, aTranslation);
InvalidatePostTransformRegion(aLayer, pixelRect, aTranslation, data);
}
@ -2106,6 +2122,89 @@ ContainerState::RecyclePaintedLayer(PaintedLayer* aLayer,
return data;
}
static void
ComputeAndSetIgnoreInvalidationRect(PaintedLayer* aLayer,
PaintedDisplayItemLayerUserData* aData,
const nsIFrame* aAnimatedGeometryRoot,
nsDisplayListBuilder* aBuilder,
const nsIntPoint& aLayerTranslation)
{
if (!aLayer->Manager()->IsWidgetLayerManager()) {
// This optimization is only useful for layers with retained content.
return;
}
const nsIFrame* parentFrame = aAnimatedGeometryRoot->GetParent();
// GetDirtyRectForScrolledContents will return an empty rect if parentFrame
// is not a scrollable frame.
nsRect dirtyRect = aBuilder->GetDirtyRectForScrolledContents(parentFrame);
if (dirtyRect.IsEmpty()) {
// parentFrame is not a scrollable frame, or we didn't encounter it during
// display list building (though this shouldn't happen), or it's empty.
// In all those cases this optimization is not needed.
return;
}
// parentFrame is a scrollable frame, and aLayer contains the scrolled
// contents of that frame.
// maxNewVisibleBounds is a conservative approximation of the new visible
// region of aLayer.
nsIntRect maxNewVisibleBounds =
dirtyRect.ScaleToOutsidePixels(aData->mXScale, aData->mYScale,
aData->mAppUnitsPerDevPixel) - aLayerTranslation;
aData->mOldVisibleBounds = aLayer->GetValidRegion().GetBounds();
// When the visible region of aLayer changes (e.g. due to scrolling),
// three distinct types of invalidations need to be triggered:
// (1) Items (or parts of items) that have left the visible region need
// to be invalidated so that the pixels they painted are no longer
// part of the layer's valid region.
// (2) Items (or parts of items) that weren't in the old visible region
// but are in the new visible region need to be invalidated. This
// invalidation isn't required for painting the right layer
// contents, because these items weren't part of the layer's valid
// region, so they'd be painted anyway. It is, however, necessary in
// order to get an accurate invalid region for the layer tree that
// aLayer is in, for example for partial compositing.
// (3) Any changes that happened in the intersection of the old and the
// new visible region need to be invalidated. There shouldn't be any
// of these when scrolling static content.
//
// We'd like to guarantee that we won't invalidate anything in the
// intersection area of the old and the new visible region if all
// invalidation are of type (1) and (2). However, if we just call
// aLayer->InvalidateRegion for the invalidations of type (1) and (2),
// at some point we'll hit the complexity limit of the layer's invalid
// region. And the resulting region simplification can cause the region
// to intersect with the intersection of the old and the new visible
// region.
// In order to get around this problem, we're using the following approach:
// - aData->mIgnoreInvalidationsOutsideRect is set to a conservative
// approximation of the intersection of the old and the new visible
// region. At this point we don't know the layer's new visible region.
// - As long as we don't know the layer's new visible region, we ignore all
// invalidations outside that rectangle, so roughly some of the
// invalidations of type (1) and (2).
// - Once we know the layer's new visible region, which happens at some
// point during PostprocessRetainedLayers, we invalidate a conservative
// approximation of (1) and (2). Specifically, we invalidate the region
// union of the old visible bounds and the new visible bounds, minus
// aData->mIgnoreInvalidationsOutsideRect. That region is simple enough
// that it will never be simplified on its own.
// We unset mIgnoreInvalidationsOutsideRect at this point.
// - Any other invalidations that happen on the layer after this point, e.g.
// during WillEndTransaction, will just happen regularly. If they are of
// type (1) or (2), they won't change the layer's invalid region because
// they fall inside the region we invalidated in the previous step.
// Consequently, aData->mIgnoreInvalidationsOutsideRect is safe from
// invalidations as long as there are no invalidations of type (3).
aData->mIgnoreInvalidationsOutsideRect =
Some(maxNewVisibleBounds.Intersect(aData->mOldVisibleBounds));
}
void
ContainerState::PreparePaintedLayerForUse(PaintedLayer* aLayer,
PaintedDisplayItemLayerUserData* aData,
@ -2139,6 +2238,8 @@ ContainerState::PreparePaintedLayerForUse(PaintedLayer* aLayer,
Matrix matrix = Matrix::Translation(pixOffset.x, pixOffset.y);
aLayer->SetBaseTransform(Matrix4x4::From2D(matrix));
ComputeAndSetIgnoreInvalidationRect(aLayer, aData, aAnimatedGeometryRoot, mBuilder, pixOffset);
// FIXME: Temporary workaround for bug 681192 and bug 724786.
#ifndef MOZ_WIDGET_ANDROID
// Calculate exact position of the top-left of the active scrolled root.
@ -3948,7 +4049,8 @@ FrameLayerBuilder::ComputeGeometryChangeForItem(DisplayItemData* aData)
}
InvalidatePostTransformRegion(paintedLayer,
combined.ScaleToOutsidePixels(layerData->mXScale, layerData->mYScale, layerData->mAppUnitsPerDevPixel),
layerData->mTranslation);
layerData->mTranslation,
layerData);
}
aData->EndUpdate(geometry);
@ -4066,7 +4168,8 @@ FrameLayerBuilder::AddPaintedDisplayItem(PaintedLayerData* aLayerData,
}
InvalidatePostTransformRegion(layer, invalid,
GetTranslationForPaintedLayer(layer));
GetTranslationForPaintedLayer(layer),
paintedData);
}
}
ClippedDisplayItem* cdi =
@ -4288,6 +4391,39 @@ ContainerState::SetupScrollingMetadata(NewLayerEntry* aEntry)
aEntry->mLayer->SetFrameMetrics(metricsArray);
}
static void
InvalidateVisibleBoundsChangesForScrolledLayer(PaintedLayer* aLayer)
{
PaintedDisplayItemLayerUserData* data =
static_cast<PaintedDisplayItemLayerUserData*>(aLayer->GetUserData(&gPaintedDisplayItemLayerUserData));
if (data->mIgnoreInvalidationsOutsideRect) {
// We haven't invalidated anything outside *data->mIgnoreInvalidationsOutsideRect
// during DLBI. Now is the right time to do that, because at this point aLayer
// knows its new visible region.
// We use the visible regions' bounds here (as opposed to the true region)
// in order to limit rgn's complexity. The only possible disadvantage of
// this is that it might cause us to unnecessarily recomposite parts of the
// window that are in the visible region's bounds but not in the visible
// region itself, but that is acceptable for scrolled layers.
nsIntRegion rgn;
rgn.Or(data->mOldVisibleBounds, aLayer->GetVisibleRegion().GetBounds());
rgn.Sub(rgn, *data->mIgnoreInvalidationsOutsideRect);
if (!rgn.IsEmpty()) {
aLayer->InvalidateRegion(rgn);
#ifdef MOZ_DUMP_PAINTING
if (nsLayoutUtils::InvalidationDebuggingIsEnabled()) {
printf_stderr("Invalidating changes of the visible region bounds of the scrolled contents\n");
nsAutoCString str;
AppendToString(str, rgn);
printf_stderr("Invalidating layer %p: %s\n", aLayer, str.get());
}
#endif
}
data->mIgnoreInvalidationsOutsideRect = Nothing();
}
}
void
ContainerState::PostprocessRetainedLayers(nsIntRegion* aOpaqueRegionForContainer)
{
@ -4326,6 +4462,11 @@ ContainerState::PostprocessRetainedLayers(nsIntRegion* aOpaqueRegionForContainer
SetOuterVisibleRegionForLayer(e->mLayer, e->mVisibleRegion,
e->mLayerContentsVisibleRect.width >= 0 ? &e->mLayerContentsVisibleRect : nullptr);
PaintedLayer* p = e->mLayer->AsPaintedLayer();
if (p) {
InvalidateVisibleBoundsChangesForScrolledLayer(p);
}
if (!e->mOpaqueRegion.IsEmpty()) {
const nsIFrame* animatedGeometryRootToCover = animatedGeometryRootForOpaqueness;
if (e->mOpaqueForAnimatedGeometryRootParent &&

View File

@ -1257,6 +1257,24 @@ nsDisplayListBuilder::AppendNewScrollInfoItemForHoisting(nsDisplayScrollInfoLaye
mScrollInfoItemsForHoisting->AppendNewToTop(aScrollInfoItem);
}
void
nsDisplayListBuilder::StoreDirtyRectForScrolledContents(const nsIFrame* aScrollableFrame,
const nsRect& aDirty)
{
mDirtyRectForScrolledContents.Put(const_cast<nsIFrame*>(aScrollableFrame),
aDirty + ToReferenceFrame(aScrollableFrame));
}
nsRect
nsDisplayListBuilder::GetDirtyRectForScrolledContents(const nsIFrame* aScrollableFrame) const
{
nsRect result;
if (!mDirtyRectForScrolledContents.Get(const_cast<nsIFrame*>(aScrollableFrame), &result)) {
return nsRect();
}
return result;
}
void nsDisplayListSet::MoveTo(const nsDisplayListSet& aDestination) const
{
aDestination.BorderBackground()->AppendToTop(BorderBackground());

View File

@ -854,6 +854,22 @@ public:
void AppendNewScrollInfoItemForHoisting(nsDisplayScrollInfoLayer* aScrollInfoItem);
/**
* Store the dirty rect of the scrolled contents of aScrollableFrame. This
* is a bound for the extents of the new visible region of the scrolled
* layer.
* @param aScrollableFrame the scrollable frame
* @param aDirty the dirty rect, relative to aScrollableFrame
*/
void StoreDirtyRectForScrolledContents(const nsIFrame* aScrollableFrame, const nsRect& aDirty);
/**
* Retrieve the stored dirty rect for the scrolled contents of aScrollableFrame.
* @param aScrollableFrame the scroll frame
* @return the dirty rect, relative to aScrollableFrame's *reference frame*
*/
nsRect GetDirtyRectForScrolledContents(const nsIFrame* aScrollableFrame) const;
private:
void MarkOutOfFlowFrameForDisplay(nsIFrame* aDirtyFrame, nsIFrame* aFrame,
const nsRect& aDirtyRect);
@ -930,6 +946,10 @@ private:
mWillChangeBudget;
// Assert that we never check the budget before its fully calculated.
mutable mozilla::DebugOnly<bool> mWillChangeBudgetCalculated;
// rects are relative to the frame's reference frame
nsDataHashtable<nsPtrHashKey<nsIFrame>, nsRect> mDirtyRectForScrolledContents;
// Relative to mCurrentFrame.
nsRect mDirtyRect;
nsRegion mWindowExcludeGlassRegion;

View File

@ -2962,6 +2962,7 @@ ScrollFrameHelper::BuildDisplayList(nsDisplayListBuilder* aBuilder,
}
}
aBuilder->StoreDirtyRectForScrolledContents(mOuter, dirtyRect);
mOuter->BuildDisplayListForChild(aBuilder, mScrolledFrame, dirtyRect, scrolledContent);
if (idSetter.ShouldForceLayerForScrollParent() &&

View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en" class="reftest-wait">
<meta charset="utf-8">
<title>Bug 1164227 - Testcase for the invalid region simplification bug</title>
<style>
#scrollbox {
width: 400px;
height: 500px;
overflow: auto;
margin: 80px;
border: 1px solid black;
}
.contents {
height: 600px;
background: white;
padding: 20px;
position: relative;
}
.boxes > div {
box-sizing: border-box;
width: 10px;
height: 10px;
border: 1px solid black;
float: left;
margin-left: -2px;
}
.boxes > div:nth-child(odd) {
transform: translateY(500px);
}
.reftest-no-paint {
position: absolute;
top: 250px;
left: 30px;
width: 200px;
height: 50px;
border: 1px solid red;
}
</style>
<div id="scrollbox">
<div class="contents">
<div class="boxes">
<div style="margin-top: 0px"></div>
<div style="margin-top: 1px"></div>
<div style="margin-top: 2px"></div>
<div style="margin-top: 3px"></div>
<div style="margin-top: 4px"></div>
<div style="margin-top: 5px"></div>
<div style="margin-top: 6px"></div>
<div style="margin-top: 7px"></div>
<div style="margin-top: 8px"></div>
<div style="margin-top: 9px"></div>
<div style="margin-top: 10px"></div>
<div style="margin-top: 11px"></div>
<div style="margin-top: 12px"></div>
<div style="margin-top: 13px"></div>
<div style="margin-top: 14px"></div>
<div style="margin-top: 15px"></div>
<div style="margin-top: 16px"></div>
<div style="margin-top: 17px"></div>
<div style="margin-top: 18px"></div>
<div style="margin-top: 19px"></div>
<div style="margin-top: 20px"></div>
<div style="margin-top: 21px"></div>
<div style="margin-top: 22px"></div>
<div style="margin-top: 23px"></div>
<div style="margin-top: 24px"></div>
<div style="margin-top: 25px"></div>
<div style="margin-top: 26px"></div>
<div style="margin-top: 27px"></div>
<div style="margin-top: 28px"></div>
<div style="margin-top: 29px"></div>
<div style="margin-top: 30px"></div>
<div style="margin-top: 31px"></div>
<div style="margin-top: 32px"></div>
<div style="margin-top: 33px"></div>
<div style="margin-top: 34px"></div>
<div style="margin-top: 35px"></div>
<div style="margin-top: 36px"></div>
<div style="margin-top: 37px"></div>
<div style="margin-top: 38px"></div>
<div style="margin-top: 39px"></div>
</div>
<div class="reftest-no-paint"></div>
</div>
</div>
<script>
var scrollbox = document.querySelector("#scrollbox");
scrollbox.scrollTop = 100;
window.addEventListener("MozReftestInvalidate", function (e) {
scrollbox.scrollTop = 0;
document.documentElement.removeAttribute("class");
});
</script>

View File

@ -66,3 +66,4 @@ pref(layout.animated-image-layers.enabled,true) skip-if(Android||gtk2Widget) ==
!= layer-splitting-7.html about:blank
fuzzy-if(gtk2Widget,2,4) == image-scrolling-zoom-1.html image-scrolling-zoom-1-ref.html
!= image-scrolling-zoom-1-ref.html image-scrolling-zoom-1-notref.html
!= fast-scrolling.html about:blank