| /* |
| * 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 = ANativeWindow_getWidth(surface.get()); |
| int height = ANativeWindow_getHeight(surface.get()); |
| 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); }); |
| } |