Fix Skia render node projection to match HWUI
Fix Skia render node projection to match HWUI. Port
FrameBuilderTests_projectionReorder test for Skia pipeline.
Add new tests in both HWUI and Skia to cover more projection
use cases.
Test: built and run on angler-eng
Change-Id: Ibf27af211452ae95d595aca7723ea63f48b0b282
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index fdf4d52..eff2499 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -181,6 +181,7 @@
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 2087fca..749efdd 100644
--- a/libs/hwui/TreeInfo.h
+++ b/libs/hwui/TreeInfo.h
@@ -116,6 +116,9 @@
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 7dcbbd0..da9002d 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
@@ -24,8 +24,41 @@
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 @@
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;
+ //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);
}
- // 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;
- }
- this->drawContent(canvas);
}
- mProjectedChildrenTarget = nullptr;
- mNextProjectedChildrenTarget = nullptr;
+ displayList->mProjectedOutline = nullptr;
}
static bool layerNeedsPaint(const LayerProperties& properties,
@@ -148,6 +153,10 @@
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 a2ffc6c..3eed647 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.h
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.h
@@ -29,6 +29,8 @@
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 @@
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 @@
*/
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 @@
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 @@
*/
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 @@
* 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 2ad7f74..9db8cd3 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
@@ -64,16 +64,31 @@
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 @@
}
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 f34b485..ff86fd1 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 @@
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 @@
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 621816a..6df544f 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -108,11 +108,12 @@
// 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 01046e1..8c956e5 100644
--- a/libs/hwui/tests/unit/FrameBuilderTests.cpp
+++ b/libs/hwui/tests/unit/FrameBuilderTests.cpp
@@ -1487,6 +1487,8 @@
*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 @@
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 @@
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 623d971..ae4f0f4 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 @@
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 @@
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 @@
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())));
+ int getIndex() { return mIndex; }
+ protected:
+ int mIndex = 0;
+ };
- RenderNodeDrawable rootDrawable(rootNode.get(), &canvas, false);
- canvas.drawDrawable(&rootDrawable);
- ASSERT_EQ(TestUtils::getColor(surface, 0, 0), SK_ColorGREEN);
+ /**
+ * 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);
- //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);
+ 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 67fb78a..899758a 100644
--- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
+++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
@@ -33,7 +33,7 @@
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 @@
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 @@
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 @@
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) {