Source code

Revision control

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "DisplayPortUtils.h"
#include "FrameMetrics.h"
#include "Layers.h"
#include "mozilla/dom/Document.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/gfx/Point.h"
#include "mozilla/layers/APZCCallbackHelper.h"
#include "mozilla/layers/APZPublicUtils.h"
#include "mozilla/layers/CompositorBridgeChild.h"
#include "mozilla/layers/PAPZ.h"
#include "mozilla/layers/RepaintRequest.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_layers.h"
#include "nsDeckFrame.h"
#include "nsIScrollableFrame.h"
#include "nsLayoutUtils.h"
#include "nsPlaceholderFrame.h"
#include "nsSubDocumentFrame.h"
#include "RetainedDisplayListBuilder.h"
#include <ostream>
namespace mozilla {
using gfx::gfxVars;
using gfx::IntSize;
using layers::APZCCallbackHelper;
using layers::FrameMetrics;
using layers::LayerManager;
using layers::RepaintRequest;
using layers::ScrollableLayerGuid;
typedef ScrollableLayerGuid::ViewID ViewID;
static LazyLogModule sDisplayportLog("apz.displayport");
/* static */
DisplayPortMargins DisplayPortMargins::FromAPZ(
const ScreenMargin& aMargins, const CSSPoint& aVisualOffset,
const CSSPoint& aLayoutOffset, const CSSToScreenScale2D& aScale) {
return DisplayPortMargins{aMargins, aVisualOffset, aLayoutOffset, aScale};
}
CSSToScreenScale2D ComputeDisplayportScale(nsIScrollableFrame* aScrollFrame) {
// The calculation here is equivalent to
// CalculateBasicFrameMetrics(aScrollFrame).DisplayportPixelsPerCSSPixel(),
// but we don't calculate the entire frame metrics.
if (!aScrollFrame) {
return CSSToScreenScale2D(1.0, 1.0);
}
nsIFrame* frame = do_QueryFrame(aScrollFrame);
MOZ_ASSERT(frame);
nsPresContext* presContext = frame->PresContext();
PresShell* presShell = presContext->PresShell();
return presContext->CSSToDevPixelScale() *
LayoutDeviceToLayerScale2D(
presShell->GetCumulativeResolution() *
nsLayoutUtils::GetTransformToAncestorScale(frame)) *
LayerToScreenScale2D(1.0, 1.0);
}
/* static */
DisplayPortMargins DisplayPortMargins::ForScrollFrame(
nsIScrollableFrame* aScrollFrame, const ScreenMargin& aMargins,
const Maybe<CSSToScreenScale2D>& aScale) {
CSSPoint visualOffset;
CSSPoint layoutOffset;
if (aScrollFrame) {
nsIFrame* scrollFrame = do_QueryFrame(aScrollFrame);
PresShell* presShell = scrollFrame->PresShell();
layoutOffset = CSSPoint::FromAppUnits(aScrollFrame->GetScrollPosition());
if (aScrollFrame->IsRootScrollFrameOfDocument() &&
presShell->IsVisualViewportOffsetSet()) {
visualOffset =
CSSPoint::FromAppUnits(presShell->GetVisualViewportOffset());
} else {
visualOffset = layoutOffset;
}
}
return DisplayPortMargins{aMargins, visualOffset, layoutOffset,
aScale.valueOrFrom([&] {
return ComputeDisplayportScale(aScrollFrame);
})};
}
/* static */
DisplayPortMargins DisplayPortMargins::ForContent(
nsIContent* aContent, const ScreenMargin& aMargins) {
return ForScrollFrame(
aContent ? nsLayoutUtils::FindScrollableFrameFor(aContent) : nullptr,
aMargins, Nothing());
}
ScreenMargin DisplayPortMargins::GetRelativeToLayoutViewport(
ContentGeometryType aGeometryType,
nsIScrollableFrame* aScrollableFrame) const {
// APZ wants |mMargins| applied relative to the visual viewport.
// The main-thread painting code applies margins relative to
// the layout viewport. To get the main thread to paint the
// area APZ wants, apply a translation between the two. The
// magnitude of the translation depends on whether we are
// applying the displayport to scrolled or fixed content.
CSSPoint scrollDeltaCss =
ComputeAsyncTranslation(aGeometryType, aScrollableFrame);
ScreenPoint scrollDelta = scrollDeltaCss * mScale;
ScreenMargin margins = mMargins;
margins.left -= scrollDelta.x;
margins.right += scrollDelta.x;
margins.top -= scrollDelta.y;
margins.bottom += scrollDelta.y;
return margins;
}
std::ostream& operator<<(std::ostream& aOs,
const DisplayPortMargins& aMargins) {
if (aMargins.mVisualOffset == CSSPoint() &&
aMargins.mLayoutOffset == CSSPoint()) {
aOs << aMargins.mMargins;
} else {
aOs << "{" << aMargins.mMargins << "," << aMargins.mVisualOffset << ","
<< aMargins.mLayoutOffset << "}";
}
return aOs;
}
CSSPoint DisplayPortMargins::ComputeAsyncTranslation(
ContentGeometryType aGeometryType,
nsIScrollableFrame* aScrollableFrame) const {
// If we are applying the displayport to scrolled content, the
// translation is the entire difference between the visual and
// layout offsets.
if (aGeometryType == ContentGeometryType::Scrolled) {
return mVisualOffset - mLayoutOffset;
}
// If we are applying the displayport to fixed content, only
// part of the difference between the visual and layout offsets
// should be applied. This is because fixed content remains fixed
// to the layout viewport, and some of the async delta between
// the visual and layout offsets can drag the layout viewport
// with it. We want only the remaining delta, i.e. the offset of
// the visual viewport relative to the (async-scrolled) layout
// viewport.
if (!aScrollableFrame) {
// Displayport on a non-scrolling frame for some reason.
// There will be no divergence between the two viewports.
return CSSPoint();
}
// Fixed content is always fixed to an RSF.
MOZ_ASSERT(aScrollableFrame->IsRootScrollFrameOfDocument());
nsIFrame* scrollFrame = do_QueryFrame(aScrollableFrame);
if (!scrollFrame->PresShell()->IsVisualViewportSizeSet()) {
// Zooming is disabled, so the layout viewport tracks the
// visual viewport completely.
return CSSPoint();
}
// Use KeepLayoutViewportEnclosingViewportVisual() to compute
// an async layout viewport the way APZ would.
const CSSRect visualViewport{
mVisualOffset,
// TODO: There are probably some edge cases here around async zooming
// that are not currently being handled properly. For proper handling,
// we'd likely need to save APZ's async zoom when populating
// mVisualOffset, and using it to adjust the visual viewport size here.
// Note that any incorrectness caused by this will only occur transiently
// during async zooming.
CSSSize::FromAppUnits(scrollFrame->PresShell()->GetVisualViewportSize())};
const CSSRect scrollableRect = CSSRect::FromAppUnits(
nsLayoutUtils::CalculateExpandedScrollableRect(scrollFrame));
CSSRect asyncLayoutViewport{
mLayoutOffset,
CSSSize::FromAppUnits(aScrollableFrame->GetScrollPortRect().Size())};
FrameMetrics::KeepLayoutViewportEnclosingVisualViewport(
visualViewport, scrollableRect, /* out */ asyncLayoutViewport);
return mVisualOffset - asyncLayoutViewport.TopLeft();
}
// Return the maximum displayport size, based on the LayerManager's maximum
// supported texture size. The result is in app units.
static nscoord GetMaxDisplayPortSize(nsIContent* aContent,
nsPresContext* aFallbackPrescontext) {
MOZ_ASSERT(!StaticPrefs::layers_enable_tiles_AtStartup(),
"Do not clamp displayports if tiling is enabled");
// Pick a safe maximum displayport size for sanity purposes. This is the
// lowest maximum texture size on tileless-platforms (Windows, D3D10).
// If the gfx.max-texture-size pref is set, further restrict the displayport
// size to fit within that, because the compositor won't upload stuff larger
// than this size.
nscoord safeMaximum = aFallbackPrescontext
? aFallbackPrescontext->DevPixelsToAppUnits(
std::min(8192, gfxPlatform::MaxTextureSize()))
: nscoord_MAX;
nsIFrame* frame = aContent->GetPrimaryFrame();
if (!frame) {
return safeMaximum;
}
frame = nsLayoutUtils::GetDisplayRootFrame(frame);
nsIWidget* widget = frame->GetNearestWidget();
if (!widget) {
return safeMaximum;
}
LayerManager* lm = widget->GetLayerManager();
if (!lm) {
return safeMaximum;
}
nsPresContext* presContext = frame->PresContext();
int32_t maxSizeInDevPixels = lm->GetMaxTextureSize();
if (maxSizeInDevPixels < 0 || maxSizeInDevPixels == INT_MAX) {
return safeMaximum;
}
maxSizeInDevPixels =
std::min(maxSizeInDevPixels, gfxPlatform::MaxTextureSize());
return presContext->DevPixelsToAppUnits(maxSizeInDevPixels);
}
static nsRect ApplyRectMultiplier(nsRect aRect, float aMultiplier) {
if (aMultiplier == 1.0f) {
return aRect;
}
float newWidth = aRect.width * aMultiplier;
float newHeight = aRect.height * aMultiplier;
float newX = aRect.x - ((newWidth - aRect.width) / 2.0f);
float newY = aRect.y - ((newHeight - aRect.height) / 2.0f);
// Rounding doesn't matter too much here, do a round-in
return nsRect(ceil(newX), ceil(newY), floor(newWidth), floor(newHeight));
}
static nsRect GetDisplayPortFromRectData(nsIContent* aContent,
DisplayPortPropertyData* aRectData,
float aMultiplier) {
// In the case where the displayport is set as a rect, we assume it is
// already aligned and clamped as necessary. The burden to do that is
// on the setter of the displayport. In practice very few places set the
// displayport directly as a rect (mostly tests). We still do need to
// expand it by the multiplier though.
return ApplyRectMultiplier(aRectData->mRect, aMultiplier);
}
static nsRect GetDisplayPortFromMarginsData(
nsIContent* aContent, DisplayPortMarginsPropertyData* aMarginsData,
float aMultiplier, const DisplayPortOptions& aOptions) {
// In the case where the displayport is set via margins, we apply the margins
// to a base rect. Then we align the expanded rect based on the alignment
// requested, further expand the rect by the multiplier, and finally, clamp it
// to the size of the scrollable rect.
nsRect base;
if (nsRect* baseData = static_cast<nsRect*>(
aContent->GetProperty(nsGkAtoms::DisplayPortBase))) {
base = *baseData;
} else {
// In theory we shouldn't get here, but we do sometimes (see bug 1212136).
// Fall through for graceful handling.
}
nsIFrame* frame = nsLayoutUtils::GetScrollFrameFromContent(aContent);
if (!frame) {
// Turns out we can't really compute it. Oops. We still should return
// something sane. Note that since we can't clamp the rect without a
// frame, we don't apply the multiplier either as it can cause the result
// to leak outside the scrollable area.
NS_WARNING(
"Attempting to get a displayport from a content with no primary "
"frame!");
return base;
}
bool isRoot = false;
if (aContent->OwnerDoc()->GetRootElement() == aContent) {
isRoot = true;
}
nsIScrollableFrame* scrollableFrame = frame->GetScrollTargetFrame();
nsPoint scrollPos;
if (scrollableFrame) {
scrollPos = scrollableFrame->GetScrollPosition();
}
nsPresContext* presContext = frame->PresContext();
int32_t auPerDevPixel = presContext->AppUnitsPerDevPixel();
LayoutDeviceToScreenScale2D res(
presContext->PresShell()->GetCumulativeResolution() *
nsLayoutUtils::GetTransformToAncestorScale(frame));
// Calculate the expanded scrollable rect, which we'll be clamping the
// displayport to.
nsRect expandedScrollableRect =
nsLayoutUtils::CalculateExpandedScrollableRect(frame);
// GetTransformToAncestorScale() can return 0. In this case, just return the
// base rect (clamped to the expanded scrollable rect), as other calculations
// would run into divisions by zero.
if (res == LayoutDeviceToScreenScale2D(0, 0)) {
// Make sure the displayport remains within the scrollable rect.
return base.MoveInsideAndClamp(expandedScrollableRect - scrollPos);
}
// First convert the base rect to screen pixels
LayoutDeviceToScreenScale2D parentRes = res;
if (isRoot) {
// the base rect for root scroll frames is specified in the parent document
// coordinate space, so it doesn't include the local resolution.
float localRes = presContext->PresShell()->GetResolution();
parentRes.xScale /= localRes;
parentRes.yScale /= localRes;
}
ScreenRect screenRect =
LayoutDeviceRect::FromAppUnits(base, auPerDevPixel) * parentRes;
// Note on the correctness of applying the alignment in Screen space:
// The correct space to apply the alignment in would be Layer space, but
// we don't necessarily know the scale to convert to Layer space at this
// point because Layout may not yet have chosen the resolution at which to
// render (it chooses that in FrameLayerBuilder, but this can be called
// during display list building). Therefore, we perform the alignment in
// Screen space, which basically assumes that Layout chose to render at
// screen resolution; since this is what Layout does most of the time,
// this is a good approximation. A proper solution would involve moving
// the choosing of the resolution to display-list building time.
ScreenSize alignment;
PresShell* presShell = presContext->PresShell();
MOZ_ASSERT(presShell);
bool useWebRender = gfxVars::UseWebRender();
ScreenMargin margins = aMarginsData->mMargins.GetRelativeToLayoutViewport(
aOptions.mGeometryType, scrollableFrame);
if (presShell->IsDisplayportSuppressed() ||
aContent->GetProperty(nsGkAtoms::MinimalDisplayPort)) {
alignment = ScreenSize(1, 1);
} else if (useWebRender) {
// Moving the displayport is relatively expensive with WR so we use a larger
// alignment that causes the displayport to move less frequently. The
// alignment scales up with the size of the base rect so larger scrollframes
// use a larger alignment, but we clamp the alignment to a power of two
// between 128 and 1024 (inclusive).
// This naturally also increases the size of the displayport compared to
// always using a 128 alignment, so the displayport multipliers are also
// correspondingly smaller when WR is enabled to prevent the displayport
// from becoming too big.
IntSize multiplier =
layers::apz::GetDisplayportAlignmentMultiplier(screenRect.Size());
alignment = ScreenSize(128 * multiplier.width, 128 * multiplier.height);
} else if (StaticPrefs::layers_enable_tiles_AtStartup()) {
// Don't align to tiles if they are too large, because we could expand
// the displayport by a lot which can take more paint time. It's a tradeoff
// though because if we don't align to tiles we have more waste on upload.
IntSize tileSize = gfxVars::TileSize();
alignment = ScreenSize(std::min(256, tileSize.width),
std::min(256, tileSize.height));
} else {
// If we're not drawing with tiles then we need to be careful about not
// hitting the max texture size and we only need 1 draw call per layer
// so we can align to a smaller multiple.
alignment = ScreenSize(128, 128);
}
// Avoid division by zero.
if (alignment.width == 0) {
alignment.width = 128;
}
if (alignment.height == 0) {
alignment.height = 128;
}
if (StaticPrefs::layers_enable_tiles_AtStartup() || useWebRender) {
// Expand the rect by the margins
screenRect.Inflate(margins);
} else {
// Calculate the displayport to make sure we fit within the max texture size
// when not tiling.
nscoord maxSizeAppUnits = GetMaxDisplayPortSize(aContent, presContext);
MOZ_ASSERT(maxSizeAppUnits < nscoord_MAX);
// The alignment code can round up to 3 tiles, we want to make sure
// that the displayport can grow by up to 3 tiles without going
// over the max texture size.
const int MAX_ALIGN_ROUNDING = 3;
// Find the maximum size in screen pixels.
int32_t maxSizeDevPx = presContext->AppUnitsToDevPixels(maxSizeAppUnits);
int32_t maxWidthScreenPx = floor(double(maxSizeDevPx) * res.xScale) -
MAX_ALIGN_ROUNDING * alignment.width;
int32_t maxHeightScreenPx = floor(double(maxSizeDevPx) * res.yScale) -
MAX_ALIGN_ROUNDING * alignment.height;
// For each axis, inflate the margins up to the maximum size.
if (screenRect.height < maxHeightScreenPx) {
int32_t budget = maxHeightScreenPx - screenRect.height;
// Scale the margins down to fit into the budget if necessary, maintaining
// their relative ratio.
float scale = 1.0f;
if (float(budget) < margins.TopBottom()) {
scale = float(budget) / margins.TopBottom();
}
float top = margins.top * scale;
float bottom = margins.bottom * scale;
screenRect.y -= top;
screenRect.height += top + bottom;
}
if (screenRect.width < maxWidthScreenPx) {
int32_t budget = maxWidthScreenPx - screenRect.width;
float scale = 1.0f;
if (float(budget) < margins.LeftRight()) {
scale = float(budget) / margins.LeftRight();
}
float left = margins.left * scale;
float right = margins.right * scale;
screenRect.x -= left;
screenRect.width += left + right;
}
}
ScreenPoint scrollPosScreen =
LayoutDevicePoint::FromAppUnits(scrollPos, auPerDevPixel) * res;
// Align the display port.
screenRect += scrollPosScreen;
float x = alignment.width * floor(screenRect.x / alignment.width);
float y = alignment.height * floor(screenRect.y / alignment.height);
float w = alignment.width * ceil(screenRect.width / alignment.width + 1);
float h = alignment.height * ceil(screenRect.height / alignment.height + 1);
screenRect = ScreenRect(x, y, w, h);
screenRect -= scrollPosScreen;
// Convert the aligned rect back into app units.
nsRect result = LayoutDeviceRect::ToAppUnits(screenRect / res, auPerDevPixel);
// If we have non-zero margins, expand the displayport for the low-res buffer
// if that's what we're drawing. If we have zero margins, we want the
// displayport to reflect the scrollport.
if (margins != ScreenMargin()) {
result = ApplyRectMultiplier(result, aMultiplier);
}
// Make sure the displayport remains within the scrollable rect.
result = result.MoveInsideAndClamp(expandedScrollableRect - scrollPos);
return result;
}
static bool GetDisplayPortData(
nsIContent* aContent, DisplayPortPropertyData** aOutRectData,
DisplayPortMarginsPropertyData** aOutMarginsData) {
MOZ_ASSERT(aOutRectData && aOutMarginsData);
*aOutRectData = static_cast<DisplayPortPropertyData*>(
aContent->GetProperty(nsGkAtoms::DisplayPort));
*aOutMarginsData = static_cast<DisplayPortMarginsPropertyData*>(
aContent->GetProperty(nsGkAtoms::DisplayPortMargins));
if (!*aOutRectData && !*aOutMarginsData) {
// This content element has no displayport data at all
return false;
}
if (*aOutRectData && *aOutMarginsData) {
// choose margins if equal priority
if ((*aOutRectData)->mPriority > (*aOutMarginsData)->mPriority) {
*aOutMarginsData = nullptr;
} else {
*aOutRectData = nullptr;
}
}
NS_ASSERTION((*aOutRectData == nullptr) != (*aOutMarginsData == nullptr),
"Only one of aOutRectData or aOutMarginsData should be set!");
return true;
}
static bool GetWasDisplayPortPainted(nsIContent* aContent) {
DisplayPortPropertyData* rectData = nullptr;
DisplayPortMarginsPropertyData* marginsData = nullptr;
if (!GetDisplayPortData(aContent, &rectData, &marginsData)) {
return false;
}
return rectData ? rectData->mPainted : marginsData->mPainted;
}
bool DisplayPortUtils::IsMissingDisplayPortBaseRect(nsIContent* aContent) {
DisplayPortPropertyData* rectData = nullptr;
DisplayPortMarginsPropertyData* marginsData = nullptr;
if (GetDisplayPortData(aContent, &rectData, &marginsData) && marginsData) {
return !aContent->GetProperty(nsGkAtoms::DisplayPortBase);
}
return false;
}
static void TranslateFromScrollPortToScrollFrame(nsIContent* aContent,
nsRect* aRect) {
MOZ_ASSERT(aRect);
if (nsIScrollableFrame* scrollableFrame =
nsLayoutUtils::FindScrollableFrameFor(aContent)) {
*aRect += scrollableFrame->GetScrollPortRect().TopLeft();
}
}
static bool GetDisplayPortImpl(nsIContent* aContent, nsRect* aResult,
float aMultiplier,
const DisplayPortOptions& aOptions) {
DisplayPortPropertyData* rectData = nullptr;
DisplayPortMarginsPropertyData* marginsData = nullptr;
if (!GetDisplayPortData(aContent, &rectData, &marginsData)) {
return false;
}
nsIFrame* frame = aContent->GetPrimaryFrame();
if (frame && !frame->PresShell()->AsyncPanZoomEnabled()) {
return false;
}
if (!aResult) {
// We have displayport data, but the caller doesn't want the actual
// rect, so we don't need to actually compute it.
return true;
}
bool isDisplayportSuppressed = false;
if (frame) {
nsPresContext* presContext = frame->PresContext();
MOZ_ASSERT(presContext);
PresShell* presShell = presContext->PresShell();
MOZ_ASSERT(presShell);
isDisplayportSuppressed = presShell->IsDisplayportSuppressed();
}
nsRect result;
if (rectData) {
result = GetDisplayPortFromRectData(aContent, rectData, aMultiplier);
} else if (isDisplayportSuppressed ||
nsLayoutUtils::ShouldDisableApzForElement(aContent) ||
aContent->GetProperty(nsGkAtoms::MinimalDisplayPort)) {
// Make a copy of the margins data but set the margins to empty.
// Do not create a new DisplayPortMargins object with
// DisplayPortMargins::Empty(), because that will record the visual
// and layout scroll offsets in place right now on the DisplayPortMargins,
// and those are only meant to be recorded when the margins are stored.
DisplayPortMarginsPropertyData noMargins = *marginsData;
noMargins.mMargins.mMargins = ScreenMargin();
result = GetDisplayPortFromMarginsData(aContent, &noMargins, aMultiplier,
aOptions);
} else {
result = GetDisplayPortFromMarginsData(aContent, marginsData, aMultiplier,
aOptions);
}
if (!StaticPrefs::layers_enable_tiles_AtStartup()) {
// Perform the desired error handling if the displayport dimensions
// exceeds the maximum allowed size
nscoord maxSize = GetMaxDisplayPortSize(aContent, nullptr);
if (result.width > maxSize || result.height > maxSize) {
switch (aOptions.mMaxSizeExceededBehaviour) {
case MaxSizeExceededBehaviour::Assert:
NS_ASSERTION(false, "Displayport must be a valid texture size");
break;
case MaxSizeExceededBehaviour::Drop:
return false;
}
}
}
if (aOptions.mRelativeTo == DisplayportRelativeTo::ScrollFrame) {
TranslateFromScrollPortToScrollFrame(aContent, &result);
}
*aResult = result;
return true;
}
bool DisplayPortUtils::GetDisplayPort(nsIContent* aContent, nsRect* aResult,
const DisplayPortOptions& aOptions) {
float multiplier = StaticPrefs::layers_low_precision_buffer()
? 1.0f / StaticPrefs::layers_low_precision_resolution()
: 1.0f;
return GetDisplayPortImpl(aContent, aResult, multiplier, aOptions);
}
bool DisplayPortUtils::HasDisplayPort(nsIContent* aContent) {
return GetDisplayPort(aContent, nullptr);
}
bool DisplayPortUtils::HasPaintedDisplayPort(nsIContent* aContent) {
DisplayPortPropertyData* rectData = nullptr;
DisplayPortMarginsPropertyData* marginsData = nullptr;
GetDisplayPortData(aContent, &rectData, &marginsData);
if (rectData) {
return rectData->mPainted;
}
if (marginsData) {
return marginsData->mPainted;
}
return false;
}
void DisplayPortUtils::MarkDisplayPortAsPainted(nsIContent* aContent) {
DisplayPortPropertyData* rectData = nullptr;
DisplayPortMarginsPropertyData* marginsData = nullptr;
GetDisplayPortData(aContent, &rectData, &marginsData);
MOZ_ASSERT(rectData || marginsData,
"MarkDisplayPortAsPainted should only be called for an element "
"with a displayport");
if (rectData) {
rectData->mPainted = true;
}
if (marginsData) {
marginsData->mPainted = true;
}
}
bool DisplayPortUtils::HasNonMinimalDisplayPort(nsIContent* aContent) {
return HasDisplayPort(aContent) &&
!aContent->GetProperty(nsGkAtoms::MinimalDisplayPort);
}
bool DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(nsIContent* aContent) {
if (!HasDisplayPort(aContent)) {
return false;
}
if (aContent->GetProperty(nsGkAtoms::MinimalDisplayPort)) {
return false;
}
DisplayPortMarginsPropertyData* currentData =
static_cast<DisplayPortMarginsPropertyData*>(
aContent->GetProperty(nsGkAtoms::DisplayPortMargins));
if (!currentData) {
// We have a display port, so if we don't have margin data we must have rect
// data. We consider such as non zero and non minimal, it's probably not too
// important as display port rects are only used in tests.
return true;
}
if (currentData->mMargins.mMargins != ScreenMargin()) {
return true;
}
return false;
}
/* static */
bool DisplayPortUtils::GetDisplayPortForVisibilityTesting(nsIContent* aContent,
nsRect* aResult) {
MOZ_ASSERT(aResult);
// Since the base rect might not have been updated very recently, it's
// possible to end up with an extra-large displayport at this point, if the
// zoom level is changed by a lot. Instead of using the default behaviour of
// asserting, we can just ignore the displayport if that happens, as this
// call site is best-effort.
return GetDisplayPortImpl(aContent, aResult, 1.0f,
DisplayPortOptions()
.With(MaxSizeExceededBehaviour::Drop)
.With(DisplayportRelativeTo::ScrollFrame));
}
void DisplayPortUtils::InvalidateForDisplayPortChange(
nsIContent* aContent, bool aHadDisplayPort, const nsRect& aOldDisplayPort,
const nsRect& aNewDisplayPort, RepaintMode aRepaintMode) {
if (aRepaintMode != RepaintMode::Repaint) {
return;
}
bool changed =
!aHadDisplayPort || !aOldDisplayPort.IsEqualEdges(aNewDisplayPort);
nsIFrame* frame = nsLayoutUtils::GetScrollFrameFromContent(aContent);
if (frame) {
frame = do_QueryFrame(frame->GetScrollTargetFrame());
}
if (changed && frame) {
// It is important to call SchedulePaint on the same frame that we set the
// dirty rect properties on so we can find the frame later to remove the
// properties.
frame->SchedulePaint();
if (!nsLayoutUtils::AreRetainedDisplayListsEnabled() ||
!nsLayoutUtils::DisplayRootHasRetainedDisplayListBuilder(frame)) {
return;
}
bool found;
nsRect* rect = frame->GetProperty(
nsDisplayListBuilder::DisplayListBuildingDisplayPortRect(), &found);
if (!found) {
rect = new nsRect();
frame->AddProperty(
nsDisplayListBuilder::DisplayListBuildingDisplayPortRect(), rect);
frame->SetHasOverrideDirtyRegion(true);
nsIFrame* rootFrame = frame->PresShell()->GetRootFrame();
MOZ_ASSERT(rootFrame);
RetainedDisplayListData* data =
GetOrSetRetainedDisplayListData(rootFrame);
data->Flags(frame) |= RetainedDisplayListData::FrameFlags::HasProps;
} else {
MOZ_ASSERT(rect, "this property should only store non-null values");
}
if (aHadDisplayPort) {
// We only need to build a display list for any new areas added
nsRegion newRegion(aNewDisplayPort);
newRegion.SubOut(aOldDisplayPort);
rect->UnionRect(*rect, newRegion.GetBounds());
} else {
rect->UnionRect(*rect, aNewDisplayPort);
}
}
}
bool DisplayPortUtils::SetDisplayPortMargins(
nsIContent* aContent, PresShell* aPresShell,
const DisplayPortMargins& aMargins,
ClearMinimalDisplayPortProperty aClearMinimalDisplayPortProperty,
uint32_t aPriority, RepaintMode aRepaintMode) {
MOZ_ASSERT(aContent);
MOZ_ASSERT(aContent->GetComposedDoc() == aPresShell->GetDocument());
DisplayPortMarginsPropertyData* currentData =
static_cast<DisplayPortMarginsPropertyData*>(
aContent->GetProperty(nsGkAtoms::DisplayPortMargins));
if (currentData && currentData->mPriority > aPriority) {
return false;
}
if (currentData && currentData->mMargins.mVisualOffset != CSSPoint() &&
aMargins.mVisualOffset == CSSPoint()) {
// If we hit this, then it's possible that we're setting a displayport
// that is wrong because the old one had a layout/visual adjustment and
// the new one does not.
MOZ_LOG(sDisplayportLog, LogLevel::Warning,
("Dropping visual offset %s",
ToString(currentData->mMargins.mVisualOffset).c_str()));
}
nsIFrame* scrollFrame = nsLayoutUtils::GetScrollFrameFromContent(aContent);
nsRect oldDisplayPort;
bool hadDisplayPort = false;
bool wasPainted = GetWasDisplayPortPainted(aContent);
if (scrollFrame) {
// We only use the two return values from this function to call
// InvalidateForDisplayPortChange. InvalidateForDisplayPortChange does
// nothing if aContent does not have a frame. So getting the displayport is
// useless if the content has no frame, so we avoid calling this to avoid
// triggering a warning about not having a frame.
hadDisplayPort = GetHighResolutionDisplayPort(aContent, &oldDisplayPort);
}
aContent->SetProperty(
nsGkAtoms::DisplayPortMargins,
new DisplayPortMarginsPropertyData(aMargins, aPriority, wasPainted),
nsINode::DeleteProperty<DisplayPortMarginsPropertyData>);
if (aClearMinimalDisplayPortProperty ==
ClearMinimalDisplayPortProperty::Yes) {
if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug) &&
aContent->GetProperty(nsGkAtoms::MinimalDisplayPort)) {
mozilla::layers::ScrollableLayerGuid::ViewID viewID =
mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
nsLayoutUtils::FindIDFor(aContent, &viewID);
MOZ_LOG(sDisplayportLog, LogLevel::Debug,
("SetDisplayPortMargins removing MinimalDisplayPort prop on "
"scrollId=%" PRIu64 "\n",
viewID));
}
aContent->RemoveProperty(nsGkAtoms::MinimalDisplayPort);
}
nsIScrollableFrame* scrollableFrame =
scrollFrame ? scrollFrame->GetScrollTargetFrame() : nullptr;
if (!scrollableFrame) {
return true;
}
nsRect newDisplayPort;
DebugOnly<bool> hasDisplayPort =
GetHighResolutionDisplayPort(aContent, &newDisplayPort);
MOZ_ASSERT(hasDisplayPort);
if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) {
mozilla::layers::ScrollableLayerGuid::ViewID viewID =
mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
nsLayoutUtils::FindIDFor(aContent, &viewID);
if (!hadDisplayPort) {
MOZ_LOG(sDisplayportLog, LogLevel::Debug,
("SetDisplayPortMargins %s on scrollId=%" PRIu64 ", newDp=%s\n",
ToString(aMargins).c_str(), viewID,
ToString(newDisplayPort).c_str()));
} else {
// Use verbose level logging for when an existing displayport got its
// margins updated.
MOZ_LOG(sDisplayportLog, LogLevel::Verbose,
("SetDisplayPortMargins %s on scrollId=%" PRIu64 ", newDp=%s\n",
ToString(aMargins).c_str(), viewID,
ToString(newDisplayPort).c_str()));
}
}
InvalidateForDisplayPortChange(aContent, hadDisplayPort, oldDisplayPort,
newDisplayPort, aRepaintMode);
scrollableFrame->TriggerDisplayPortExpiration();
// Display port margins changing means that the set of visible frames may
// have drastically changed. Check if we should schedule an update.
hadDisplayPort =
scrollableFrame->GetDisplayPortAtLastApproximateFrameVisibilityUpdate(
&oldDisplayPort);
bool needVisibilityUpdate = !hadDisplayPort;
// Check if the total size has changed by a large factor.
if (!needVisibilityUpdate) {
if ((newDisplayPort.width > 2 * oldDisplayPort.width) ||
(oldDisplayPort.width > 2 * newDisplayPort.width) ||
(newDisplayPort.height > 2 * oldDisplayPort.height) ||
(oldDisplayPort.height > 2 * newDisplayPort.height)) {
needVisibilityUpdate = true;
}
}
// Check if it's moved by a significant amount.
if (!needVisibilityUpdate) {
if (nsRect* baseData = static_cast<nsRect*>(
aContent->GetProperty(nsGkAtoms::DisplayPortBase))) {
nsRect base = *baseData;
if ((std::abs(newDisplayPort.X() - oldDisplayPort.X()) > base.width) ||
(std::abs(newDisplayPort.XMost() - oldDisplayPort.XMost()) >
base.width) ||
(std::abs(newDisplayPort.Y() - oldDisplayPort.Y()) > base.height) ||
(std::abs(newDisplayPort.YMost() - oldDisplayPort.YMost()) >
base.height)) {
needVisibilityUpdate = true;
}
}
}
if (needVisibilityUpdate) {
aPresShell->ScheduleApproximateFrameVisibilityUpdateNow();
}
return true;
}
void DisplayPortUtils::SetDisplayPortBase(nsIContent* aContent,
const nsRect& aBase) {
if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Verbose)) {
ViewID viewId = nsLayoutUtils::FindOrCreateIDFor(aContent);
MOZ_LOG(sDisplayportLog, LogLevel::Verbose,
("Setting base rect %s for scrollId=%" PRIu64 "\n",
ToString(aBase).c_str(), viewId));
}
aContent->SetProperty(nsGkAtoms::DisplayPortBase, new nsRect(aBase),
nsINode::DeleteProperty<nsRect>);
}
void DisplayPortUtils::SetDisplayPortBaseIfNotSet(nsIContent* aContent,
const nsRect& aBase) {
if (!aContent->GetProperty(nsGkAtoms::DisplayPortBase)) {
SetDisplayPortBase(aContent, aBase);
}
}
bool DisplayPortUtils::GetCriticalDisplayPort(
nsIContent* aContent, nsRect* aResult, const DisplayPortOptions& aOptions) {
if (StaticPrefs::layers_low_precision_buffer()) {
return GetDisplayPortImpl(aContent, aResult, 1.0f, aOptions);
}
return false;
}
bool DisplayPortUtils::HasCriticalDisplayPort(nsIContent* aContent) {
return GetCriticalDisplayPort(aContent, nullptr);
}
bool DisplayPortUtils::GetHighResolutionDisplayPort(
nsIContent* aContent, nsRect* aResult, const DisplayPortOptions& aOptions) {
if (StaticPrefs::layers_low_precision_buffer()) {
return GetCriticalDisplayPort(aContent, aResult, aOptions);
}
return GetDisplayPort(aContent, aResult, aOptions);
}
void DisplayPortUtils::RemoveDisplayPort(nsIContent* aContent) {
aContent->RemoveProperty(nsGkAtoms::DisplayPort);
aContent->RemoveProperty(nsGkAtoms::DisplayPortMargins);
}
bool DisplayPortUtils::ViewportHasDisplayPort(nsPresContext* aPresContext) {
nsIFrame* rootScrollFrame = aPresContext->PresShell()->GetRootScrollFrame();
return rootScrollFrame && HasDisplayPort(rootScrollFrame->GetContent());
}
bool DisplayPortUtils::IsFixedPosFrameInDisplayPort(const nsIFrame* aFrame) {
// Fixed-pos frames are parented by the viewport frame or the page content
// frame. We'll assume that printing/print preview don't have displayports for
// their pages!
nsIFrame* parent = aFrame->GetParent();
if (!parent || parent->GetParent() ||
aFrame->StyleDisplay()->mPosition != StylePositionProperty::Fixed) {
return false;
}
return ViewportHasDisplayPort(aFrame->PresContext());
}
// We want to this return true for the scroll frame, but not the
// scrolled frame (which has the same content).
bool DisplayPortUtils::FrameHasDisplayPort(nsIFrame* aFrame,
const nsIFrame* aScrolledFrame) {
if (!aFrame->GetContent() || !HasDisplayPort(aFrame->GetContent())) {
return false;
}
nsIScrollableFrame* sf = do_QueryFrame(aFrame);
if (sf) {
if (aScrolledFrame && aScrolledFrame != sf->GetScrolledFrame()) {
return false;
}
return true;
}
return false;
}
bool DisplayPortUtils::CalculateAndSetDisplayPortMargins(
nsIScrollableFrame* aScrollFrame, RepaintMode aRepaintMode) {
nsIFrame* frame = do_QueryFrame(aScrollFrame);
MOZ_ASSERT(frame);
nsIContent* content = frame->GetContent();
MOZ_ASSERT(content);
FrameMetrics metrics =
nsLayoutUtils::CalculateBasicFrameMetrics(aScrollFrame);
ScreenMargin displayportMargins = layers::apz::CalculatePendingDisplayPort(
metrics, ParentLayerPoint(0.0f, 0.0f));
PresShell* presShell = frame->PresContext()->GetPresShell();
DisplayPortMargins margins = DisplayPortMargins::ForScrollFrame(
aScrollFrame, displayportMargins,
Some(metrics.DisplayportPixelsPerCSSPixel()));
return SetDisplayPortMargins(content, presShell, margins,
ClearMinimalDisplayPortProperty::Yes, 0,
aRepaintMode);
}
bool DisplayPortUtils::MaybeCreateDisplayPort(nsDisplayListBuilder* aBuilder,
nsIFrame* aScrollFrame,
RepaintMode aRepaintMode) {
nsIContent* content = aScrollFrame->GetContent();
nsIScrollableFrame* scrollableFrame = do_QueryFrame(aScrollFrame);
if (!content || !scrollableFrame) {
return false;
}
bool haveDisplayPort = HasNonMinimalNonZeroDisplayPort(content);
// We perform an optimization where we ensure that at least one
// async-scrollable frame (i.e. one that WantsAsyncScroll()) has a
// displayport. If that's not the case yet, and we are async-scrollable, we
// will get a displayport.
if (aBuilder->IsPaintingToWindow() &&
nsLayoutUtils::AsyncPanZoomEnabled(aScrollFrame) &&
!aBuilder->HaveScrollableDisplayPort() &&
scrollableFrame->WantAsyncScroll()) {
// If we don't already have a displayport, calculate and set one.
if (!haveDisplayPort) {
// We only use the viewId for logging purposes, but create it
// unconditionally to minimize impact of enabling logging. If we don't
// assign a viewId here it will get assigned later anyway so functionally
// there should be no difference.
ViewID viewId = nsLayoutUtils::FindOrCreateIDFor(content);
MOZ_LOG(
sDisplayportLog, LogLevel::Debug,
("Setting DP on first-encountered scrollId=%" PRIu64 "\n", viewId));
CalculateAndSetDisplayPortMargins(scrollableFrame, aRepaintMode);
#ifdef DEBUG
haveDisplayPort = HasNonMinimalDisplayPort(content);
MOZ_ASSERT(haveDisplayPort,
"should have a displayport after having just set it");
#endif
}
// Record that the we now have a scrollable display port.
aBuilder->SetHaveScrollableDisplayPort();
return true;
}
return false;
}
void DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
nsIFrame* aFrame) {
nsIFrame* frame = aFrame;
while (frame) {
frame = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(frame);
if (!frame) {
break;
}
nsIScrollableFrame* scrollAncestor =
nsLayoutUtils::GetAsyncScrollableAncestorFrame(frame);
if (!scrollAncestor) {
break;
}
frame = do_QueryFrame(scrollAncestor);
MOZ_ASSERT(frame);
MOZ_ASSERT(scrollAncestor->WantAsyncScroll() ||
frame->PresShell()->GetRootScrollFrame() == frame);
if (nsLayoutUtils::AsyncPanZoomEnabled(frame) &&
!HasDisplayPort(frame->GetContent())) {
SetDisplayPortMargins(frame->GetContent(), frame->PresShell(),
DisplayPortMargins::Empty(frame->GetContent()),
ClearMinimalDisplayPortProperty::No, 0,
RepaintMode::Repaint);
}
}
}
bool DisplayPortUtils::MaybeCreateDisplayPortInFirstScrollFrameEncountered(
nsIFrame* aFrame, nsDisplayListBuilder* aBuilder) {
nsIScrollableFrame* sf = do_QueryFrame(aFrame);
if (sf) {
if (MaybeCreateDisplayPort(aBuilder, aFrame, RepaintMode::Repaint)) {
return true;
}
}
if (aFrame->IsPlaceholderFrame()) {
nsPlaceholderFrame* placeholder = static_cast<nsPlaceholderFrame*>(aFrame);
if (MaybeCreateDisplayPortInFirstScrollFrameEncountered(
placeholder->GetOutOfFlowFrame(), aBuilder)) {
return true;
}
}
if (aFrame->IsSubDocumentFrame()) {
PresShell* presShell = static_cast<nsSubDocumentFrame*>(aFrame)
->GetSubdocumentPresShellForPainting(0);
nsIFrame* root = presShell ? presShell->GetRootFrame() : nullptr;
if (root) {
if (MaybeCreateDisplayPortInFirstScrollFrameEncountered(root, aBuilder)) {
return true;
}
}
}
if (aFrame->IsDeckFrame()) {
// only descend the visible card of a decks
nsIFrame* child = static_cast<nsDeckFrame*>(aFrame)->GetSelectedBox();
if (child) {
return MaybeCreateDisplayPortInFirstScrollFrameEncountered(child,
aBuilder);
}
}
for (nsIFrame* child : aFrame->PrincipalChildList()) {
if (MaybeCreateDisplayPortInFirstScrollFrameEncountered(child, aBuilder)) {
return true;
}
}
return false;
}
void DisplayPortUtils::ExpireDisplayPortOnAsyncScrollableAncestor(
nsIFrame* aFrame) {
nsIFrame* frame = aFrame;
while (frame) {
frame = nsLayoutUtils::GetCrossDocParentFrame(frame);
if (!frame) {
break;
}
nsIScrollableFrame* scrollAncestor =
nsLayoutUtils::GetAsyncScrollableAncestorFrame(frame);
if (!scrollAncestor) {
break;
}
frame = do_QueryFrame(scrollAncestor);
MOZ_ASSERT(frame);
if (!frame) {
break;
}
MOZ_ASSERT(scrollAncestor->WantAsyncScroll() ||
frame->PresShell()->GetRootScrollFrame() == frame);
if (HasDisplayPort(frame->GetContent())) {
scrollAncestor->TriggerDisplayPortExpiration();
// Stop after the first trigger. If it failed, there's no point in
// continuing because all the rest of the frames we encounter are going
// to be ancestors of |scrollAncestor| which will keep its displayport.
// If the trigger succeeded, we stop because when the trigger executes
// it will call this function again to trigger the next ancestor up the
// chain.
break;
}
}
}
static PresShell* GetPresShell(const nsIContent* aContent) {
if (dom::Document* doc = aContent->GetComposedDoc()) {
return doc->GetPresShell();
}
return nullptr;
}
static void UpdateDisplayPortMarginsForPendingMetrics(
const RepaintRequest& aMetrics) {
nsIContent* content = nsLayoutUtils::FindContentFor(aMetrics.GetScrollId());
if (!content) {
return;
}
RefPtr<PresShell> presShell = GetPresShell(content);
if (!presShell) {
return;
}
if (nsLayoutUtils::AllowZoomingForDocument(presShell->GetDocument()) &&
aMetrics.IsRootContent()) {
// See APZCCallbackHelper::UpdateRootFrame for details.
float presShellResolution = presShell->GetResolution();
if (presShellResolution != aMetrics.GetPresShellResolution()) {
return;
}
}
nsIScrollableFrame* frame =
nsLayoutUtils::FindScrollableFrameFor(aMetrics.GetScrollId());
if (!frame) {
return;
}
if (APZCCallbackHelper::IsScrollInProgress(frame)) {
// If these conditions are true, then the UpdateFrame
// message may be ignored by the main-thread, so we
// shouldn't update the displayport based on it.
return;
}
DisplayPortMarginsPropertyData* currentData =
static_cast<DisplayPortMarginsPropertyData*>(
content->GetProperty(nsGkAtoms::DisplayPortMargins));
if (!currentData) {
return;
}
CSSPoint frameScrollOffset =
CSSPoint::FromAppUnits(frame->GetScrollPosition());
DisplayPortUtils::SetDisplayPortMargins(
content, presShell,
DisplayPortMargins::FromAPZ(
aMetrics.GetDisplayPortMargins(), aMetrics.GetVisualScrollOffset(),
frameScrollOffset, aMetrics.DisplayportPixelsPerCSSPixel()),
DisplayPortUtils::ClearMinimalDisplayPortProperty::No, 0);
}
/* static */
void DisplayPortUtils::UpdateDisplayPortMarginsFromPendingMessages() {
if (XRE_IsContentProcess() && layers::CompositorBridgeChild::Get() &&
layers::CompositorBridgeChild::Get()->GetIPCChannel()) {
layers::CompositorBridgeChild::Get()->GetIPCChannel()->PeekMessages(
[](const IPC::Message& aMsg) -> bool {
if (aMsg.type() == layers::PAPZ::Msg_RequestContentRepaint__ID) {
PickleIterator iter(aMsg);
RepaintRequest request;
if (!IPC::ReadParam(&aMsg, &iter, &request)) {
MOZ_ASSERT(false);
return true;
}
UpdateDisplayPortMarginsForPendingMetrics(request);
}
return true;
});
}
}
} // namespace mozilla