diff options
-rw-r--r-- | libs/hwui/Android.mk | 1 | ||||
-rw-r--r-- | libs/hwui/TreeInfo.h | 3 | ||||
-rw-r--r-- | libs/hwui/pipeline/skia/RenderNodeDrawable.cpp | 99 | ||||
-rw-r--r-- | libs/hwui/pipeline/skia/RenderNodeDrawable.h | 50 | ||||
-rw-r--r-- | libs/hwui/pipeline/skia/SkiaDisplayList.cpp | 21 | ||||
-rw-r--r-- | libs/hwui/pipeline/skia/SkiaDisplayList.h | 25 | ||||
-rw-r--r-- | libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp | 5 | ||||
-rw-r--r-- | libs/hwui/tests/unit/FrameBuilderTests.cpp | 375 | ||||
-rw-r--r-- | libs/hwui/tests/unit/RenderNodeDrawableTests.cpp | 692 | ||||
-rw-r--r-- | libs/hwui/tests/unit/SkiaDisplayListTests.cpp | 9 |
10 files changed, 1147 insertions, 133 deletions
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk index fdf4d52f357b..eff24990a438 100644 --- a/libs/hwui/Android.mk +++ b/libs/hwui/Android.mk @@ -181,6 +181,7 @@ hwui_c_includes += \ external/skia/include/private \ external/skia/src/core \ external/skia/src/effects \ + external/skia/src/image \ external/harfbuzz_ng/src \ external/freetype/include diff --git a/libs/hwui/TreeInfo.h b/libs/hwui/TreeInfo.h index 2087fca205da..749efdd26927 100644 --- a/libs/hwui/TreeInfo.h +++ b/libs/hwui/TreeInfo.h @@ -116,6 +116,9 @@ public: bool canDrawThisFrame = true; } out; + // This flag helps to disable projection for receiver nodes that do not have any backward + // projected children. + bool hasBackwardProjectedNodes = false; // TODO: Damage calculations }; diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index 7dcbbd059e88..da9002d01bb7 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -24,8 +24,41 @@ namespace android { namespace uirenderer { namespace skiapipeline { +void RenderNodeDrawable::drawBackwardsProjectedNodes(SkCanvas* canvas, const SkiaDisplayList& displayList, + int nestLevel) { + LOG_ALWAYS_FATAL_IF(0 == nestLevel && !displayList.mProjectionReceiver); + for (auto& child : displayList.mChildNodes) { + const RenderProperties& childProperties = child.getNodeProperties(); + + //immediate children cannot be projected on their parent + if (childProperties.getProjectBackwards() && nestLevel > 0) { + SkAutoCanvasRestore acr2(canvas, true); + //Apply recorded matrix, which is a total matrix saved at recording time to avoid + //replaying all DL commands. + canvas->concat(child.getRecordedMatrix()); + child.drawContent(canvas); + } + + //skip walking sub-nodes if current display list contains a receiver with exception of + //level 0, which is a known receiver + if (0 == nestLevel || !displayList.containsProjectionReceiver()) { + SkAutoCanvasRestore acr(canvas, true); + SkMatrix nodeMatrix; + mat4 hwuiMatrix(child.getRecordedMatrix()); + auto childNode = child.getRenderNode(); + childNode->applyViewPropertyTransforms(hwuiMatrix); + hwuiMatrix.copyTo(nodeMatrix); + canvas->concat(nodeMatrix); + SkiaDisplayList* childDisplayList = static_cast<SkiaDisplayList*>( + (const_cast<DisplayList*>(childNode->getDisplayList()))); + if (childDisplayList) { + drawBackwardsProjectedNodes(canvas, *childDisplayList, nestLevel+1); + } + } + } +} + static void clipOutline(const Outline& outline, SkCanvas* canvas, const SkRect* pendingClip) { - SkASSERT(outline.willClip()); Rect possibleRect; float radius; LOG_ALWAYS_FATAL_IF(!outline.getAsRoundRect(&possibleRect, &radius), @@ -74,53 +107,25 @@ void RenderNodeDrawable::forceDraw(SkCanvas* canvas) { SkiaDisplayList* displayList = (SkiaDisplayList*)renderNode->getDisplayList(); SkAutoCanvasRestore acr(canvas, true); - const RenderProperties& properties = this->getNodeProperties(); - if (displayList->mIsProjectionReceiver) { - // this node is a projection receiver. We will gather the projected nodes as we draw our - // children, and then draw them on top of this node's content. - std::vector<ProjectedChild> newList; - for (auto& child : displayList->mChildNodes) { - // our direct children are not supposed to project into us (nodes project to, at the - // nearest, their grandparents). So we "delay" the list's activation one level by - // passing it into mNextProjectedChildrenTarget rather than mProjectedChildrenTarget. - child.mProjectedChildrenTarget = mNextProjectedChildrenTarget; - child.mNextProjectedChildrenTarget = &newList; - } - // draw ourselves and our children. As a side effect, this will add projected nodes to - // newList. - this->drawContent(canvas); - bool willClip = properties.getOutline().willClip(); - if (willClip) { - canvas->save(); - clipOutline(properties.getOutline(), canvas, nullptr); - } - // draw the collected projected nodes - for (auto& projectedChild : newList) { - canvas->setMatrix(projectedChild.matrix); - projectedChild.node->drawContent(canvas); - } - if (willClip) { - canvas->restore(); - } - } else { - if (properties.getProjectBackwards() && mProjectedChildrenTarget) { - // We are supposed to project this node, so add it to the list and do not actually draw - // yet. It will be drawn by its projection receiver. - mProjectedChildrenTarget->push_back({ this, canvas->getTotalMatrix() }); - return; - } - for (auto& child : displayList->mChildNodes) { - // storing these values in the nodes themselves is a bit ugly; they should "really" be - // function parameters, but we have to go through the preexisting draw() method and - // therefore cannot add additional parameters to it - child.mProjectedChildrenTarget = mNextProjectedChildrenTarget; - child.mNextProjectedChildrenTarget = mNextProjectedChildrenTarget; + //pass this outline to the children that may clip backward projected nodes + displayList->mProjectedOutline = displayList->containsProjectionReceiver() + ? &properties.getOutline() : nullptr; + if (!properties.getProjectBackwards()) { + drawContent(canvas); + if (mProjectedDisplayList) { + acr.restore(); //draw projected children using parent matrix + LOG_ALWAYS_FATAL_IF(!mProjectedDisplayList->mProjectedOutline); + const bool shouldClip = mProjectedDisplayList->mProjectedOutline->getPath(); + SkAutoCanvasRestore acr2(canvas, shouldClip); + canvas->setMatrix(mProjectedDisplayList->mProjectedReceiverParentMatrix); + if (shouldClip) { + clipOutline(*mProjectedDisplayList->mProjectedOutline, canvas, nullptr); + } + drawBackwardsProjectedNodes(canvas, *mProjectedDisplayList); } - this->drawContent(canvas); } - mProjectedChildrenTarget = nullptr; - mNextProjectedChildrenTarget = nullptr; + displayList->mProjectedOutline = nullptr; } static bool layerNeedsPaint(const LayerProperties& properties, @@ -148,6 +153,10 @@ void RenderNodeDrawable::drawContent(SkCanvas* canvas) const { if (mComposeLayer) { setViewProperties(properties, canvas, &alphaMultiplier); } + SkiaDisplayList* displayList = (SkiaDisplayList*)mRenderNode->getDisplayList(); + if (displayList->containsProjectionReceiver()) { + displayList->mProjectedReceiverParentMatrix = canvas->getTotalMatrix(); + } //TODO should we let the bound of the drawable do this for us? const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.h b/libs/hwui/pipeline/skia/RenderNodeDrawable.h index a2ffc6c3647b..3eed6476c994 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.h +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.h @@ -29,6 +29,8 @@ class RenderProperties; namespace skiapipeline { +class SkiaDisplayList; + /** * This drawable wraps a RenderNode and enables it to be recorded into a list * of Skia drawing commands. @@ -36,18 +38,6 @@ namespace skiapipeline { class RenderNodeDrawable : public SkDrawable { public: /** - * This struct contains a pointer to a node that is to be - * projected into the drawing order of its closest ancestor - * (excluding its parent) that is marked as a projection - * receiver. The matrix is used to ensure that the node is - * drawn with same matrix as it would have prior to projection. - */ - struct ProjectedChild { - const RenderNodeDrawable* node; - const SkMatrix matrix; - }; - - /** * Creates a new RenderNodeDrawable backed by a render node. * * @param node that has to be drawn @@ -86,6 +76,14 @@ public: */ const SkMatrix& getRecordedMatrix() const { return mRecordedTransform; } + /** + * Sets a pointer to a display list of the parent render node. The display list is used when + * drawing backward projected nodes, when this node is a projection receiver. + */ + void setProjectedDisplayList(SkiaDisplayList* projectedDisplayList) { + mProjectedDisplayList = projectedDisplayList; + } + protected: /* * Return the (conservative) bounds of what the drawable will draw. @@ -108,6 +106,16 @@ private: sp<RenderNode> mRenderNode; /** + * Walks recursively the display list and draws the content of backward projected nodes. + * + * @param canvas used to draw the backward projected nodes + * @param displayList is a display list that contains a projection receiver + * @param nestLevel should be always 0. Used to track how far we are from the receiver. + */ + void drawBackwardsProjectedNodes(SkCanvas* canvas, const SkiaDisplayList& displayList, + int nestLevel = 0); + + /** * Applies the rendering properties of a view onto a SkCanvas. */ static void setViewProperties(const RenderProperties& properties, SkCanvas* canvas, @@ -126,19 +134,6 @@ private: */ const bool mComposeLayer; - /** - * List to which we will add any projected children we encounter while walking our descendents. - * This pointer is valid only while the node (including its children) is actively being drawn. - */ - std::vector<ProjectedChild>* mProjectedChildrenTarget = nullptr; - - /** - * The value to which we should set our children's mProjectedChildrenTarget. We use two pointers - * (mProjectedChildrenTarget and mNextProjectedChildrenTarget) because we need to skip over our - * parent when looking for a projection receiver. - */ - std::vector<ProjectedChild>* mNextProjectedChildrenTarget = nullptr; - /* * True if the render node is in a reordering section */ @@ -148,6 +143,11 @@ private: * Draw the content into a canvas, depending on the render node layer type and mComposeLayer. */ void drawContent(SkCanvas* canvas) const; + + /* + * display list that is searched for any render nodes with getProjectBackwards==true + */ + SkiaDisplayList* mProjectedDisplayList = nullptr; }; }; // namespace skiapipeline diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp index 2ad7f74560d6..9db8cd3fe2e2 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp @@ -64,16 +64,31 @@ bool SkiaDisplayList::prepareListAndChildren(TreeInfo& info, bool functorsNeedLa info.canvasContext.unpinImages(); } + bool hasBackwardProjectedNodesHere = false; + bool hasBackwardProjectedNodesSubtree= false; + for (auto& child : mChildNodes) { + hasBackwardProjectedNodesHere |= child.getNodeProperties().getProjectBackwards(); RenderNode* childNode = child.getRenderNode(); Matrix4 mat4(child.getRecordedMatrix()); info.damageAccumulator->pushTransform(&mat4); // TODO: a layer is needed if the canvas is rotated or has a non-rect clip - bool childFunctorsNeedLayer = functorsNeedLayer; - childFn(childNode, info, childFunctorsNeedLayer); + info.hasBackwardProjectedNodes = false; + childFn(childNode, info, functorsNeedLayer); + hasBackwardProjectedNodesSubtree |= info.hasBackwardProjectedNodes; info.damageAccumulator->popTransform(); } + //The purpose of next block of code is to reset projected display list if there are no + //backward projected nodes. This speeds up drawing, by avoiding an extra walk of the tree + if (mProjectionReceiver) { + mProjectionReceiver->setProjectedDisplayList(hasBackwardProjectedNodesSubtree ? this : nullptr); + info.hasBackwardProjectedNodes = hasBackwardProjectedNodesHere; + } else { + info.hasBackwardProjectedNodes = hasBackwardProjectedNodesSubtree + || hasBackwardProjectedNodesHere; + } + bool isDirty = false; for (auto& vectorDrawable : mVectorDrawables) { // If any vector drawable in the display list needs update, damage the node. @@ -86,7 +101,7 @@ bool SkiaDisplayList::prepareListAndChildren(TreeInfo& info, bool functorsNeedLa } void SkiaDisplayList::reset(SkRect bounds) { - mIsProjectionReceiver = false; + mProjectionReceiver = nullptr; mDrawable->reset(bounds); diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h index f34b48541953..ff86fd18a9a4 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.h +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h @@ -26,6 +26,9 @@ namespace android { namespace uirenderer { + +class Outline; + namespace skiapipeline { /** @@ -119,6 +122,11 @@ public: void updateChildren(std::function<void(RenderNode*)> updateFn) override; /** + * Returns true if there is a child render node that is a projection receiver. + */ + inline bool containsProjectionReceiver() const { return mProjectionReceiver; } + + /** * We use std::deque here because (1) we need to iterate through these * elements and (2) mDrawable holds pointers to the elements, so they cannot * relocate. @@ -129,7 +137,22 @@ public: std::vector<VectorDrawableRoot*> mVectorDrawables; sk_sp<SkLiteDL> mDrawable; - bool mIsProjectionReceiver = false; + //mProjectionReceiver points to a child node (stored in mChildNodes) that is as a projection + //receiver. It is set at record time and used at both prepare and draw tree traversals to + //make sure backward projected nodes are found and drawn immediately after mProjectionReceiver. + RenderNodeDrawable* mProjectionReceiver = nullptr; + + //mProjectedOutline is valid only when render node tree is traversed during the draw pass. + //Render nodes that have a child receiver node, will store a pointer to their outline in + //mProjectedOutline. Child receiver node will apply the clip before any backward projected + //node is drawn. + const Outline* mProjectedOutline = nullptr; + + //mProjectedReceiverParentMatrix is valid when render node tree is traversed during the draw + //pass. Render nodes that have a child receiver node, will store their matrix in + //mProjectedReceiverParentMatrix. Child receiver node will set the matrix and then clip with the + //outline of their parent. + SkMatrix mProjectedReceiverParentMatrix; }; }; // namespace skiapipeline diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index 621816add1ed..6df544fa9e77 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -108,11 +108,12 @@ void SkiaRecordingCanvas::drawRenderNode(uirenderer::RenderNode* renderNode) { // record the child node mDisplayList->mChildNodes.emplace_back(renderNode, asSkCanvas(), true, mCurrentBarrier); - drawDrawable(&mDisplayList->mChildNodes.back()); + auto& renderNodeDrawable = mDisplayList->mChildNodes.back(); + drawDrawable(&renderNodeDrawable); // use staging property, since recording on UI thread if (renderNode->stagingProperties().isProjectionReceiver()) { - mDisplayList->mIsProjectionReceiver = true; + mDisplayList->mProjectionReceiver = &renderNodeDrawable; // set projectionReceiveIndex so that RenderNode.hasProjectionReceiver returns true mDisplayList->projectionReceiveIndex = mDisplayList->mChildNodes.size() - 1; } diff --git a/libs/hwui/tests/unit/FrameBuilderTests.cpp b/libs/hwui/tests/unit/FrameBuilderTests.cpp index 01046e1ecad9..8c956e5bcc84 100644 --- a/libs/hwui/tests/unit/FrameBuilderTests.cpp +++ b/libs/hwui/tests/unit/FrameBuilderTests.cpp @@ -1487,6 +1487,8 @@ RENDERTHREAD_TEST(FrameBuilder, buildLayer) { *layerHandle = nullptr; } +namespace { + static void drawOrderedRect(Canvas* canvas, uint8_t expectedDrawOrder) { SkPaint paint; // order put in blue channel, transparent so overlapped content doesn't get rejected @@ -1502,15 +1504,30 @@ static void drawOrderedNode(Canvas* canvas, uint8_t expectedDrawOrder, float z) node->setPropertyFieldsDirty(RenderNode::TRANSLATION_Z); canvas->drawRenderNode(node.get()); // canvas takes reference/sole ownership } -RENDERTHREAD_TEST(FrameBuilder, zReorder) { - class ZReorderTestRenderer : public TestRendererBase { - public: - void onRectOp(const RectOp& op, const BakedOpState& state) override { - int expectedOrder = SkColorGetB(op.paint->getColor()); // extract order from blue channel - EXPECT_EQ(expectedOrder, mIndex++) << "An op was drawn out of order"; + +static void drawOrderedNode(Canvas* canvas, uint8_t expectedDrawOrder, + std::function<void(RenderProperties& props, RecordingCanvas& canvas)> setup) { + auto node = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [expectedDrawOrder, setup](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedRect(&canvas, expectedDrawOrder); + if (setup) { + setup(props, canvas); } - }; + }); + canvas->drawRenderNode(node.get()); // canvas takes reference/sole ownership +} + +class ZReorderTestRenderer : public TestRendererBase { +public: + void onRectOp(const RectOp& op, const BakedOpState& state) override { + int expectedOrder = SkColorGetB(op.paint->getColor()); // extract order from blue channel + EXPECT_EQ(expectedOrder, mIndex++) << "An op was drawn out of order"; + } +}; + +} // end anonymous namespace +RENDERTHREAD_TEST(FrameBuilder, zReorder) { auto parent = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, [](RenderProperties& props, RecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, 10.0f); // in reorder=false at this point, so played inorder @@ -2238,5 +2255,349 @@ RENDERTHREAD_TEST(FrameBuilder, clip_replace) { EXPECT_EQ(1, renderer.getIndex()); } +TEST(FrameBuilder, projectionReorderProjectedInMiddle) { + /* R is backward projected on B + A + / \ + B C + | + R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, [](RenderProperties& props, RecordingCanvas& canvas) { + props.setProjectionReceiver(true); + } ); //nodeB + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + } ); //nodeC + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(3, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderProjectLast) { + /* R is backward projected on E + A + / | \ + / | \ + B C E + | + R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, nullptr); //nodeB + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 3, [](RenderProperties& props, RecordingCanvas& canvas) { //drawn as 2 + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + } ); //nodeC + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { //drawn as 3 + props.setProjectionReceiver(true); + } ); //nodeE + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(4, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderNoReceivable) { + /* R is backward projected without receiver + A + / \ + B C + | + R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, nullptr); //nodeB + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 255, [](RenderProperties& props, RecordingCanvas& canvas) { + //not having a projection receiver is an undefined behavior + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + } ); //nodeC + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(2, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderParentReceivable) { + /* R is backward projected on C + A + / \ + B C + | + R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, nullptr); //nodeB + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { + props.setProjectionReceiver(true); + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + } ); //nodeC + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(3, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderSameNodeReceivable) { + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, nullptr); //nodeB + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 255, [](RenderProperties& props, RecordingCanvas& 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 + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(2, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderProjectedSibling) { + //TODO: this test together with the next "projectionReorderProjectedSibling2" likely expose a + //bug in HWUI. First test draws R, while the second test does not draw R for a nearly identical + //tree setup. The correct behaviour is to not draw R, because the receiver cannot be a sibling + /* R is backward projected on B. R is not expected to be drawn (see Sibling2 outcome below), + but for some reason it is drawn. + A + /|\ + / | \ + B C R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, [](RenderProperties& props, RecordingCanvas& canvas) { + props.setProjectionReceiver(true); + } ); //nodeB + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { + } ); //nodeC + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(3, renderer.getIndex()); +} + +TEST(FrameBuilder, 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::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, [](RenderProperties& props, RecordingCanvas& canvas) { //G + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { //B + props.setProjectionReceiver(true); + } ); //nodeB + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { //C + } ); //nodeC + drawOrderedNode(&canvas, 255, [](RenderProperties& props, RecordingCanvas& canvas) { //R + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + } ); //nodeG + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(3, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderGrandparentReceivable) { + /* R is backward projected on B + A + | + B + | + C + | + R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, [](RenderProperties& props, RecordingCanvas& canvas) { + props.setProjectionReceiver(true); + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + } ); //nodeC + } ); //nodeB + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(3, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderTwoReceivables) { + /* B and G are receivables, R is backward projected + A + / \ + B C + / \ + G R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, [](RenderProperties& props, RecordingCanvas& canvas) { //B + props.setProjectionReceiver(true); + } ); //nodeB + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { //C + drawOrderedNode(&canvas, 3, [](RenderProperties& props, RecordingCanvas& canvas) { //G + props.setProjectionReceiver(true); + } ); //nodeG + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { //R + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + } ); //nodeC + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(4, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderTwoReceivablesLikelyScenario) { + /* B and G are receivables, G is backward projected + A + / \ + B C + / \ + G R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, [](RenderProperties& props, RecordingCanvas& canvas) { //B + props.setProjectionReceiver(true); + } ); //nodeB + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { //C + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { //G + props.setProjectionReceiver(true); + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeG + drawOrderedNode(&canvas, 3, [](RenderProperties& props, RecordingCanvas& canvas) { //R + } ); //nodeR + } ); //nodeC + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(4, renderer.getIndex()); +} + +TEST(FrameBuilder, projectionReorderTwoReceivablesDeeper) { + /* B and G are receivables, R is backward projected + A + / \ + B C + / \ + G D + | + R + */ + auto nodeA = TestUtils::createNode<RecordingCanvas>(0, 0, 100, 100, + [](RenderProperties& props, RecordingCanvas& canvas) { + drawOrderedNode(&canvas, 0, [](RenderProperties& props, RecordingCanvas& canvas) { //B + props.setProjectionReceiver(true); + } ); //nodeB + drawOrderedNode(&canvas, 1, [](RenderProperties& props, RecordingCanvas& canvas) { //C + drawOrderedNode(&canvas, 2, [](RenderProperties& props, RecordingCanvas& canvas) { //G + props.setProjectionReceiver(true); + } ); //nodeG + drawOrderedNode(&canvas, 4, [](RenderProperties& props, RecordingCanvas& canvas) { //D + drawOrderedNode(&canvas, 3, [](RenderProperties& props, RecordingCanvas& canvas) { //R + props.setProjectBackwards(true); + props.setClipToBounds(false); + } ); //nodeR + } ); //nodeD + } ); //nodeC + }); //nodeA + + FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(nodeA)); + + ZReorderTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(5, renderer.getIndex()); +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp index 623d9712fd56..ae4f0f42e669 100644 --- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp @@ -21,11 +21,14 @@ #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 <string.h> @@ -51,6 +54,8 @@ TEST(RenderNodeDrawable, create) { 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 @@ -67,18 +72,33 @@ static void drawOrderedNode(Canvas* canvas, uint8_t expectedDrawOrder, float z) canvas->drawRenderNode(node.get()); // canvas takes reference/sole ownership } -TEST(RenderNodeDrawable, zReorder) { - 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, mIndex++) << "An op was drawn out of order"; +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); } - int getIndex() { return mIndex; } - protected: - int mIndex = 0; - }; + }); + 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, mIndex++) << "An op was drawn out of order"; + } + int getIndex() { return mIndex; } +protected: + int mIndex = 0; +}; + +} // end anonymous namespace + +TEST(RenderNodeDrawable, zReorder) { auto parent = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { @@ -136,42 +156,622 @@ TEST(RenderNodeDrawable, composeOnLayer) rootNode->setLayerSurface(sk_sp<SkSurface>()); } -//TODO: refactor to cover test cases from FrameBuilderTests_projectionReorder -//validate with bounds and projection path mask. -//TODO: research if we could hook in and mock/validate different aspects of the drawing, -//instead of validating pixels -TEST(RenderNodeDrawable, projectDraw) { - 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); +namespace { +class ContextFactory : public IContextFactory { +public: + virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override { + return new AnimationContext(clock); + } +}; - auto redNode = TestUtils::createSkiaNode(0, 0, 1, 1, - [](RenderProperties& props, SkiaRecordingCanvas& redCanvas) { - redCanvas.drawColor(SK_ColorRED, SkBlendMode::kSrcOver); - }, "redNode"); +inline SkRect getBounds(const SkCanvas* canvas) { + SkClipStack::BoundsType boundType; + SkRect clipBounds; + canvas->getClipStack()->getBounds(&clipBounds, &boundType); + return clipBounds; +} +inline SkRect getLocalBounds(const SkCanvas* canvas) { + SkMatrix invertedTotalMatrix; + EXPECT_TRUE(canvas->getTotalMatrix().invert(&invertedTotalMatrix)); + SkRect outlineInDeviceCoord = getBounds(canvas); + SkRect outlineInLocalCoord; + invertedTotalMatrix.mapRect(&outlineInLocalCoord, outlineInDeviceCoord); + return outlineInLocalCoord; +} +} // end anonymous namespace - auto greenNodeWithRedChild = TestUtils::createSkiaNode(0, 0, 1, 1, - [&](RenderProperties& props, SkiaRecordingCanvas& greenCanvasWithRedChild) { - greenCanvasWithRedChild.drawRenderNode(redNode.get()); - greenCanvasWithRedChild.drawColor(SK_ColorGREEN, SkBlendMode::kSrcOver); - }, "greenNodeWithRedChild"); +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 = mIndex++; + 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), getBounds(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), getLocalBounds(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), getBounds(this)); + break; + default: + ADD_FAILURE(); + } + EXPECT_EQ(expectedMatrix, getTotalMatrix()); + } - auto rootNode = TestUtils::createSkiaNode(0, 0, 1, 1, - [&](RenderProperties& props, SkiaRecordingCanvas& rootCanvas) { - rootCanvas.drawRenderNode(greenNodeWithRedChild.get()); - }, "rootNode"); - SkiaDisplayList* rootDisplayList = static_cast<SkiaDisplayList*>( - (const_cast<DisplayList*>(rootNode->getDisplayList()))); - - RenderNodeDrawable rootDrawable(rootNode.get(), &canvas, false); - canvas.drawDrawable(&rootDrawable); - ASSERT_EQ(TestUtils::getColor(surface, 0, 0), SK_ColorGREEN); - - //project redNode on rootNode, which will change the test outcome, - //because redNode will draw after greenNodeWithRedChild - rootDisplayList->mIsProjectionReceiver = true; - redNode->animatorProperties().setProjectBackwards(true); - canvas.drawDrawable(&rootDrawable); - ASSERT_EQ(TestUtils::getColor(surface, 0, 0), SK_ColorRED); + int getIndex() { return mIndex; } + protected: + int mIndex = 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; + info.observer = nullptr; + 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() : SkCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) {} + void onDrawArc(const SkRect&, SkScalar startAngle, SkScalar sweepAngle, bool useCenter, + const SkPaint&) override { + EXPECT_EQ(0, mIndex++); //part of painting the layer + EXPECT_EQ(SkRect::MakeLTRB(0, 0, LAYER_WIDTH, LAYER_HEIGHT), getBounds(this)); + } + void onDrawRect(const SkRect& rect, const SkPaint& paint) override { + EXPECT_EQ(1, mIndex++); + EXPECT_EQ(SkRect::MakeLTRB(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT), getBounds(this)); + } + void onDrawOval(const SkRect&, const SkPaint&) override { + EXPECT_EQ(2, mIndex++); + SkMatrix expectedMatrix; + expectedMatrix.setTranslate(100 - SCROLL_X, 100 - SCROLL_Y); + EXPECT_EQ(expectedMatrix, getTotalMatrix()); + EXPECT_EQ(SkRect::MakeLTRB(-85, -80, 295, 300), getLocalBounds(this)); + } + int mIndex = 0; + }; + + class ProjectionLayer : public SkSurface_Base { + public: + ProjectionLayer(ProjectionTestCanvas *canvas) + : SkSurface_Base(SkImageInfo::MakeN32Premul(LAYER_WIDTH, LAYER_HEIGHT), nullptr) + , mCanvas(canvas) { + } + void onDraw(SkCanvas*, SkScalar x, SkScalar y, const SkPaint*) override { + EXPECT_EQ(3, mCanvas->mIndex++); + EXPECT_EQ(SkRect::MakeLTRB(100 - SCROLL_X, 100 - SCROLL_Y, 300 - SCROLL_X, + 300 - SCROLL_Y), getBounds(mCanvas)); + } + SkCanvas* onNewCanvas() override { + mCanvas->ref(); + return mCanvas; + } + sk_sp<SkSurface> onNewSurface(const SkImageInfo&) override { + return sk_sp<SkSurface>(); + } + sk_sp<SkImage> onNewImageSnapshot(SkBudgeted, SkCopyPixelsMode) override { + return sk_sp<SkImage>(); + } + void onCopyOnWrite(ContentChangeMode) override {} + ProjectionTestCanvas* mCanvas; + }; + + 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; + info.observer = nullptr; + parent->prepareTree(info); + + sk_sp<ProjectionTestCanvas> canvas(new ProjectionTestCanvas()); + //set a layer after prepareTree to avoid layer logic there + child->animatorProperties().mutateLayerProperties().setType(LayerType::RenderLayer); + sk_sp<SkSurface> surfaceLayer1(new ProjectionLayer(canvas.get())); + 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, canvas->mIndex); //assert index 0 is drawn on the layer + + RenderNodeDrawable drawable(parent.get(), canvas.get(), true); + canvas->drawDrawable(&drawable); + EXPECT_EQ(4, canvas->mIndex); + + // 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, mIndex++); + EXPECT_TRUE(getTotalMatrix().isIdentity()); + } + void onDrawOval(const SkRect&, const SkPaint&) override { + EXPECT_EQ(1, mIndex++); + EXPECT_EQ(SkRect::MakeWH(CANVAS_WIDTH, CANVAS_HEIGHT), getBounds(this)); + EXPECT_TRUE(getTotalMatrix().isIdentity()); + } + int mIndex = 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, SkRegion::kIntersect_Op); + + 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; + info.observer = nullptr; + parent->prepareTree(info); + + sk_sp<ProjectionChildScrollTestCanvas> canvas(new ProjectionChildScrollTestCanvas()); + RenderNodeDrawable drawable(parent.get(), canvas.get(), true); + canvas->drawDrawable(&drawable); + EXPECT_EQ(2, canvas->mIndex); +} + +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; + info.observer = nullptr; + 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)); } diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp index 67fb78a69c9a..899758a9e6fb 100644 --- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp +++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp @@ -33,7 +33,7 @@ TEST(SkiaDisplayList, create) { SkRect bounds = SkRect::MakeWH(200, 200); SkiaDisplayList skiaDL(bounds); ASSERT_TRUE(skiaDL.isEmpty()); - ASSERT_FALSE(skiaDL.mIsProjectionReceiver); + ASSERT_FALSE(skiaDL.mProjectionReceiver); ASSERT_EQ(skiaDL.mDrawable->getBounds(), bounds); } @@ -42,12 +42,13 @@ TEST(SkiaDisplayList, reset) { SkiaDisplayList skiaDL(bounds); SkCanvas dummyCanvas; + RenderNodeDrawable drawable(nullptr, &dummyCanvas); skiaDL.mChildNodes.emplace_back(nullptr, &dummyCanvas); skiaDL.mChildFunctors.emplace_back(nullptr, nullptr, &dummyCanvas); skiaDL.mMutableImages.push_back(nullptr); skiaDL.mVectorDrawables.push_back(nullptr); skiaDL.mDrawable->drawAnnotation(bounds, "testAnnotation", nullptr); - skiaDL.mIsProjectionReceiver = true; + skiaDL.mProjectionReceiver = &drawable; ASSERT_EQ(skiaDL.mDrawable->getBounds(), bounds); ASSERT_FALSE(skiaDL.mChildNodes.empty()); @@ -55,7 +56,7 @@ TEST(SkiaDisplayList, reset) { ASSERT_FALSE(skiaDL.mMutableImages.empty()); ASSERT_FALSE(skiaDL.mVectorDrawables.empty()); ASSERT_FALSE(skiaDL.isEmpty()); - ASSERT_TRUE(skiaDL.mIsProjectionReceiver); + ASSERT_TRUE(skiaDL.mProjectionReceiver); bounds = SkRect::MakeWH(100, 100); skiaDL.reset(bounds); @@ -66,7 +67,7 @@ TEST(SkiaDisplayList, reset) { ASSERT_TRUE(skiaDL.mMutableImages.empty()); ASSERT_TRUE(skiaDL.mVectorDrawables.empty()); ASSERT_TRUE(skiaDL.isEmpty()); - ASSERT_FALSE(skiaDL.mIsProjectionReceiver); + ASSERT_FALSE(skiaDL.mProjectionReceiver); } TEST(SkiaDisplayList, reuseDisplayList) { |