/* * 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 <SkClipStack.h> #include <SkSurface_Base.h> #include <string.h> #include "AnimationContext.h" #include "DamageAccumulator.h" #include "FatalTestCanvas.h" #include "IContextFactory.h" #include "RecordingCanvas.h" #include "SkiaCanvas.h" #include "pipeline/skia/SkiaDisplayList.h" #include "pipeline/skia/SkiaOpenGLPipeline.h" #include "pipeline/skia/SkiaPipeline.h" #include "pipeline/skia/SkiaRecordingCanvas.h" #include "renderthread/CanvasContext.h" #include "tests/common/TestUtils.h" #include "utils/Color.h" using namespace android; using namespace android::uirenderer; using namespace android::uirenderer::renderthread; using namespace android::uirenderer::skiapipeline; TEST(RenderNodeDrawable, create) { auto rootNode = TestUtils::createNode(0, 0, 200, 400, [](RenderProperties& props, Canvas& canvas) { canvas.drawColor(Color::Red_500, SkBlendMode::kSrcOver); }); DisplayListData skLiteDL; RecordingCanvas canvas; canvas.reset(&skLiteDL, SkIRect::MakeWH(1, 1)); canvas.translate(100, 100); RenderNodeDrawable drawable(rootNode.get(), &canvas); ASSERT_EQ(drawable.getRenderNode(), rootNode.get()); ASSERT_EQ(&drawable.getNodeProperties(), &rootNode->properties()); ASSERT_EQ(drawable.getRecordedMatrix(), canvas.getTotalMatrix()); } namespace { static void drawOrderedRect(Canvas* canvas, uint8_t expectedDrawOrder) { SkPaint paint; // order put in blue channel, transparent so overlapped content doesn't get rejected paint.setColor(SkColorSetARGB(1, 0, 0, expectedDrawOrder)); canvas->drawRect(0, 0, 100, 100, paint); } static void drawOrderedNode(Canvas* canvas, uint8_t expectedDrawOrder, float z) { auto node = TestUtils::createSkiaNode( 0, 0, 100, 100, [expectedDrawOrder, z](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedRect(&canvas, expectedDrawOrder); props.setTranslationZ(z); }); canvas->drawRenderNode(node.get()); // canvas takes reference/sole ownership } static void drawOrderedNode( Canvas* canvas, uint8_t expectedDrawOrder, std::function<void(RenderProperties& props, SkiaRecordingCanvas& canvas)> setup) { auto node = TestUtils::createSkiaNode( 0, 0, 100, 100, [expectedDrawOrder, setup](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedRect(&canvas, expectedDrawOrder); if (setup) { setup(props, canvas); } }); canvas->drawRenderNode(node.get()); // canvas takes reference/sole ownership } class ZReorderCanvas : public SkCanvas { public: ZReorderCanvas(int width, int height) : SkCanvas(width, height) {} void onDrawRect(const SkRect& rect, const SkPaint& paint) override { int expectedOrder = SkColorGetB(paint.getColor()); // extract order from blue channel EXPECT_EQ(expectedOrder, mDrawCounter++) << "An op was drawn out of order"; } int getIndex() { return mDrawCounter; } protected: int mDrawCounter = 0; }; } // end anonymous namespace TEST(RenderNodeDrawable, zReorder) { auto parent = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { canvas.insertReorderBarrier(true); canvas.insertReorderBarrier(false); drawOrderedNode(&canvas, 0, 10.0f); // in reorder=false at this point, so played inorder drawOrderedRect(&canvas, 1); canvas.insertReorderBarrier(true); drawOrderedNode(&canvas, 6, 2.0f); drawOrderedRect(&canvas, 3); drawOrderedNode(&canvas, 4, 0.0f); drawOrderedRect(&canvas, 5); drawOrderedNode(&canvas, 2, -2.0f); drawOrderedNode(&canvas, 7, 2.0f); canvas.insertReorderBarrier(false); drawOrderedRect(&canvas, 8); drawOrderedNode(&canvas, 9, -10.0f); // in reorder=false at this point, so played inorder canvas.insertReorderBarrier(true); // reorder a node ahead of drawrect op drawOrderedRect(&canvas, 11); drawOrderedNode(&canvas, 10, -1.0f); canvas.insertReorderBarrier(false); canvas.insertReorderBarrier(true); // test with two empty reorder sections canvas.insertReorderBarrier(true); canvas.insertReorderBarrier(false); drawOrderedRect(&canvas, 12); }); // create a canvas not backed by any device/pixels, but with dimensions to avoid quick rejection ZReorderCanvas canvas(100, 100); RenderNodeDrawable drawable(parent.get(), &canvas, false); canvas.drawDrawable(&drawable); EXPECT_EQ(13, canvas.getIndex()); } TEST(RenderNodeDrawable, composeOnLayer) { auto surface = SkSurface::MakeRasterN32Premul(1, 1); SkCanvas& canvas = *surface->getCanvas(); canvas.drawColor(SK_ColorBLUE, SkBlendMode::kSrcOver); ASSERT_EQ(TestUtils::getColor(surface, 0, 0), SK_ColorBLUE); auto rootNode = TestUtils::createSkiaNode( 0, 0, 1, 1, [](RenderProperties& props, SkiaRecordingCanvas& recorder) { recorder.drawColor(SK_ColorRED, SkBlendMode::kSrcOver); }); // attach a layer to the render node auto surfaceLayer = SkSurface::MakeRasterN32Premul(1, 1); auto canvas2 = surfaceLayer->getCanvas(); canvas2->drawColor(SK_ColorWHITE, SkBlendMode::kSrcOver); rootNode->setLayerSurface(surfaceLayer); RenderNodeDrawable drawable1(rootNode.get(), &canvas, false); canvas.drawDrawable(&drawable1); ASSERT_EQ(SK_ColorRED, TestUtils::getColor(surface, 0, 0)); RenderNodeDrawable drawable2(rootNode.get(), &canvas, true); canvas.drawDrawable(&drawable2); ASSERT_EQ(SK_ColorWHITE, TestUtils::getColor(surface, 0, 0)); RenderNodeDrawable drawable3(rootNode.get(), &canvas, false); canvas.drawDrawable(&drawable3); ASSERT_EQ(SK_ColorRED, TestUtils::getColor(surface, 0, 0)); rootNode->setLayerSurface(sk_sp<SkSurface>()); } namespace { static SkRect getRecorderClipBounds(const SkiaRecordingCanvas& recorder) { SkRect clipBounds; recorder.getClipBounds(&clipBounds); return clipBounds; } static SkMatrix getRecorderMatrix(const SkiaRecordingCanvas& recorder) { SkMatrix matrix; recorder.getMatrix(&matrix); return matrix; } } TEST(RenderNodeDrawable, saveLayerClipAndMatrixRestore) { auto surface = SkSurface::MakeRasterN32Premul(400, 800); SkCanvas& canvas = *surface->getCanvas(); canvas.drawColor(SK_ColorWHITE, SkBlendMode::kSrcOver); ASSERT_EQ(TestUtils::getColor(surface, 0, 0), SK_ColorWHITE); auto rootNode = TestUtils::createSkiaNode( 0, 0, 400, 800, [](RenderProperties& props, SkiaRecordingCanvas& recorder) { SkPaint layerPaint; ASSERT_EQ(SkRect::MakeLTRB(0, 0, 400, 800), getRecorderClipBounds(recorder)); EXPECT_TRUE(getRecorderMatrix(recorder).isIdentity()); // note we don't pass SaveFlags::MatrixClip, but matrix and clip will be saved recorder.saveLayer(0, 0, 400, 400, &layerPaint, SaveFlags::ClipToLayer); ASSERT_EQ(SkRect::MakeLTRB(0, 0, 400, 400), getRecorderClipBounds(recorder)); EXPECT_TRUE(getRecorderMatrix(recorder).isIdentity()); recorder.clipRect(50, 50, 350, 350, SkClipOp::kIntersect); ASSERT_EQ(SkRect::MakeLTRB(50, 50, 350, 350), getRecorderClipBounds(recorder)); recorder.translate(300.0f, 400.0f); EXPECT_EQ(SkMatrix::MakeTrans(300.0f, 400.0f), getRecorderMatrix(recorder)); recorder.restore(); ASSERT_EQ(SkRect::MakeLTRB(0, 0, 400, 800), getRecorderClipBounds(recorder)); EXPECT_TRUE(getRecorderMatrix(recorder).isIdentity()); SkPaint paint; paint.setAntiAlias(true); paint.setColor(SK_ColorGREEN); recorder.drawRect(0.0f, 400.0f, 400.0f, 800.0f, paint); }); RenderNodeDrawable drawable(rootNode.get(), &canvas, true); canvas.drawDrawable(&drawable); ASSERT_EQ(SK_ColorGREEN, TestUtils::getColor(surface, 200, 600)); } namespace { class ContextFactory : public IContextFactory { public: virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override { return new AnimationContext(clock); } }; } // end anonymous namespace RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorder) { static const int SCROLL_X = 5; static const int SCROLL_Y = 10; class ProjectionTestCanvas : public SkCanvas { public: ProjectionTestCanvas(int width, int height) : SkCanvas(width, height) {} void onDrawRect(const SkRect& rect, const SkPaint& paint) override { const int index = mDrawCounter++; SkMatrix expectedMatrix; ; switch (index) { case 0: // this is node "B" EXPECT_EQ(SkRect::MakeWH(100, 100), rect); EXPECT_EQ(SK_ColorWHITE, paint.getColor()); expectedMatrix.reset(); EXPECT_EQ(SkRect::MakeLTRB(0, 0, 100, 100), TestUtils::getClipBounds(this)); break; case 1: // this is node "P" EXPECT_EQ(SkRect::MakeLTRB(-10, -10, 60, 60), rect); EXPECT_EQ(SK_ColorDKGRAY, paint.getColor()); expectedMatrix.setTranslate(50 - SCROLL_X, 50 - SCROLL_Y); EXPECT_EQ(SkRect::MakeLTRB(-35, -30, 45, 50), TestUtils::getLocalClipBounds(this)); break; case 2: // this is node "C" EXPECT_EQ(SkRect::MakeWH(100, 50), rect); EXPECT_EQ(SK_ColorBLUE, paint.getColor()); expectedMatrix.setTranslate(-SCROLL_X, 50 - SCROLL_Y); EXPECT_EQ(SkRect::MakeLTRB(0, 40, 95, 90), TestUtils::getClipBounds(this)); break; default: ADD_FAILURE(); } EXPECT_EQ(expectedMatrix, getTotalMatrix()); } int getIndex() { return mDrawCounter; } protected: int mDrawCounter = 0; }; /** * Construct a tree of nodes, where the root (A) has a receiver background (B), and a child (C) * with a projecting child (P) of its own. P would normally draw between B and C's "background" * draw, but because it is projected backwards, it's drawn in between B and C. * * The parent is scrolled by SCROLL_X/SCROLL_Y, but this does not affect the background * (which isn't affected by scroll). */ auto receiverBackground = TestUtils::createSkiaNode( 0, 0, 100, 100, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { properties.setProjectionReceiver(true); // scroll doesn't apply to background, so undone via translationX/Y // NOTE: translationX/Y only! no other transform properties may be set for a proj // receiver! properties.setTranslationX(SCROLL_X); properties.setTranslationY(SCROLL_Y); SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }, "B"); auto projectingRipple = TestUtils::createSkiaNode( 50, 0, 100, 50, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { properties.setProjectBackwards(true); properties.setClipToBounds(false); SkPaint paint; paint.setColor(SK_ColorDKGRAY); canvas.drawRect(-10, -10, 60, 60, paint); }, "P"); auto child = TestUtils::createSkiaNode( 0, 50, 100, 100, [&projectingRipple](RenderProperties& properties, SkiaRecordingCanvas& canvas) { SkPaint paint; paint.setColor(SK_ColorBLUE); canvas.drawRect(0, 0, 100, 50, paint); canvas.drawRenderNode(projectingRipple.get()); }, "C"); auto parent = TestUtils::createSkiaNode( 0, 0, 100, 100, [&receiverBackground, &child](RenderProperties& properties, SkiaRecordingCanvas& canvas) { // Set a rect outline for the projecting ripple to be masked against. properties.mutableOutline().setRoundRect(10, 10, 90, 90, 5, 1.0f); canvas.save(SaveFlags::MatrixClip); canvas.translate(-SCROLL_X, -SCROLL_Y); // Apply scroll (note: bg undoes this internally) canvas.drawRenderNode(receiverBackground.get()); canvas.drawRenderNode(child.get()); canvas.restore(); }, "A"); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( CanvasContext::create(renderThread, false, parent.get(), &contextFactory)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; parent->prepareTree(info); // parent(A) -> (receiverBackground, child) // child(C) -> (rect[0, 0, 100, 50], projectingRipple) // projectingRipple(P) -> (rect[-10, -10, 60, 60]) -> projects backwards // receiverBackground(B) -> (rect[0, 0, 100, 100]) -> projection receiver // create a canvas not backed by any device/pixels, but with dimensions to avoid quick rejection ProjectionTestCanvas canvas(100, 100); RenderNodeDrawable drawable(parent.get(), &canvas, true); canvas.drawDrawable(&drawable); EXPECT_EQ(3, canvas.getIndex()); } RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, emptyReceiver) { class ProjectionTestCanvas : public SkCanvas { public: ProjectionTestCanvas(int width, int height) : SkCanvas(width, height) {} void onDrawRect(const SkRect& rect, const SkPaint& paint) override { mDrawCounter++; } int getDrawCounter() { return mDrawCounter; } private: int mDrawCounter = 0; }; auto receiverBackground = TestUtils::createSkiaNode( 0, 0, 100, 100, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { properties.setProjectionReceiver(true); }, "B"); // a receiver with an empty display list auto projectingRipple = TestUtils::createSkiaNode( 0, 0, 100, 100, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { properties.setProjectBackwards(true); properties.setClipToBounds(false); SkPaint paint; canvas.drawRect(0, 0, 100, 100, paint); }, "P"); auto child = TestUtils::createSkiaNode( 0, 0, 100, 100, [&projectingRipple](RenderProperties& properties, SkiaRecordingCanvas& canvas) { SkPaint paint; canvas.drawRect(0, 0, 100, 100, paint); canvas.drawRenderNode(projectingRipple.get()); }, "C"); auto parent = TestUtils::createSkiaNode(0, 0, 100, 100, [&receiverBackground, &child](RenderProperties& properties, SkiaRecordingCanvas& canvas) { canvas.drawRenderNode(receiverBackground.get()); canvas.drawRenderNode(child.get()); }, "A"); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( CanvasContext::create(renderThread, false, parent.get(), &contextFactory)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; parent->prepareTree(info); // parent(A) -> (receiverBackground, child) // child(C) -> (rect[0, 0, 100, 100], projectingRipple) // projectingRipple(P) -> (rect[0, 0, 100, 100]) -> projects backwards // receiverBackground(B) -> (empty) -> projection receiver // create a canvas not backed by any device/pixels, but with dimensions to avoid quick rejection ProjectionTestCanvas canvas(100, 100); RenderNodeDrawable drawable(parent.get(), &canvas, true); canvas.drawDrawable(&drawable); EXPECT_EQ(2, canvas.getDrawCounter()); } RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, projectionHwLayer) { /* R is backward projected on B and C is a layer. A / \ B C | R */ static const int SCROLL_X = 5; static const int SCROLL_Y = 10; static const int CANVAS_WIDTH = 400; static const int CANVAS_HEIGHT = 400; static const int LAYER_WIDTH = 200; static const int LAYER_HEIGHT = 200; class ProjectionTestCanvas : public SkCanvas { public: ProjectionTestCanvas(int* drawCounter) : SkCanvas(CANVAS_WIDTH, CANVAS_HEIGHT), mDrawCounter(drawCounter) {} void onDrawArc(const SkRect&, SkScalar startAngle, SkScalar sweepAngle, bool useCenter, const SkPaint&) override { EXPECT_EQ(0, (*mDrawCounter)++); // part of painting the layer EXPECT_EQ(SkRect::MakeLTRB(0, 0, LAYER_WIDTH, LAYER_HEIGHT), TestUtils::getClipBounds(this)); } void onDrawRect(const SkRect& rect, const SkPaint& paint) override { EXPECT_EQ(1, (*mDrawCounter)++); EXPECT_EQ(SkRect::MakeLTRB(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT), TestUtils::getClipBounds(this)); } void onDrawOval(const SkRect&, const SkPaint&) override { EXPECT_EQ(2, (*mDrawCounter)++); SkMatrix expectedMatrix; expectedMatrix.setTranslate(100 - SCROLL_X, 100 - SCROLL_Y); EXPECT_EQ(expectedMatrix, getTotalMatrix()); EXPECT_EQ(SkRect::MakeLTRB(-85, -80, 295, 300), TestUtils::getLocalClipBounds(this)); } int* mDrawCounter; }; class ProjectionLayer : public SkSurface_Base { public: ProjectionLayer(int* drawCounter) : SkSurface_Base(SkImageInfo::MakeN32Premul(LAYER_WIDTH, LAYER_HEIGHT), nullptr) , mDrawCounter(drawCounter) {} virtual sk_sp<SkImage> onNewImageSnapshot(const SkIRect* bounds) override { EXPECT_EQ(3, (*mDrawCounter)++); EXPECT_EQ(SkRect::MakeLTRB(100 - SCROLL_X, 100 - SCROLL_Y, 300 - SCROLL_X, 300 - SCROLL_Y), TestUtils::getClipBounds(this->getCanvas())); return nullptr; } SkCanvas* onNewCanvas() override { return new ProjectionTestCanvas(mDrawCounter); } sk_sp<SkSurface> onNewSurface(const SkImageInfo&) override { return nullptr; } void onCopyOnWrite(ContentChangeMode) override {} int* mDrawCounter; void onWritePixels(const SkPixmap&, int x, int y) {} }; auto receiverBackground = TestUtils::createSkiaNode( 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { properties.setProjectionReceiver(true); // scroll doesn't apply to background, so undone via translationX/Y // NOTE: translationX/Y only! no other transform properties may be set for a proj // receiver! properties.setTranslationX(SCROLL_X); properties.setTranslationY(SCROLL_Y); canvas.drawRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, SkPaint()); }, "B"); // B auto projectingRipple = TestUtils::createSkiaNode( 0, 0, LAYER_WIDTH, LAYER_HEIGHT, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { properties.setProjectBackwards(true); properties.setClipToBounds(false); canvas.drawOval(100, 100, 300, 300, SkPaint()); // drawn mostly out of layer bounds }, "R"); // R auto child = TestUtils::createSkiaNode( 100, 100, 300, 300, [&projectingRipple](RenderProperties& properties, SkiaRecordingCanvas& canvas) { canvas.drawRenderNode(projectingRipple.get()); canvas.drawArc(0, 0, LAYER_WIDTH, LAYER_HEIGHT, 0.0f, 280.0f, true, SkPaint()); }, "C"); // C auto parent = TestUtils::createSkiaNode( 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [&receiverBackground, &child](RenderProperties& properties, SkiaRecordingCanvas& canvas) { // Set a rect outline for the projecting ripple to be masked against. properties.mutableOutline().setRoundRect(10, 10, 390, 390, 0, 1.0f); canvas.translate(-SCROLL_X, -SCROLL_Y); // Apply scroll (note: bg undoes this internally) canvas.drawRenderNode(receiverBackground.get()); canvas.drawRenderNode(child.get()); }, "A"); // A // prepareTree is required to find, which receivers have backward projected nodes ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( CanvasContext::create(renderThread, false, parent.get(), &contextFactory)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; parent->prepareTree(info); int drawCounter = 0; // set a layer after prepareTree to avoid layer logic there child->animatorProperties().mutateLayerProperties().setType(LayerType::RenderLayer); sk_sp<SkSurface> surfaceLayer1(new ProjectionLayer(&drawCounter)); child->setLayerSurface(surfaceLayer1); Matrix4 windowTransform; windowTransform.loadTranslate(100, 100, 0); child->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); LayerUpdateQueue layerUpdateQueue; layerUpdateQueue.enqueueLayerWithDamage(child.get(), android::uirenderer::Rect(LAYER_WIDTH, LAYER_HEIGHT)); auto pipeline = std::make_unique<SkiaOpenGLPipeline>(renderThread); pipeline->renderLayersImpl(layerUpdateQueue, true); EXPECT_EQ(1, drawCounter); // assert index 0 is drawn on the layer RenderNodeDrawable drawable(parent.get(), surfaceLayer1->getCanvas(), true); surfaceLayer1->getCanvas()->drawDrawable(&drawable); EXPECT_EQ(4, drawCounter); // clean up layer pointer, so we can safely destruct RenderNode child->setLayerSurface(nullptr); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionChildScroll) { /* R is backward projected on B. A / \ B C | R */ static const int SCROLL_X = 500000; static const int SCROLL_Y = 0; static const int CANVAS_WIDTH = 400; static const int CANVAS_HEIGHT = 400; class ProjectionChildScrollTestCanvas : public SkCanvas { public: ProjectionChildScrollTestCanvas() : SkCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) {} void onDrawRect(const SkRect& rect, const SkPaint& paint) override { EXPECT_EQ(0, mDrawCounter++); EXPECT_TRUE(getTotalMatrix().isIdentity()); } void onDrawOval(const SkRect&, const SkPaint&) override { EXPECT_EQ(1, mDrawCounter++); EXPECT_EQ(SkRect::MakeWH(CANVAS_WIDTH, CANVAS_HEIGHT), TestUtils::getClipBounds(this)); EXPECT_TRUE(getTotalMatrix().isIdentity()); } int mDrawCounter = 0; }; auto receiverBackground = TestUtils::createSkiaNode( 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { properties.setProjectionReceiver(true); canvas.drawRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, SkPaint()); }, "B"); // B auto projectingRipple = TestUtils::createSkiaNode( 0, 0, 200, 200, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { // scroll doesn't apply to background, so undone via translationX/Y // NOTE: translationX/Y only! no other transform properties may be set for a proj // receiver! properties.setTranslationX(SCROLL_X); properties.setTranslationY(SCROLL_Y); properties.setProjectBackwards(true); properties.setClipToBounds(false); canvas.drawOval(0, 0, 200, 200, SkPaint()); }, "R"); // R auto child = TestUtils::createSkiaNode( 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [&projectingRipple](RenderProperties& properties, SkiaRecordingCanvas& canvas) { // Record time clip will be ignored by projectee canvas.clipRect(100, 100, 300, 300, SkClipOp::kIntersect); canvas.translate(-SCROLL_X, -SCROLL_Y); // Apply scroll (note: bg undoes this internally) canvas.drawRenderNode(projectingRipple.get()); }, "C"); // C auto parent = TestUtils::createSkiaNode(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [&receiverBackground, &child](RenderProperties& properties, SkiaRecordingCanvas& canvas) { canvas.drawRenderNode(receiverBackground.get()); canvas.drawRenderNode(child.get()); }, "A"); // A // prepareTree is required to find, which receivers have backward projected nodes ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( CanvasContext::create(renderThread, false, parent.get(), &contextFactory)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; parent->prepareTree(info); std::unique_ptr<ProjectionChildScrollTestCanvas> canvas(new ProjectionChildScrollTestCanvas()); RenderNodeDrawable drawable(parent.get(), canvas.get(), true); canvas->drawDrawable(&drawable); EXPECT_EQ(2, canvas->mDrawCounter); } namespace { static int drawNode(RenderThread& renderThread, const sp<RenderNode>& renderNode) { ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( CanvasContext::create(renderThread, false, renderNode.get(), &contextFactory)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; renderNode->prepareTree(info); // create a canvas not backed by any device/pixels, but with dimensions to avoid quick rejection ZReorderCanvas canvas(100, 100); RenderNodeDrawable drawable(renderNode.get(), &canvas, false); canvas.drawDrawable(&drawable); return canvas.getIndex(); } } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderProjectedInMiddle) { /* R is backward projected on B A / \ B C | R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectionReceiver(true); }); // nodeB drawOrderedNode(&canvas, 2, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeC }); // nodeA EXPECT_EQ(3, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderProjectLast) { /* R is backward projected on E A / | \ / | \ B C E | R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, nullptr); // nodeB drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 3, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // drawn as 2 props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeC drawOrderedNode(&canvas, 2, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // drawn as 3 props.setProjectionReceiver(true); }); // nodeE }); // nodeA EXPECT_EQ(4, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderNoReceivable) { /* R is backward projected without receiver A / \ B C | R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, nullptr); // nodeB drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 255, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // not having a projection receiver is an undefined behavior props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeC }); // nodeA EXPECT_EQ(2, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderParentReceivable) { /* R is backward projected on C A / \ B C | R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, nullptr); // nodeB drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectionReceiver(true); drawOrderedNode(&canvas, 2, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeC }); // nodeA EXPECT_EQ(3, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderSameNodeReceivable) { /* R is backward projected on R A / \ B C | R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, nullptr); // nodeB drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 255, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // having a node that is projected on itself is an undefined/unexpected behavior props.setProjectionReceiver(true); props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeC }); // nodeA EXPECT_EQ(2, drawNode(renderThread, nodeA)); } // Note: the outcome for this test is different in HWUI RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderProjectedSibling) { /* R is set to project on B, but R is not drawn because projecting on a sibling is not allowed. A /|\ / | \ B C R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectionReceiver(true); }); // nodeB drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) {}); // nodeC drawOrderedNode(&canvas, 255, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeA EXPECT_EQ(2, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderProjectedSibling2) { /* R is set to project on B, but R is not drawn because projecting on a sibling is not allowed. A | G /|\ / | \ B C R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectionReceiver(true); }); // nodeB drawOrderedNode(&canvas, 2, [](RenderProperties& props, SkiaRecordingCanvas& canvas) {}); // nodeC drawOrderedNode(&canvas, 255, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeG }); // nodeA EXPECT_EQ(3, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderGrandparentReceivable) { /* R is backward projected on B A | B | C | R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectionReceiver(true); drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 2, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeC }); // nodeB }); // nodeA EXPECT_EQ(3, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderTwoReceivables) { /* B and G are receivables, R is backward projected A / \ B C / \ G R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // B props.setProjectionReceiver(true); }); // nodeB drawOrderedNode(&canvas, 2, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // C drawOrderedNode(&canvas, 3, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // G props.setProjectionReceiver(true); }); // nodeG drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // R props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeC }); // nodeA EXPECT_EQ(4, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderTwoReceivablesLikelyScenario) { /* B and G are receivables, G is backward projected A / \ B C / \ G R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // B props.setProjectionReceiver(true); }); // nodeB drawOrderedNode(&canvas, 2, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // C drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // G props.setProjectionReceiver(true); props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeG drawOrderedNode(&canvas, 3, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // R }); // nodeR }); // nodeC }); // nodeA EXPECT_EQ(4, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorderTwoReceivablesDeeper) { /* B and G are receivables, R is backward projected A / \ B C / \ G D | R */ auto nodeA = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // B props.setProjectionReceiver(true); }); // nodeB drawOrderedNode(&canvas, 1, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // C drawOrderedNode(&canvas, 2, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // G props.setProjectionReceiver(true); }); // nodeG drawOrderedNode(&canvas, 4, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // D drawOrderedNode(&canvas, 3, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { // R props.setProjectBackwards(true); props.setClipToBounds(false); }); // nodeR }); // nodeD }); // nodeC }); // nodeA EXPECT_EQ(5, drawNode(renderThread, nodeA)); } RENDERTHREAD_TEST(RenderNodeDrawable, simple) { static const int CANVAS_WIDTH = 100; static const int CANVAS_HEIGHT = 200; class SimpleTestCanvas : public TestCanvasBase { public: SimpleTestCanvas() : TestCanvasBase(CANVAS_WIDTH, CANVAS_HEIGHT) {} void onDrawRect(const SkRect& rect, const SkPaint& paint) override { EXPECT_EQ(0, mDrawCounter++); } void onDrawImage(const SkImage*, SkScalar dx, SkScalar dy, const SkPaint*) override { EXPECT_EQ(1, mDrawCounter++); } }; auto node = TestUtils::createSkiaNode(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { sk_sp<Bitmap> bitmap(TestUtils::createBitmap(25, 25)); canvas.drawRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, SkPaint()); canvas.drawBitmap(*bitmap, 10, 10, nullptr); }); SimpleTestCanvas canvas; RenderNodeDrawable drawable(node.get(), &canvas, true); canvas.drawDrawable(&drawable); EXPECT_EQ(2, canvas.mDrawCounter); } RENDERTHREAD_TEST(RenderNodeDrawable, colorOp_unbounded) { static const int CANVAS_WIDTH = 200; static const int CANVAS_HEIGHT = 200; class ColorTestCanvas : public TestCanvasBase { public: ColorTestCanvas() : TestCanvasBase(CANVAS_WIDTH, CANVAS_HEIGHT) {} void onDrawPaint(const SkPaint&) { switch (mDrawCounter++) { case 0: EXPECT_EQ(SkRect::MakeWH(CANVAS_WIDTH, CANVAS_HEIGHT), TestUtils::getClipBounds(this)); break; case 1: EXPECT_EQ(SkRect::MakeWH(10, 10), TestUtils::getClipBounds(this)); break; default: ADD_FAILURE(); } } }; auto unclippedColorView = TestUtils::createSkiaNode( 0, 0, 10, 10, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setClipToBounds(false); canvas.drawColor(SK_ColorWHITE, SkBlendMode::kSrcOver); }); auto clippedColorView = TestUtils::createSkiaNode( 0, 0, 10, 10, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { canvas.drawColor(SK_ColorWHITE, SkBlendMode::kSrcOver); }); ColorTestCanvas canvas; RenderNodeDrawable drawable(unclippedColorView.get(), &canvas, true); canvas.drawDrawable(&drawable); EXPECT_EQ(1, canvas.mDrawCounter); RenderNodeDrawable drawable2(clippedColorView.get(), &canvas, true); canvas.drawDrawable(&drawable2); EXPECT_EQ(2, canvas.mDrawCounter); } TEST(RenderNodeDrawable, renderNode) { static const int CANVAS_WIDTH = 200; static const int CANVAS_HEIGHT = 200; class RenderNodeTestCanvas : public TestCanvasBase { public: RenderNodeTestCanvas() : TestCanvasBase(CANVAS_WIDTH, CANVAS_HEIGHT) {} void onDrawRect(const SkRect& rect, const SkPaint& paint) override { switch (mDrawCounter++) { case 0: EXPECT_EQ(SkRect::MakeWH(CANVAS_WIDTH, CANVAS_HEIGHT), TestUtils::getClipBounds(this)); EXPECT_EQ(SK_ColorDKGRAY, paint.getColor()); break; case 1: EXPECT_EQ(SkRect::MakeLTRB(50, 50, 150, 150), TestUtils::getClipBounds(this)); EXPECT_EQ(SK_ColorWHITE, paint.getColor()); break; default: ADD_FAILURE(); } } }; auto child = TestUtils::createSkiaNode( 10, 10, 110, 110, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }); auto parent = TestUtils::createSkiaNode( 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [&child](RenderProperties& props, SkiaRecordingCanvas& canvas) { SkPaint paint; paint.setColor(SK_ColorDKGRAY); canvas.drawRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, paint); canvas.save(SaveFlags::MatrixClip); canvas.translate(40, 40); canvas.drawRenderNode(child.get()); canvas.restore(); }); RenderNodeTestCanvas canvas; RenderNodeDrawable drawable(parent.get(), &canvas, true); canvas.drawDrawable(&drawable); EXPECT_EQ(2, canvas.mDrawCounter); } // Verify that layers are composed with kLow_SkFilterQuality filter quality. RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, layerComposeQuality) { static const int CANVAS_WIDTH = 1; static const int CANVAS_HEIGHT = 1; static const int LAYER_WIDTH = 1; static const int LAYER_HEIGHT = 1; class FrameTestCanvas : public TestCanvasBase { public: FrameTestCanvas() : TestCanvasBase(CANVAS_WIDTH, CANVAS_HEIGHT) {} void onDrawImageRect(const SkImage* image, const SkRect* src, const SkRect& dst, const SkPaint* paint, SrcRectConstraint constraint) override { mDrawCounter++; EXPECT_EQ(kLow_SkFilterQuality, paint->getFilterQuality()); } }; auto layerNode = TestUtils::createSkiaNode( 0, 0, LAYER_WIDTH, LAYER_HEIGHT, [](RenderProperties& properties, SkiaRecordingCanvas& canvas) { canvas.drawPaint(SkPaint()); }); layerNode->animatorProperties().mutateLayerProperties().setType(LayerType::RenderLayer); layerNode->setLayerSurface(SkSurface::MakeRasterN32Premul(LAYER_WIDTH, LAYER_HEIGHT)); FrameTestCanvas canvas; RenderNodeDrawable drawable(layerNode.get(), &canvas, true); canvas.drawDrawable(&drawable); EXPECT_EQ(1, canvas.mDrawCounter); // make sure the layer was composed // clean up layer pointer, so we can safely destruct RenderNode layerNode->setLayerSurface(nullptr); } TEST(ReorderBarrierDrawable, testShadowMatrix) { static const int CANVAS_WIDTH = 100; static const int CANVAS_HEIGHT = 100; static const float TRANSLATE_X = 11.0f; static const float TRANSLATE_Y = 22.0f; static const float CASTER_X = 40.0f; static const float CASTER_Y = 40.0f; static const float CASTER_WIDTH = 20.0f; static const float CASTER_HEIGHT = 20.0f; class ShadowTestCanvas : public SkCanvas { public: ShadowTestCanvas(int width, int height) : SkCanvas(width, height) {} int getDrawCounter() { return mDrawCounter; } virtual void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override { // expect to draw 2 RenderNodeDrawable, 1 StartReorderBarrierDrawable, // 1 EndReorderBarrierDrawable mDrawCounter++; SkCanvas::onDrawDrawable(drawable, matrix); } virtual void didTranslate(SkScalar dx, SkScalar dy) override { mDrawCounter++; EXPECT_EQ(dx, TRANSLATE_X); EXPECT_EQ(dy, TRANSLATE_Y); } virtual void didSetMatrix(const SkMatrix& matrix) override { mDrawCounter++; // First invocation is EndReorderBarrierDrawable::drawShadow to apply shadow matrix. // Second invocation is preparing the matrix for an elevated RenderNodeDrawable. EXPECT_TRUE(matrix.isIdentity()); EXPECT_TRUE(getTotalMatrix().isIdentity()); } virtual void didConcat(const SkMatrix& matrix) override { mDrawCounter++; if (mFirstDidConcat) { // First invocation is EndReorderBarrierDrawable::drawShadow to apply shadow matrix. mFirstDidConcat = false; EXPECT_EQ(SkMatrix::MakeTrans(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), matrix); EXPECT_EQ(SkMatrix::MakeTrans(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), getTotalMatrix()); } else { // Second invocation is preparing the matrix for an elevated RenderNodeDrawable. EXPECT_EQ(SkMatrix::MakeTrans(TRANSLATE_X, TRANSLATE_Y), matrix); EXPECT_EQ(SkMatrix::MakeTrans(TRANSLATE_X, TRANSLATE_Y), getTotalMatrix()); } } protected: int mDrawCounter = 0; private: bool mFirstDidConcat = true; }; auto parent = TestUtils::createSkiaNode( 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { canvas.translate(TRANSLATE_X, TRANSLATE_Y); canvas.insertReorderBarrier(true); auto node = TestUtils::createSkiaNode( CASTER_X, CASTER_Y, CASTER_X + CASTER_WIDTH, CASTER_Y + CASTER_HEIGHT, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { props.setElevation(42); props.mutableOutline().setRoundRect(0, 0, 20, 20, 5, 1); props.mutableOutline().setShouldClip(true); }); canvas.drawRenderNode(node.get()); canvas.insertReorderBarrier(false); }); // create a canvas not backed by any device/pixels, but with dimensions to avoid quick rejection ShadowTestCanvas canvas(CANVAS_WIDTH, CANVAS_HEIGHT); RenderNodeDrawable drawable(parent.get(), &canvas, false); canvas.drawDrawable(&drawable); EXPECT_EQ(9, canvas.getDrawCounter()); } // Draw a vector drawable twice but with different bounds and verify correct bounds are used. RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaRecordingCanvas, drawVectorDrawable) { static const int CANVAS_WIDTH = 100; static const int CANVAS_HEIGHT = 200; class VectorDrawableTestCanvas : public TestCanvasBase { public: VectorDrawableTestCanvas() : TestCanvasBase(CANVAS_WIDTH, CANVAS_HEIGHT) {} void onDrawBitmapRect(const SkBitmap& bitmap, const SkRect* src, const SkRect& dst, const SkPaint* paint, SrcRectConstraint constraint) override { const int index = mDrawCounter++; switch (index) { case 0: EXPECT_EQ(dst, SkRect::MakeWH(CANVAS_WIDTH, CANVAS_HEIGHT)); break; case 1: EXPECT_EQ(dst, SkRect::MakeWH(CANVAS_WIDTH / 2, CANVAS_HEIGHT)); break; default: ADD_FAILURE(); } } }; VectorDrawable::Group* group = new VectorDrawable::Group(); sp<VectorDrawableRoot> vectorDrawable(new VectorDrawableRoot(group)); vectorDrawable->mutateStagingProperties()->setScaledSize(CANVAS_WIDTH / 10, CANVAS_HEIGHT / 10); auto node = TestUtils::createSkiaNode(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [&](RenderProperties& props, SkiaRecordingCanvas& canvas) { vectorDrawable->mutateStagingProperties()->setBounds( SkRect::MakeWH(CANVAS_WIDTH, CANVAS_HEIGHT)); canvas.drawVectorDrawable(vectorDrawable.get()); vectorDrawable->mutateStagingProperties()->setBounds( SkRect::MakeWH(CANVAS_WIDTH / 2, CANVAS_HEIGHT)); canvas.drawVectorDrawable(vectorDrawable.get()); }); VectorDrawableTestCanvas canvas; RenderNodeDrawable drawable(node.get(), &canvas, true); canvas.drawDrawable(&drawable); EXPECT_EQ(2, canvas.mDrawCounter); }