| /* |
| * 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 <gtest/gtest.h> |
| #include <VectorDrawable.h> |
| |
| #include "AnimationContext.h" |
| #include "DamageAccumulator.h" |
| #include "IContextFactory.h" |
| #include "pipeline/skia/SkiaDisplayList.h" |
| #include "pipeline/skia/SkiaPipeline.h" |
| #include "pipeline/skia/SkiaRecordingCanvas.h" |
| #include "renderthread/CanvasContext.h" |
| #include "tests/common/TestUtils.h" |
| #include "SkiaCanvas.h" |
| #include <SkSurface_Base.h> |
| #include <SkLiteRecorder.h> |
| #include <SkClipStack.h> |
| #include "FatalTestCanvas.h" |
| #include <string.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); |
| }); |
| |
| SkLiteDL skLiteDL; |
| SkLiteRecorder 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_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) { |
| } |
| void onDraw(SkCanvas*, SkScalar x, SkScalar y, const SkPaint*) 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())); |
| } |
| SkCanvas* onNewCanvas() override { |
| return new ProjectionTestCanvas(mDrawCounter); |
| } |
| sk_sp<SkSurface> onNewSurface(const SkImageInfo&) override { |
| return sk_sp<SkSurface>(); |
| } |
| sk_sp<SkImage> onNewImageSnapshot(SkBudgeted) override { |
| return sk_sp<SkImage>(); |
| } |
| void onCopyOnWrite(ContentChangeMode) override {} |
| int* mDrawCounter; |
| }; |
| |
| 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)); |
| SkiaPipeline::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); |
| } |
| |