/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <VectorDrawable.h>
#include <gtest/gtest.h>
#include "AnimationContext.h"
#include "DamageAccumulator.h"
#include "IContextFactory.h"
#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;
using namespace android::uirenderer;
using namespace android::uirenderer::renderthread;
using namespace android::uirenderer::skiapipeline;
TEST(SkiaDisplayList, create) {
SkiaDisplayList skiaDL;
ASSERT_TRUE(skiaDL.isEmpty());
ASSERT_FALSE(skiaDL.mProjectionReceiver);
}
TEST(SkiaDisplayList, reset) {
std::unique_ptr<SkiaDisplayList> skiaDL;
{
SkiaRecordingCanvas canvas{nullptr, 1, 1};
canvas.drawColor(0, SkBlendMode::kSrc);
skiaDL.reset(canvas.finishRecording());
}
SkCanvas dummyCanvas;
RenderNodeDrawable drawable(nullptr, &dummyCanvas);
skiaDL->mChildNodes.emplace_back(nullptr, &dummyCanvas);
GLFunctorDrawable functorDrawable(nullptr, nullptr, &dummyCanvas);
skiaDL->mChildFunctors.push_back(&functorDrawable);
skiaDL->mMutableImages.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_TRUE(skiaDL->hasVectorDrawables());
ASSERT_FALSE(skiaDL->isEmpty());
ASSERT_TRUE(skiaDL->mProjectionReceiver);
skiaDL->reset();
ASSERT_TRUE(skiaDL->mChildNodes.empty());
ASSERT_TRUE(skiaDL->mChildFunctors.empty());
ASSERT_TRUE(skiaDL->mMutableImages.empty());
ASSERT_FALSE(skiaDL->hasVectorDrawables());
ASSERT_TRUE(skiaDL->isEmpty());
ASSERT_FALSE(skiaDL->mProjectionReceiver);
}
TEST(SkiaDisplayList, reuseDisplayList) {
sp<RenderNode> renderNode = new RenderNode();
std::unique_ptr<SkiaDisplayList> availableList;
// no list has been attached so it should return a nullptr
availableList = renderNode->detachAvailableList();
ASSERT_EQ(availableList.get(), nullptr);
// attach a displayList for reuse
SkiaDisplayList skiaDL;
ASSERT_TRUE(skiaDL.reuseDisplayList(renderNode.get(), nullptr));
// detach the list that you just attempted to reuse
availableList = renderNode->detachAvailableList();
ASSERT_EQ(availableList.get(), &skiaDL);
availableList.release(); // prevents an invalid free since our DL is stack allocated
// after detaching there should return no available list
availableList = renderNode->detachAvailableList();
ASSERT_EQ(availableList.get(), nullptr);
}
TEST(SkiaDisplayList, syncContexts) {
SkiaDisplayList skiaDL;
SkCanvas dummyCanvas;
TestUtils::MockFunctor functor;
GLFunctorDrawable functorDrawable(&functor, nullptr, &dummyCanvas);
skiaDL.mChildFunctors.push_back(&functorDrawable);
int functor2 = WebViewFunctor_create(
nullptr, TestUtils::createMockFunctor(RenderMode::OpenGL_ES), RenderMode::OpenGL_ES);
auto& counts = TestUtils::countsForFunctor(functor2);
skiaDL.mChildFunctors.push_back(
skiaDL.allocateDrawable<GLFunctorDrawable>(functor2, &dummyCanvas));
WebViewFunctor_release(functor2);
SkRect bounds = SkRect::MakeWH(200, 200);
VectorDrawableRoot vectorDrawable(new VectorDrawable::Group());
vectorDrawable.mutateStagingProperties()->setBounds(bounds);
skiaDL.appendVD(&vectorDrawable);
// ensure that the functor and vectorDrawable are properly synced
TestUtils::runOnRenderThread([&](auto&) {
skiaDL.syncContents(WebViewSyncData{
.applyForceDark = false,
});
});
EXPECT_EQ(functor.getLastMode(), DrawGlInfo::kModeSync);
EXPECT_EQ(counts.sync, 1);
EXPECT_EQ(counts.destroyed, 0);
EXPECT_EQ(vectorDrawable.mutateProperties()->getBounds(), bounds);
skiaDL.reset();
TestUtils::runOnRenderThread([](auto&) {
// Fence
});
EXPECT_EQ(counts.destroyed, 1);
}
class ContextFactory : public IContextFactory {
public:
virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override {
return new AnimationContext(clock);
}
};
RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren) {
auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));
TreeInfo info(TreeInfo::MODE_FULL, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
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());
cleanVD.mutateProperties()->setBounds(bounds);
skiaDL.appendVD(&cleanVD);
cleanVD.getBitmapUpdateIfDirty(); // this clears the dirty bit
ASSERT_FALSE(cleanVD.isDirty());
ASSERT_FALSE(cleanVD.getPropertyChangeWillBeConsumed());
TestUtils::MockTreeObserver observer;
ASSERT_FALSE(skiaDL.prepareListAndChildren(observer, info, false,
[](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_FALSE(cleanVD.getPropertyChangeWillBeConsumed());
// prepare again this time adding a dirty VD
VectorDrawableRoot dirtyVD(new VectorDrawable::Group());
dirtyVD.mutateProperties()->setBounds(bounds);
skiaDL.appendVD(&dirtyVD);
ASSERT_TRUE(dirtyVD.isDirty());
ASSERT_FALSE(dirtyVD.getPropertyChangeWillBeConsumed());
ASSERT_TRUE(skiaDL.prepareListAndChildren(observer, info, false,
[](RenderNode*, TreeObserver&, TreeInfo&, bool) {}));
ASSERT_TRUE(dirtyVD.getPropertyChangeWillBeConsumed());
// prepare again this time adding a RenderNode and a callback
sp<RenderNode> renderNode = new RenderNode();
TreeInfo* infoPtr = &info;
SkCanvas dummyCanvas;
skiaDL.mChildNodes.emplace_back(renderNode.get(), &dummyCanvas);
bool hasRun = false;
ASSERT_TRUE(skiaDL.prepareListAndChildren(
observer, info, false,
[&hasRun, renderNode, infoPtr](RenderNode* n, TreeObserver& observer, TreeInfo& i,
bool r) {
hasRun = true;
ASSERT_EQ(renderNode.get(), n);
ASSERT_EQ(infoPtr, &i);
ASSERT_FALSE(r);
}));
ASSERT_TRUE(hasRun);
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;
sp<RenderNode> renderNode = new RenderNode();
SkCanvas dummyCanvas;
skiaDL.mChildNodes.emplace_back(renderNode.get(), &dummyCanvas);
skiaDL.updateChildren([renderNode](RenderNode* n) { ASSERT_EQ(renderNode.get(), n); });
}