Do not cache AVDs that are off screen

Bug: 128805564
Test: Manual + systrace; hwui_unit_tests; CtsUiRenderingTestCases

Only update a VectorDrawable's cache if it is onscreen. This fixes a
Twitter use case where the app has a ProgressBar that is exactly one
pixel offscreen. Prior to this CL, we repeatedly drew the ProgressBar's
AVD to a GPU surface, even though we clip it out later and never draw
that GPU surface. Now, we recognize that the AVD is outside of the
bounds of the screen, so we never draw to the GPU surface.

TreeInfo:
- store the size of the screen, retrieved from
  CanvasContext::getNextFrameSize.
SkiaDisplayList:
- Store the matrix at the time of recording a VectorDrawable. Concat
  that with the current matrix to determine whether the VD is on screen,
  based on the TreeInfo. If it is offscreen, do not add it to the list
  of AVDs that need to be updated ahead of rendering.
- In addition, if it is offscreen (or not dirty), do not call
  setPropertyChangeWillBeConsumed(true). This prevents triggering
  dispatchFrameCallbacks to update on the RenderThread when there is no
  need to. This also mimics what would happen if the View/RenderNode had
  been completely offscreen.
- Add a method to append an AVD to mVectorDrawables. Now that the vector
  is of Pairs, this simplifies the call sites. Add a second helper to
  just add an AVD without a matrix, for use in tests.
SkiaRecordingCanvas:
- get the current matrix and store it in the display list along with the
  AVD.
CanvasContext:
- add getNextFrameSize, for reporting the size of the next frame without
  dequeuing it
VectorDrawable.cpp:
- call quickReject to potentially short circuit drawing. This is for a
  hypothetical use case (verified in a test app) where the containing
  RenderNode is partially onscreen, but the AVD itself is not. Even
  without the change to VectorDrawable.cpp, we skip uploading to the GPU
  cache, the SkiaDisplayList still attempts to draw it. This change
  keeps us from drawing it at all.
SkiaDisplayListTests.cpp:
- Now that I've hidden mVectorDrawables, call the new public APIs.
- prepareListAndChildren test
  - for the clean VD, assert that getPropertyChangeWillBeConsumed
    returns FALSE. This is due to the behavior change that we do not
    set it unless the VD is dirty.
  - set the bounds, so our onscreen check works.
- Add another test for prepareListAndChildren, which puts VDs offscreen.

Change-Id: Iae0a07adcf58e7884e0854720de644e7b2faf2bf
This commit is contained in:
Leon Scroggins III
2019-04-03 15:09:25 -04:00
parent f0af267513
commit 6c5864c098
9 changed files with 257 additions and 24 deletions

View File

@@ -25,6 +25,7 @@ TreeInfo::TreeInfo(TraversalMode mode, renderthread::CanvasContext& canvasContex
, prepareTextures(mode == MODE_FULL)
, canvasContext(canvasContext)
, damageGenerationId(canvasContext.getFrameNumber())
, disableForceDark(canvasContext.useForceDark() ? 0 : 1) {}
, disableForceDark(canvasContext.useForceDark() ? 0 : 1)
, screenSize(canvasContext.getNextFrameSize()) {}
} // namespace android::uirenderer

View File

@@ -20,6 +20,7 @@
#include "utils/Macros.h"
#include <utils/Timers.h>
#include "SkSize.h"
#include <string>
@@ -96,6 +97,8 @@ public:
int disableForceDark;
const SkISize screenSize;
struct Out {
bool hasFunctors = false;
// This is only updated if evaluateAnimations is true

View File

@@ -547,6 +547,11 @@ void Tree::Cache::clear() {
}
void Tree::draw(SkCanvas* canvas, const SkRect& bounds, const SkPaint& inPaint) {
if (canvas->quickReject(bounds)) {
// The RenderNode is on screen, but the AVD is not.
return;
}
// Update the paint for any animatable properties
SkPaint paint = inPaint;
paint.setAlpha(mProperties.getRootAlpha() * 255);

View File

@@ -22,6 +22,7 @@
#include "renderthread/CanvasContext.h"
#include <SkImagePriv.h>
#include <SkPathOps.h>
namespace android {
namespace uirenderer {
@@ -35,7 +36,7 @@ void SkiaDisplayList::syncContents(const WebViewSyncData& data) {
animatedImage->syncProperties();
}
for (auto& vectorDrawable : mVectorDrawables) {
vectorDrawable->syncProperties();
vectorDrawable.first->syncProperties();
}
}
@@ -51,6 +52,29 @@ void SkiaDisplayList::updateChildren(std::function<void(RenderNode*)> updateFn)
}
}
static bool intersects(const SkISize screenSize, const Matrix4& mat, const SkRect& bounds) {
Vector3 points[] = { Vector3 {bounds.fLeft, bounds.fTop, 0},
Vector3 {bounds.fRight, bounds.fTop, 0},
Vector3 {bounds.fRight, bounds.fBottom, 0},
Vector3 {bounds.fLeft, bounds.fBottom, 0}};
float minX, minY, maxX, maxY;
bool first = true;
for (auto& point : points) {
mat.mapPoint3d(point);
if (first) {
minX = maxX = point.x;
minY = maxY = point.y;
first = false;
} else {
minX = std::min(minX, point.x);
minY = std::min(minY, point.y);
maxX = std::max(maxX, point.x);
maxY = std::max(maxY, point.y);
}
}
return SkRect::Make(screenSize).intersects(SkRect::MakeLTRB(minX, minY, maxX, maxY));
}
bool SkiaDisplayList::prepareListAndChildren(
TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer,
std::function<void(RenderNode*, TreeObserver&, TreeInfo&, bool)> childFn) {
@@ -107,15 +131,23 @@ bool SkiaDisplayList::prepareListAndChildren(
}
}
for (auto& vectorDrawable : mVectorDrawables) {
for (auto& vectorDrawablePair : mVectorDrawables) {
// If any vector drawable in the display list needs update, damage the node.
auto& vectorDrawable = vectorDrawablePair.first;
if (vectorDrawable->isDirty()) {
isDirty = true;
static_cast<SkiaPipeline*>(info.canvasContext.getRenderPipeline())
->getVectorDrawables()
->push_back(vectorDrawable);
Matrix4 totalMatrix;
info.damageAccumulator->computeCurrentTransform(&totalMatrix);
Matrix4 canvasMatrix(vectorDrawablePair.second);
totalMatrix.multiply(canvasMatrix);
const SkRect& bounds = vectorDrawable->properties().getBounds();
if (intersects(info.screenSize, totalMatrix, bounds)) {
isDirty = true;
static_cast<SkiaPipeline*>(info.canvasContext.getRenderPipeline())
->getVectorDrawables()
->push_back(vectorDrawable);
vectorDrawable->setPropertyChangeWillBeConsumed(true);
}
}
vectorDrawable->setPropertyChangeWillBeConsumed(true);
}
return isDirty;
}

View File

@@ -22,6 +22,7 @@
#include "TreeInfo.h"
#include "hwui/AnimatedImageDrawable.h"
#include "utils/LinearAllocator.h"
#include "utils/Pair.h"
#include <deque>
@@ -41,12 +42,6 @@ typedef uirenderer::VectorDrawable::Tree VectorDrawableRoot;
namespace skiapipeline {
/**
* This class is intended to be self contained, but still subclasses from
* DisplayList to make it easier to support switching between the two at
* runtime. The downside of this inheritance is that we pay for the overhead
* of the parent class construction/destruction without any real benefit.
*/
class SkiaDisplayList {
public:
size_t getUsedSize() { return allocator.usedSize() + mDisplayList.usedSize(); }
@@ -156,7 +151,17 @@ public:
std::deque<RenderNodeDrawable> mChildNodes;
std::deque<FunctorDrawable*> mChildFunctors;
std::vector<SkImage*> mMutableImages;
std::vector<VectorDrawableRoot*> mVectorDrawables;
private:
std::vector<Pair<VectorDrawableRoot*, SkMatrix>> mVectorDrawables;
public:
void appendVD(VectorDrawableRoot* r) {
appendVD(r, SkMatrix::I());
}
void appendVD(VectorDrawableRoot* r, const SkMatrix& mat) {
mVectorDrawables.push_back(Pair<VectorDrawableRoot*, SkMatrix>(r, mat));
}
std::vector<AnimatedImageDrawable*> mAnimatedImages;
DisplayListData mDisplayList;

View File

@@ -153,7 +153,9 @@ void SkiaRecordingCanvas::drawWebViewFunctor(int functor) {
void SkiaRecordingCanvas::drawVectorDrawable(VectorDrawableRoot* tree) {
mRecorder.drawVectorDrawable(tree);
mDisplayList->mVectorDrawables.push_back(tree);
SkMatrix mat;
this->getMatrix(&mat);
mDisplayList->appendVD(tree, mat);
}
// ----------------------------------------------------------------------------

View File

@@ -41,6 +41,7 @@
#include <sys/stat.h>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <functional>
@@ -510,6 +511,17 @@ void CanvasContext::doFrame() {
prepareAndDraw(nullptr);
}
SkISize CanvasContext::getNextFrameSize() const {
ReliableSurface* surface = mNativeSurface.get();
if (surface) {
SkISize size;
surface->query(NATIVE_WINDOW_WIDTH, &size.fWidth);
surface->query(NATIVE_WINDOW_HEIGHT, &size.fHeight);
return size;
}
return {INT32_MAX, INT32_MAX};
}
void CanvasContext::prepareAndDraw(RenderNode* node) {
ATRACE_CALL();

View File

@@ -32,6 +32,7 @@
#include <EGL/egl.h>
#include <SkBitmap.h>
#include <SkRect.h>
#include <SkSize.h>
#include <cutils/compiler.h>
#include <gui/Surface.h>
#include <utils/Functor.h>
@@ -112,7 +113,7 @@ public:
void setSurface(sp<Surface>&& surface);
bool pauseSurface();
void setStopped(bool stopped);
bool hasSurface() { return mNativeSurface.get(); }
bool hasSurface() const { return mNativeSurface.get(); }
void allocateBuffers();
void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
@@ -206,6 +207,8 @@ public:
void setRenderAheadDepth(int renderAhead);
SkISize getNextFrameSize() const;
private:
CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline);

View File

@@ -23,6 +23,7 @@
#include "pipeline/skia/GLFunctorDrawable.h"
#include "pipeline/skia/SkiaDisplayList.h"
#include "renderthread/CanvasContext.h"
#include "tests/common/TestContext.h"
#include "tests/common/TestUtils.h"
using namespace android;
@@ -50,13 +51,13 @@ TEST(SkiaDisplayList, reset) {
GLFunctorDrawable functorDrawable(nullptr, nullptr, &dummyCanvas);
skiaDL->mChildFunctors.push_back(&functorDrawable);
skiaDL->mMutableImages.push_back(nullptr);
skiaDL->mVectorDrawables.push_back(nullptr);
skiaDL->appendVD(nullptr);
skiaDL->mProjectionReceiver = &drawable;
ASSERT_FALSE(skiaDL->mChildNodes.empty());
ASSERT_FALSE(skiaDL->mChildFunctors.empty());
ASSERT_FALSE(skiaDL->mMutableImages.empty());
ASSERT_FALSE(skiaDL->mVectorDrawables.empty());
ASSERT_TRUE(skiaDL->hasVectorDrawables());
ASSERT_FALSE(skiaDL->isEmpty());
ASSERT_TRUE(skiaDL->mProjectionReceiver);
@@ -65,7 +66,7 @@ TEST(SkiaDisplayList, reset) {
ASSERT_TRUE(skiaDL->mChildNodes.empty());
ASSERT_TRUE(skiaDL->mChildFunctors.empty());
ASSERT_TRUE(skiaDL->mMutableImages.empty());
ASSERT_TRUE(skiaDL->mVectorDrawables.empty());
ASSERT_FALSE(skiaDL->hasVectorDrawables());
ASSERT_TRUE(skiaDL->isEmpty());
ASSERT_FALSE(skiaDL->mProjectionReceiver);
}
@@ -110,7 +111,7 @@ TEST(SkiaDisplayList, syncContexts) {
SkRect bounds = SkRect::MakeWH(200, 200);
VectorDrawableRoot vectorDrawable(new VectorDrawable::Group());
vectorDrawable.mutateStagingProperties()->setBounds(bounds);
skiaDL.mVectorDrawables.push_back(&vectorDrawable);
skiaDL.appendVD(&vectorDrawable);
// ensure that the functor and vectorDrawable are properly synced
TestUtils::runOnRenderThread([&](auto&) {
@@ -149,9 +150,14 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren) {
SkiaDisplayList skiaDL;
// The VectorDrawableRoot needs to have bounds on screen (and therefore not
// empty) in order to have PropertyChangeWillBeConsumed set.
const auto bounds = SkRect::MakeIWH(100, 100);
// prepare with a clean VD
VectorDrawableRoot cleanVD(new VectorDrawable::Group());
skiaDL.mVectorDrawables.push_back(&cleanVD);
cleanVD.mutateProperties()->setBounds(bounds);
skiaDL.appendVD(&cleanVD);
cleanVD.getBitmapUpdateIfDirty(); // this clears the dirty bit
ASSERT_FALSE(cleanVD.isDirty());
@@ -159,11 +165,12 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren) {
TestUtils::MockTreeObserver observer;
ASSERT_FALSE(skiaDL.prepareListAndChildren(observer, info, false,
[](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_TRUE(cleanVD.getPropertyChangeWillBeConsumed());
ASSERT_FALSE(cleanVD.getPropertyChangeWillBeConsumed());
// prepare again this time adding a dirty VD
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
skiaDL.mVectorDrawables.push_back(&dirtyVD);
dirtyVD.mutateProperties()->setBounds(bounds);
skiaDL.appendVD(&dirtyVD);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
@@ -191,6 +198,169 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren) {
canvasContext->destroy();
}
RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren_vdOffscreen) {
auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));
// Set up a Surface so that we can position the VectorDrawable offscreen.
test::TestContext testContext;
testContext.setRenderOffscreen(true);
auto surface = testContext.surface();
int width, height;
surface->query(NATIVE_WINDOW_WIDTH, &width);
surface->query(NATIVE_WINDOW_HEIGHT, &height);
canvasContext->setSurface(std::move(surface));
TreeInfo info(TreeInfo::MODE_FULL, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
// The VectorDrawableRoot needs to have bounds on screen (and therefore not
// empty) in order to have PropertyChangeWillBeConsumed set.
const auto bounds = SkRect::MakeIWH(100, 100);
for (const SkRect b : {bounds.makeOffset(width, 0),
bounds.makeOffset(0, height),
bounds.makeOffset(-bounds.width(), 0),
bounds.makeOffset(0, -bounds.height())}) {
SkiaDisplayList skiaDL;
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
dirtyVD.mutateProperties()->setBounds(b);
skiaDL.appendVD(&dirtyVD);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
TestUtils::MockTreeObserver observer;
ASSERT_FALSE(skiaDL.prepareListAndChildren(
observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
}
// The DamageAccumulator's transform can also result in the
// VectorDrawableRoot being offscreen.
for (const SkISize translate : { SkISize{width, 0},
SkISize{0, height},
SkISize{-width, 0},
SkISize{0, -height}}) {
Matrix4 mat4;
mat4.translate(translate.fWidth, translate.fHeight);
damageAccumulator.pushTransform(&mat4);
SkiaDisplayList skiaDL;
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
dirtyVD.mutateProperties()->setBounds(bounds);
skiaDL.appendVD(&dirtyVD);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
TestUtils::MockTreeObserver observer;
ASSERT_FALSE(skiaDL.prepareListAndChildren(
observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
damageAccumulator.popTransform();
}
// Another way to be offscreen: a matrix from the draw call.
for (const SkMatrix translate : { SkMatrix::MakeTrans(width, 0),
SkMatrix::MakeTrans(0, height),
SkMatrix::MakeTrans(-width, 0),
SkMatrix::MakeTrans(0, -height)}) {
SkiaDisplayList skiaDL;
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
dirtyVD.mutateProperties()->setBounds(bounds);
skiaDL.appendVD(&dirtyVD, translate);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
TestUtils::MockTreeObserver observer;
ASSERT_FALSE(skiaDL.prepareListAndChildren(
observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
}
// Verify that the matrices are combined in the right order.
{
// Rotate and then translate, so the VD is offscreen.
Matrix4 mat4;
mat4.loadRotate(180);
damageAccumulator.pushTransform(&mat4);
SkiaDisplayList skiaDL;
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
dirtyVD.mutateProperties()->setBounds(bounds);
SkMatrix translate = SkMatrix::MakeTrans(50, 50);
skiaDL.appendVD(&dirtyVD, translate);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
TestUtils::MockTreeObserver observer;
ASSERT_FALSE(skiaDL.prepareListAndChildren(
observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
damageAccumulator.popTransform();
}
{
// Switch the order of rotate and translate, so it is on screen.
Matrix4 mat4;
mat4.translate(50, 50);
damageAccumulator.pushTransform(&mat4);
SkiaDisplayList skiaDL;
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
dirtyVD.mutateProperties()->setBounds(bounds);
SkMatrix rotate;
rotate.setRotate(180);
skiaDL.appendVD(&dirtyVD, rotate);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
TestUtils::MockTreeObserver observer;
ASSERT_TRUE(skiaDL.prepareListAndChildren(
observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_TRUE(dirtyVD.getPropertyChangeWillBeConsumed());
damageAccumulator.popTransform();
}
{
// An AVD that is larger than the screen.
SkiaDisplayList skiaDL;
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
dirtyVD.mutateProperties()->setBounds(SkRect::MakeLTRB(-1, -1, width + 1, height + 1));
skiaDL.appendVD(&dirtyVD);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
TestUtils::MockTreeObserver observer;
ASSERT_TRUE(skiaDL.prepareListAndChildren(
observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_TRUE(dirtyVD.getPropertyChangeWillBeConsumed());
}
{
// An AVD whose bounds are not a rectangle after applying a matrix.
SkiaDisplayList skiaDL;
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
dirtyVD.mutateProperties()->setBounds(bounds);
SkMatrix mat;
mat.setRotate(45, 50, 50);
skiaDL.appendVD(&dirtyVD, mat);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
TestUtils::MockTreeObserver observer;
ASSERT_TRUE(skiaDL.prepareListAndChildren(
observer, info, false, [](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_TRUE(dirtyVD.getPropertyChangeWillBeConsumed());
}
}
TEST(SkiaDisplayList, updateChildren) {
SkiaDisplayList skiaDL;