Support projection in OpReorderer

bug:22480459

Change-Id: Iceb71732dc50957cfb47fa1ba9b8e18e6fc51132
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index cc68fb2..9e61575 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -275,7 +275,9 @@
 LOCAL_MODULE_STEM_32 := hwuimicro
 LOCAL_MODULE_STEM_64 := hwuimicro64
 LOCAL_SHARED_LIBRARIES := $(hwui_shared_libraries)
-LOCAL_CFLAGS := $(hwui_cflags)
+LOCAL_CFLAGS := \
+        $(hwui_cflags) \
+        -DHWUI_NULL_GPU
 LOCAL_C_INCLUDES += bionic/benchmarks/
 
 LOCAL_WHOLE_STATIC_LIBRARIES := libhwui_static_null_gpu
diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h
index 00c4e2d..60cc7ba 100644
--- a/libs/hwui/DisplayList.h
+++ b/libs/hwui/DisplayList.h
@@ -132,7 +132,7 @@
     DisplayList();
     ~DisplayList();
 
-    // index of DisplayListOp restore, after which projected descendents should be drawn
+    // index of DisplayListOp restore, after which projected descendants should be drawn
     int projectionReceiveIndex;
 
     const LsaVector<Chunk>& getChunks() const { return chunks; }
diff --git a/libs/hwui/DisplayListCanvas.h b/libs/hwui/DisplayListCanvas.h
index bf98f79..72fc100 100644
--- a/libs/hwui/DisplayListCanvas.h
+++ b/libs/hwui/DisplayListCanvas.h
@@ -55,6 +55,7 @@
 class DeferredLayerUpdater;
 class DisplayListOp;
 class DrawOp;
+class DrawRenderNodeOp;
 class RenderNode;
 class StateOp;
 
diff --git a/libs/hwui/DisplayListOp.h b/libs/hwui/DisplayListOp.h
index 977b53c..bd11d0a 100644
--- a/libs/hwui/DisplayListOp.h
+++ b/libs/hwui/DisplayListOp.h
@@ -1386,19 +1386,19 @@
             : DrawBoundedOp(0, 0, renderNode->getWidth(), renderNode->getHeight(), nullptr)
             , renderNode(renderNode)
             , mRecordedWithPotentialStencilClip(!clipIsSimple || !transformFromParent.isSimple())
-            , mTransformFromParent(transformFromParent)
-            , mSkipInOrderDraw(false) {}
+            , localMatrix(transformFromParent)
+            , skipInOrderDraw(false) {}
 
     virtual void defer(DeferStateStruct& deferStruct, int saveCount, int level,
             bool useQuickReject) override {
-        if (renderNode->isRenderable() && !mSkipInOrderDraw) {
+        if (renderNode->isRenderable() && !skipInOrderDraw) {
             renderNode->defer(deferStruct, level + 1);
         }
     }
 
     virtual void replay(ReplayStateStruct& replayStruct, int saveCount, int level,
             bool useQuickReject) override {
-        if (renderNode->isRenderable() && !mSkipInOrderDraw) {
+        if (renderNode->isRenderable() && !skipInOrderDraw) {
             renderNode->replay(replayStruct, level + 1);
         }
     }
@@ -1439,7 +1439,7 @@
     /**
      * Records transform vs parent, used for computing total transform without rerunning DL contents
      */
-    const mat4 mTransformFromParent;
+    const mat4 localMatrix;
 
     /**
      * Holds the transformation between the projection surface ViewGroup and this RenderNode
@@ -1449,8 +1449,8 @@
      *
      * Note: doesn't include transformation within the RenderNode, or its properties.
      */
-    mat4 mTransformFromCompositingAncestor;
-    bool mSkipInOrderDraw;
+    mat4 transformFromCompositingAncestor;
+    bool skipInOrderDraw;
 };
 
 /**
diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp
index 5e954ae..9cbd9c2d 100644
--- a/libs/hwui/OpReorderer.cpp
+++ b/libs/hwui/OpReorderer.cpp
@@ -330,6 +330,7 @@
     for (int i = layers.entries().size() - 1; i >= 0; i--) {
         RenderNode* layerNode = layers.entries()[i].renderNode;
         const Rect& layerDamage = layers.entries()[i].damage;
+        layerNode->computeOrdering();
 
         // map current light center into RenderNode's coordinate space
         Vector3 lightCenter = mCanvasState.currentSnapshot()->getRelativeLightCenter();
@@ -339,7 +340,7 @@
                 layerDamage, lightCenter, nullptr, layerNode);
 
         if (layerNode->getDisplayList()) {
-            deferDisplayList(*(layerNode->getDisplayList()));
+            deferNodeOps(*layerNode);
         }
         restoreForLayer();
     }
@@ -347,6 +348,7 @@
     // Defer Fbo0
     for (const sp<RenderNode>& node : nodes) {
         if (node->nothingToDraw()) continue;
+        node->computeOrdering();
 
         int count = mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag);
         deferNodePropsAndOps(*node);
@@ -354,20 +356,6 @@
     }
 }
 
-OpReorderer::OpReorderer(int viewportWidth, int viewportHeight, const DisplayList& displayList,
-        const Vector3& lightCenter)
-        : mCanvasState(*this) {
-    ATRACE_NAME("prepare drawing commands");
-    // Prepare to defer Fbo0
-    mLayerReorderers.emplace_back(viewportWidth, viewportHeight,
-            Rect(viewportWidth, viewportHeight));
-    mLayerStack.push_back(0);
-    mCanvasState.initializeSaveStack(viewportWidth, viewportHeight,
-            0, 0, viewportWidth, viewportHeight, lightCenter);
-
-    deferDisplayList(displayList);
-}
-
 void OpReorderer::onViewportInitialized() {}
 
 void OpReorderer::onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {}
@@ -462,10 +450,10 @@
                     Matrix4::identity(),
                     saveLayerBounds,
                     &saveLayerPaint));
-            deferDisplayList(*(node.getDisplayList()));
+            deferNodeOps(node);
             onEndLayerOp(*new (mAllocator) EndLayerOp());
         } else {
-            deferDisplayList(*(node.getDisplayList()));
+            deferNodeOps(node);
         }
     }
 }
@@ -610,18 +598,53 @@
     }
 }
 
+void OpReorderer::deferProjectedChildren(const RenderNode& renderNode) {
+    const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath();
+    int count = mCanvasState.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+
+    // can't be null, since DL=null node rejection happens before deferNodePropsAndOps
+    const DisplayList& displayList = *(renderNode.getDisplayList());
+
+    const RecordedOp* op = (displayList.getOps()[displayList.projectionReceiveIndex]);
+    const RenderNodeOp* backgroundOp = static_cast<const RenderNodeOp*>(op);
+    const RenderProperties& backgroundProps = backgroundOp->renderNode->properties();
+
+    // Transform renderer to match background we're projecting onto
+    // (by offsetting canvas by translationX/Y of background rendernode, since only those are set)
+    mCanvasState.translate(backgroundProps.getTranslationX(), backgroundProps.getTranslationY());
+
+    // If the projection receiver has an outline, we mask projected content to it
+    // (which we know, apriori, are all tessellated paths)
+    mCanvasState.setProjectionPathMask(mAllocator, projectionReceiverOutline);
+
+    // draw projected nodes
+    for (size_t i = 0; i < renderNode.mProjectedNodes.size(); i++) {
+        RenderNodeOp* childOp = renderNode.mProjectedNodes[i];
+
+        int restoreTo = mCanvasState.save(SkCanvas::kMatrix_SaveFlag);
+        mCanvasState.concatMatrix(childOp->transformFromCompositingAncestor);
+        deferRenderNodeOp(*childOp);
+        mCanvasState.restoreToCount(restoreTo);
+    }
+
+    mCanvasState.restoreToCount(count);
+}
+
 /**
  * Used to define a list of lambdas referencing private OpReorderer::onXXXXOp() methods.
  *
- * This allows opIds embedded in the RecordedOps to be used for dispatching to these lambdas. E.g. a
- * BitmapOp op then would be dispatched to OpReorderer::onBitmapOp(const BitmapOp&)
+ * This allows opIds embedded in the RecordedOps to be used for dispatching to these lambdas.
+ * E.g. a BitmapOp op then would be dispatched to OpReorderer::onBitmapOp(const BitmapOp&)
  */
 #define OP_RECEIVER(Type) \
         [](OpReorderer& reorderer, const RecordedOp& op) { reorderer.on##Type(static_cast<const Type&>(op)); },
-void OpReorderer::deferDisplayList(const DisplayList& displayList) {
+void OpReorderer::deferNodeOps(const RenderNode& renderNode) {
     static std::function<void(OpReorderer& reorderer, const RecordedOp&)> receivers[] = {
         MAP_OPS(OP_RECEIVER)
     };
+
+    // can't be null, since DL=null node rejection happens before deferNodePropsAndOps
+    const DisplayList& displayList = *(renderNode.getDisplayList());
     for (const DisplayList::Chunk& chunk : displayList.getChunks()) {
         FatVector<ZRenderNodeOpPair, 16> zTranslatedNodes;
         buildZSortedChildList(&zTranslatedNodes, displayList, chunk);
@@ -630,6 +653,12 @@
         for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) {
             const RecordedOp* op = displayList.getOps()[opIndex];
             receivers[op->opId](*this, *op);
+
+            if (CC_UNLIKELY(!renderNode.mProjectedNodes.empty()
+                    && displayList.projectionReceiveIndex >= 0
+                    && static_cast<int>(opIndex) == displayList.projectionReceiveIndex)) {
+                deferProjectedChildren(renderNode);
+            }
         }
         defer3dChildren(ChildrenSelectMode::Positive, zTranslatedNodes);
     }
diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h
index 976f413..00df8b0 100644
--- a/libs/hwui/OpReorderer.h
+++ b/libs/hwui/OpReorderer.h
@@ -124,9 +124,6 @@
             uint32_t viewportWidth, uint32_t viewportHeight,
             const std::vector< sp<RenderNode> >& nodes, const Vector3& lightCenter);
 
-    OpReorderer(int viewportWidth, int viewportHeight, const DisplayList& displayList,
-            const Vector3& lightCenter);
-
     virtual ~OpReorderer() {}
 
     /**
@@ -202,16 +199,18 @@
         return BakedOpState::tryConstruct(mAllocator, *mCanvasState.currentSnapshot(), recordedOp);
     }
 
-    // should always be surrounded by a save/restore pair
+    // should always be surrounded by a save/restore pair, and not called if DisplayList is null
     void deferNodePropsAndOps(RenderNode& node);
 
-    void deferShadow(const RenderNodeOp& casterOp);
-
-    void deferDisplayList(const DisplayList& displayList);
-
     template <typename V>
     void defer3dChildren(ChildrenSelectMode mode, const V& zTranslatedNodes);
 
+    void deferShadow(const RenderNodeOp& casterOp);
+
+    void deferProjectedChildren(const RenderNode& renderNode);
+
+    void deferNodeOps(const RenderNode& renderNode);
+
     void deferRenderNodeOp(const RenderNodeOp& op);
 
     void replayBakedOpsImpl(void* arg, BakedOpDispatcher* receivers);
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
index 127dca5..b4a201e 100644
--- a/libs/hwui/RecordedOp.h
+++ b/libs/hwui/RecordedOp.h
@@ -97,7 +97,17 @@
     RenderNodeOp(BASE_PARAMS_PAINTLESS, RenderNode* renderNode)
             : SUPER_PAINTLESS(RenderNodeOp)
             , renderNode(renderNode) {}
-    RenderNode * renderNode; // not const, since drawing modifies it (somehow...)
+    RenderNode * renderNode; // not const, since drawing modifies it
+
+    /**
+     * Holds the transformation between the projection surface ViewGroup and this RenderNode
+     * drawing instance. Represents any translations / transformations done within the drawing of
+     * the compositing ancestor ViewGroup's draw, before the draw of the View represented by this
+     * DisplayList draw instance.
+     *
+     * Note: doesn't include transformation within the RenderNode, or its properties.
+     */
+    Matrix4 transformFromCompositingAncestor;
     bool skipInOrderDraw = false;
 };
 
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 3f24f44..ae690fd 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -487,7 +487,7 @@
             info.damageAccumulator->pushTransform(&op->localMatrix);
             bool childFunctorsNeedLayer = functorsNeedLayer; // TODO! || op->mRecordedWithPotentialStencilClip;
 #else
-            info.damageAccumulator->pushTransform(&op->mTransformFromParent);
+            info.damageAccumulator->pushTransform(&op->localMatrix);
             bool childFunctorsNeedLayer = functorsNeedLayer
                     // Recorded with non-rect clip, or canvas-rotated by parent
                     || op->mRecordedWithPotentialStencilClip;
@@ -658,7 +658,6 @@
  * which are flagged to not draw in the standard draw loop.
  */
 void RenderNode::computeOrdering() {
-#if !HWUI_NEW_OPS
     ATRACE_CALL();
     mProjectedNodes.clear();
 
@@ -666,43 +665,41 @@
     // transform properties are applied correctly to top level children
     if (mDisplayList == nullptr) return;
     for (unsigned int i = 0; i < mDisplayList->getChildren().size(); i++) {
-        DrawRenderNodeOp* childOp = mDisplayList->getChildren()[i];
+        renderNodeOp_t* childOp = mDisplayList->getChildren()[i];
         childOp->renderNode->computeOrderingImpl(childOp, &mProjectedNodes, &mat4::identity());
     }
-#endif
 }
 
 void RenderNode::computeOrderingImpl(
-        DrawRenderNodeOp* opState,
-        std::vector<DrawRenderNodeOp*>* compositedChildrenOfProjectionSurface,
+        renderNodeOp_t* opState,
+        std::vector<renderNodeOp_t*>* compositedChildrenOfProjectionSurface,
         const mat4* transformFromProjectionSurface) {
-#if !HWUI_NEW_OPS
     mProjectedNodes.clear();
     if (mDisplayList == nullptr || mDisplayList->isEmpty()) return;
 
     // TODO: should avoid this calculation in most cases
     // TODO: just calculate single matrix, down to all leaf composited elements
     Matrix4 localTransformFromProjectionSurface(*transformFromProjectionSurface);
-    localTransformFromProjectionSurface.multiply(opState->mTransformFromParent);
+    localTransformFromProjectionSurface.multiply(opState->localMatrix);
 
     if (properties().getProjectBackwards()) {
         // composited projectee, flag for out of order draw, save matrix, and store in proj surface
-        opState->mSkipInOrderDraw = true;
-        opState->mTransformFromCompositingAncestor = localTransformFromProjectionSurface;
+        opState->skipInOrderDraw = true;
+        opState->transformFromCompositingAncestor = localTransformFromProjectionSurface;
         compositedChildrenOfProjectionSurface->push_back(opState);
     } else {
         // standard in order draw
-        opState->mSkipInOrderDraw = false;
+        opState->skipInOrderDraw = false;
     }
 
     if (mDisplayList->getChildren().size() > 0) {
         const bool isProjectionReceiver = mDisplayList->projectionReceiveIndex >= 0;
         bool haveAppliedPropertiesToProjection = false;
         for (unsigned int i = 0; i < mDisplayList->getChildren().size(); i++) {
-            DrawRenderNodeOp* childOp = mDisplayList->getChildren()[i];
+            renderNodeOp_t* childOp = mDisplayList->getChildren()[i];
             RenderNode* child = childOp->renderNode;
 
-            std::vector<DrawRenderNodeOp*>* projectionChildren = nullptr;
+            std::vector<renderNodeOp_t*>* projectionChildren = nullptr;
             const mat4* projectionTransform = nullptr;
             if (isProjectionReceiver && !child->properties().getProjectBackwards()) {
                 // if receiving projections, collect projecting descendant
@@ -723,7 +720,6 @@
             child->computeOrderingImpl(childOp, projectionChildren, projectionTransform);
         }
     }
-#endif
 }
 
 class DeferOperationHandler {
@@ -793,10 +789,10 @@
 
         if (!MathUtils::isZero(childZ) && chunk.reorderChildren) {
             zTranslatedNodes.push_back(ZDrawRenderNodeOpPair(childZ, childOp));
-            childOp->mSkipInOrderDraw = true;
+            childOp->skipInOrderDraw = true;
         } else if (!child->properties().getProjectBackwards()) {
             // regular, in order drawing DisplayList
-            childOp->mSkipInOrderDraw = false;
+            childOp->skipInOrderDraw = false;
         }
     }
 
@@ -913,7 +909,7 @@
             // attempt to render the shadow if the caster about to be drawn is its caster,
             // OR if its caster's Z value is similar to the previous potential caster
             if (shadowIndex == drawIndex || casterZ - lastCasterZ < SHADOW_DELTA) {
-                caster->issueDrawShadowOperation(casterOp->mTransformFromParent, handler);
+                caster->issueDrawShadowOperation(casterOp->localMatrix, handler);
 
                 lastCasterZ = casterZ; // must do this even if current caster not casting a shadow
                 shadowIndex++;
@@ -927,10 +923,10 @@
 
         DrawRenderNodeOp* childOp = zTranslatedNodes[drawIndex].value;
 
-        renderer.concatMatrix(childOp->mTransformFromParent);
-        childOp->mSkipInOrderDraw = false; // this is horrible, I'm so sorry everyone
+        renderer.concatMatrix(childOp->localMatrix);
+        childOp->skipInOrderDraw = false; // this is horrible, I'm so sorry everyone
         handler(childOp, renderer.getSaveCount() - 1, properties().getClipToBounds());
-        childOp->mSkipInOrderDraw = true;
+        childOp->skipInOrderDraw = true;
 
         renderer.restoreToCount(restoreTo);
         drawIndex++;
@@ -967,14 +963,14 @@
 
     // draw projected nodes
     for (size_t i = 0; i < mProjectedNodes.size(); i++) {
-        DrawRenderNodeOp* childOp = mProjectedNodes[i];
+        renderNodeOp_t* childOp = mProjectedNodes[i];
 
         // matrix save, concat, and restore can be done safely without allocating operations
         int restoreTo = renderer.save(SkCanvas::kMatrix_SaveFlag);
-        renderer.concatMatrix(childOp->mTransformFromCompositingAncestor);
-        childOp->mSkipInOrderDraw = false; // this is horrible, I'm so sorry everyone
+        renderer.concatMatrix(childOp->transformFromCompositingAncestor);
+        childOp->skipInOrderDraw = false; // this is horrible, I'm so sorry everyone
         handler(childOp, renderer.getSaveCount() - 1, properties().getClipToBounds());
-        childOp->mSkipInOrderDraw = true;
+        childOp->skipInOrderDraw = true;
         renderer.restoreToCount(restoreTo);
     }
 
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index 83d1b58..b6f50b1 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -51,20 +51,22 @@
 class Rect;
 class SkiaShader;
 
-
 #if HWUI_NEW_OPS
 class OffscreenBuffer;
+struct RenderNodeOp;
 typedef OffscreenBuffer layer_t;
+typedef RenderNodeOp renderNodeOp_t;
 #else
 class Layer;
 typedef Layer layer_t;
+typedef DrawRenderNodeOp renderNodeOp_t;
 #endif
 
 class ClipRectOp;
+class DrawRenderNodeOp;
 class SaveLayerOp;
 class SaveOp;
 class RestoreToCountOp;
-class DrawRenderNodeOp;
 class TreeInfo;
 
 namespace proto {
@@ -85,6 +87,7 @@
  */
 class RenderNode : public VirtualLightRefBase {
 friend class TestUtils; // allow TestUtils to access syncDisplayList / syncProperties
+friend class OpReorderer;
 public:
     enum DirtyPropertyMask {
         GENERIC         = 1 << 1,
@@ -221,8 +224,8 @@
         PositiveZChildren
     };
 
-    void computeOrderingImpl(DrawRenderNodeOp* opState,
-            std::vector<DrawRenderNodeOp*>* compositedChildrenOfProjectionSurface,
+    void computeOrderingImpl(renderNodeOp_t* opState,
+            std::vector<renderNodeOp_t*>* compositedChildrenOfProjectionSurface,
             const mat4* transformFromProjectionSurface);
 
     template <class T>
@@ -305,7 +308,7 @@
      */
 
     // for projection surfaces, contains a list of all children items
-    std::vector<DrawRenderNodeOp*> mProjectedNodes;
+    std::vector<renderNodeOp_t*> mProjectedNodes;
 
     // How many references our parent(s) have to us. Typically this should alternate
     // between 2 and 1 (when a staging push happens we inc first then dec)
diff --git a/libs/hwui/microbench/OpReordererBench.cpp b/libs/hwui/microbench/OpReordererBench.cpp
index eea0c7f..fc56988 100644
--- a/libs/hwui/microbench/OpReordererBench.cpp
+++ b/libs/hwui/microbench/OpReordererBench.cpp
@@ -18,10 +18,12 @@
 
 #include "BakedOpState.h"
 #include "BakedOpRenderer.h"
+#include "LayerUpdateQueue.h"
 #include "OpReorderer.h"
 #include "RecordedOp.h"
 #include "RecordingCanvas.h"
 #include "utils/TestUtils.h"
+#include "Vector.h"
 #include "microbench/MicroBench.h"
 
 #include <vector>
@@ -29,26 +31,38 @@
 using namespace android;
 using namespace android::uirenderer;
 
-auto sReorderingDisplayList = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
-    SkBitmap bitmap = TestUtils::createSkBitmap(10, 10);
-    SkPaint paint;
+const LayerUpdateQueue sEmptyLayerUpdateQueue;
+const Vector3 sLightCenter = {100, 100, 100};
 
-    // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects.
-    // Rects don't overlap bitmaps, so bitmaps should be brought to front as a group.
-    canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
-    for (int i = 0; i < 30; i++) {
-        canvas.translate(0, 10);
-        canvas.drawRect(0, 0, 10, 10, paint);
-        canvas.drawBitmap(bitmap, 5, 0, nullptr);
-    }
-    canvas.restore();
-});
+static std::vector<sp<RenderNode>> createTestNodeList() {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
+        SkBitmap bitmap = TestUtils::createSkBitmap(10, 10);
+        SkPaint paint;
+
+        // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects.
+        // Rects don't overlap bitmaps, so bitmaps should be brought to front as a group.
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        for (int i = 0; i < 30; i++) {
+            canvas.translate(0, 10);
+            canvas.drawRect(0, 0, 10, 10, paint);
+            canvas.drawBitmap(bitmap, 5, 0, nullptr);
+        }
+        canvas.restore();
+    });
+    TestUtils::syncHierarchyPropertiesAndDisplayList(node);
+    std::vector<sp<RenderNode>> vec;
+    vec.emplace_back(node);
+    return vec;
+}
 
 BENCHMARK_NO_ARG(BM_OpReorderer_defer);
 void BM_OpReorderer_defer::Run(int iters) {
+    auto nodes = createTestNodeList();
     StartBenchmarkTiming();
     for (int i = 0; i < iters; i++) {
-        OpReorderer reorderer(200, 200, *sReorderingDisplayList, (Vector3) { 100, 100, 100 });
+        OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200,
+                nodes, sLightCenter);
         MicroBench::DoNotOptimize(&reorderer);
     }
     StopBenchmarkTiming();
@@ -57,13 +71,16 @@
 BENCHMARK_NO_ARG(BM_OpReorderer_deferAndRender);
 void BM_OpReorderer_deferAndRender::Run(int iters) {
     TestUtils::runOnRenderThread([this, iters](renderthread::RenderThread& thread) {
+        auto nodes = createTestNodeList();
+        BakedOpRenderer::LightInfo lightInfo = {50.0f, 128, 128 };
+
         RenderState& renderState = thread.renderState();
         Caches& caches = Caches::getInstance();
-        BakedOpRenderer::LightInfo lightInfo = { 50.0f, 128, 128 };
 
         StartBenchmarkTiming();
         for (int i = 0; i < iters; i++) {
-            OpReorderer reorderer(200, 200, *sReorderingDisplayList, (Vector3) { 100, 100, 100 });
+            OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200,
+                    nodes, sLightCenter);
 
             BakedOpRenderer renderer(caches, renderState, true, lightInfo);
             reorderer.replayBakedOps<BakedOpDispatcher>(renderer);
diff --git a/libs/hwui/tests/scenes/OvalAnimation.cpp b/libs/hwui/tests/scenes/OvalAnimation.cpp
index 919a53d..936aba1 100644
--- a/libs/hwui/tests/scenes/OvalAnimation.cpp
+++ b/libs/hwui/tests/scenes/OvalAnimation.cpp
@@ -29,17 +29,14 @@
     sp<RenderNode> card;
     void createContent(int width, int height, TestCanvas& canvas) override {
         canvas.drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode);
-        canvas.insertReorderBarrier(true);
-
-        card = TestUtils::createNode(0, 0, 200, 200, [](TestCanvas& canvas) {
+        card = TestUtils::createNode(0, 0, 200, 200,
+                [](RenderProperties& props, TestCanvas& canvas) {
             SkPaint paint;
             paint.setAntiAlias(true);
             paint.setColor(0xFF000000);
             canvas.drawOval(0, 0, 200, 200, paint);
         });
-
         canvas.drawRenderNode(card.get());
-        canvas.insertReorderBarrier(false);
     }
 
     void doFrame(int frameNr) override {
diff --git a/libs/hwui/tests/scenes/PartialDamageAnimation.cpp b/libs/hwui/tests/scenes/PartialDamageAnimation.cpp
index 0fba4eb..c31ddd1 100644
--- a/libs/hwui/tests/scenes/PartialDamageAnimation.cpp
+++ b/libs/hwui/tests/scenes/PartialDamageAnimation.cpp
@@ -44,7 +44,7 @@
                 SkColor color = COLORS[static_cast<int>((y / dp(116))) % 4];
                 sp<RenderNode> card = TestUtils::createNode(x, y,
                         x + dp(100), y + dp(100),
-                        [color](TestCanvas& canvas) {
+                        [color](RenderProperties& props, TestCanvas& canvas) {
                     canvas.drawColor(color, SkXfermode::kSrcOver_Mode);
                 });
                 canvas.drawRenderNode(card.get());
diff --git a/libs/hwui/tests/scenes/RectGridAnimation.cpp b/libs/hwui/tests/scenes/RectGridAnimation.cpp
index 254f828..a1f04d6 100644
--- a/libs/hwui/tests/scenes/RectGridAnimation.cpp
+++ b/libs/hwui/tests/scenes/RectGridAnimation.cpp
@@ -34,7 +34,7 @@
         canvas.insertReorderBarrier(true);
 
         card = TestUtils::createNode(50, 50, 250, 250,
-                [](TestCanvas& canvas) {
+                [](RenderProperties& props, TestCanvas& canvas) {
             canvas.drawColor(0xFFFF00FF, SkXfermode::kSrcOver_Mode);
 
             SkRegion region;
diff --git a/libs/hwui/tests/scenes/SaveLayerAnimation.cpp b/libs/hwui/tests/scenes/SaveLayerAnimation.cpp
index c62dd19..c73e97b 100644
--- a/libs/hwui/tests/scenes/SaveLayerAnimation.cpp
+++ b/libs/hwui/tests/scenes/SaveLayerAnimation.cpp
@@ -32,7 +32,7 @@
         canvas.drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode); // background
 
         card = TestUtils::createNode(0, 0, 200, 200,
-                [](TestCanvas& canvas) {
+                [](RenderProperties& props, TestCanvas& canvas) {
             canvas.saveLayerAlpha(0, 0, 200, 200, 128, SkCanvas::kClipToLayer_SaveFlag);
             canvas.drawColor(0xFF00FF00, SkXfermode::kSrcOver_Mode); // outer, unclipped
             canvas.saveLayerAlpha(50, 50, 150, 150, 128, SkCanvas::kClipToLayer_SaveFlag);
diff --git a/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp b/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp
index 05fd08a..cc15cc6 100644
--- a/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp
+++ b/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp
@@ -31,7 +31,7 @@
 
 // sync node properties, so properties() reflects correct width and height
 static sp<RenderNode> createSyncedNode(uint32_t width, uint32_t height) {
-    sp<RenderNode> node = TestUtils::createNode(0, 0, width, height);
+    sp<RenderNode> node = TestUtils::createNode(0, 0, width, height, nullptr);
     TestUtils::syncHierarchyPropertiesAndDisplayList(node);
     return node;
 }
diff --git a/libs/hwui/unit_tests/OpReordererTests.cpp b/libs/hwui/unit_tests/OpReordererTests.cpp
index d76086c..607329c 100644
--- a/libs/hwui/unit_tests/OpReordererTests.cpp
+++ b/libs/hwui/unit_tests/OpReordererTests.cpp
@@ -111,25 +111,29 @@
         }
     };
 
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 100, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkBitmap bitmap = TestUtils::createSkBitmap(25, 25);
         canvas.drawRect(0, 0, 100, 200, SkPaint());
         canvas.drawBitmap(bitmap, 10, 10, nullptr);
     });
-    OpReorderer reorderer(100, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200,
+            createSyncedNodeList(node), sLightCenter);
     SimpleTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(4, renderer.getIndex()); // 2 ops + start + end
 }
 
 TEST(OpReorderer, simpleRejection) {
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
         canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op); // intersection should be empty
         canvas.drawRect(0, 0, 400, 400, SkPaint());
         canvas.restore();
     });
-    OpReorderer reorderer(200, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
 
     FailRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
@@ -147,7 +151,8 @@
         }
     };
 
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkBitmap bitmap = TestUtils::createSkBitmap(10, 10);
 
         // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects.
@@ -161,7 +166,8 @@
         canvas.restore();
     });
 
-    OpReorderer reorderer(200, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
     SimpleBatchingTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(2 * LOOPS, renderer.getIndex())
@@ -179,7 +185,8 @@
             EXPECT_TRUE(mIndex++ < LOOPS) << "Text should be beneath all strikethrough rects";
         }
     };
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 2000, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 2000,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkPaint textPaint;
         textPaint.setAntiAlias(true);
         textPaint.setTextSize(20);
@@ -188,7 +195,8 @@
             TestUtils::drawTextToCanvas(&canvas, "test text", textPaint, 10, 100 * (i + 1));
         }
     });
-    OpReorderer reorderer(200, 2000, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 2000), 200, 2000,
+            createSyncedNodeList(node), sLightCenter);
     TextStrikethroughTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(2 * LOOPS, renderer.getIndex())
@@ -214,14 +222,16 @@
         }
     };
 
-    sp<RenderNode> child = TestUtils::createNode(10, 10, 110, 110, [](RecordingCanvas& canvas) {
+    auto child = TestUtils::createNode(10, 10, 110, 110,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkPaint paint;
         paint.setColor(SK_ColorWHITE);
         canvas.drawRect(0, 0, 100, 100, paint);
     });
 
     RenderNode* childPtr = child.get();
-    sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200, [childPtr](RecordingCanvas& canvas) {
+    auto parent = TestUtils::createNode(0, 0, 200, 200,
+            [childPtr](RenderProperties& props, RecordingCanvas& canvas) {
         SkPaint paint;
         paint.setColor(SK_ColorDKGRAY);
         canvas.drawRect(0, 0, 200, 200, paint);
@@ -249,7 +259,8 @@
         }
     };
 
-    sp<RenderNode> node = TestUtils::createNode(0, 0, 200, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         SkBitmap bitmap = TestUtils::createSkBitmap(200, 200);
         canvas.drawBitmap(bitmap, 0, 0, nullptr);
     });
@@ -291,13 +302,14 @@
         }
     };
 
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.saveLayerAlpha(10, 10, 190, 190, 128, SkCanvas::kClipToLayer_SaveFlag);
         canvas.drawRect(10, 10, 190, 190, SkPaint());
         canvas.restore();
     });
-
-    OpReorderer reorderer(200, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
     SaveLayerSimpleTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(4, renderer.getIndex());
@@ -354,7 +366,8 @@
         }
     };
 
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(800, 800, [](RecordingCanvas& canvas) {
+    auto node = TestUtils::createNode(0, 0, 800, 800,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.saveLayerAlpha(0, 0, 800, 800, 128, SkCanvas::kClipToLayer_SaveFlag);
         {
             canvas.drawRect(0, 0, 800, 800, SkPaint());
@@ -367,14 +380,16 @@
         canvas.restore();
     });
 
-    OpReorderer reorderer(800, 800, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(800, 800), 800, 800,
+            createSyncedNodeList(node), sLightCenter);
     SaveLayerNestedTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(10, renderer.getIndex());
 }
 
 TEST(OpReorderer, saveLayerContentRejection) {
-    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        auto node = TestUtils::createNode(0, 0, 200, 200,
+                [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
         canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op);
         canvas.saveLayerAlpha(200, 200, 400, 400, 128, SkCanvas::kClipToLayer_SaveFlag);
@@ -385,7 +400,8 @@
         canvas.restore();
         canvas.restore();
     });
-    OpReorderer reorderer(200, 200, *dl, sLightCenter);
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
 
     FailRenderer renderer;
     // should see no ops, even within the layer, since the layer should be rejected
@@ -424,7 +440,7 @@
         }
     };
 
-    sp<RenderNode> node = TestUtils::createNode(10, 10, 110, 110,
+    auto node = TestUtils::createNode(10, 10, 110, 110,
             [](RenderProperties& props, RecordingCanvas& canvas) {
         props.mutateLayerProperties().setType(LayerType::RenderLayer);
         SkPaint paint;
@@ -562,7 +578,7 @@
 }
 static void drawOrderedNode(RecordingCanvas* canvas, uint8_t expectedDrawOrder, float z) {
     auto node = TestUtils::createNode(0, 0, 100, 100,
-            [expectedDrawOrder](RecordingCanvas& canvas) {
+            [expectedDrawOrder](RenderProperties& props, RecordingCanvas& canvas) {
         drawOrderedRect(&canvas, expectedDrawOrder);
     });
     node->mutateStagingProperties().setTranslationZ(z);
@@ -579,7 +595,7 @@
     };
 
     auto parent = TestUtils::createNode(0, 0, 100, 100,
-            [](RecordingCanvas& canvas) {
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         drawOrderedNode(&canvas, 0, 10.0f); // in reorder=false at this point, so played inorder
         drawOrderedRect(&canvas, 1);
         canvas.insertReorderBarrier(true);
@@ -600,10 +616,93 @@
     EXPECT_EQ(10, renderer.getIndex());
 };
 
+TEST(OpReorderer, projectionReorder) {
+    static const int scrollX = 5;
+    static const int scrollY = 10;
+    class ProjectionReorderTestRenderer : public TestRendererBase {
+    public:
+        void onRectOp(const RectOp& op, const BakedOpState& state) override {
+            const int index = mIndex++;
+
+            Matrix4 expectedMatrix;
+            switch (index) {
+            case 0:
+                EXPECT_EQ(Rect(100, 100), op.unmappedBounds);
+                EXPECT_EQ(SK_ColorWHITE, op.paint->getColor());
+                expectedMatrix.loadIdentity();
+                break;
+            case 1:
+                EXPECT_EQ(Rect(-10, -10, 60, 60), op.unmappedBounds);
+                EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor());
+                expectedMatrix.loadTranslate(50, 50, 0); // TODO: should scroll be respected here?
+                break;
+            case 2:
+                EXPECT_EQ(Rect(100, 50), op.unmappedBounds);
+                EXPECT_EQ(SK_ColorBLUE, op.paint->getColor());
+                expectedMatrix.loadTranslate(-scrollX, 50 - scrollY, 0);
+                break;
+            default:
+                ADD_FAILURE();
+            }
+            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, state.computedState.transform);
+        }
+    };
+
+    /**
+     * 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 scrollX/scrollY, but this does not affect the background
+     * (which isn't affected by scroll).
+     */
+    auto receiverBackground = TestUtils::createNode(0, 0, 100, 100,
+            [](RenderProperties& properties, RecordingCanvas& 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(scrollX);
+        properties.setTranslationY(scrollY);
+
+        SkPaint paint;
+        paint.setColor(SK_ColorWHITE);
+        canvas.drawRect(0, 0, 100, 100, paint);
+    });
+    auto projectingRipple = TestUtils::createNode(50, 0, 100, 50,
+            [](RenderProperties& properties, RecordingCanvas& canvas) {
+        properties.setProjectBackwards(true);
+        properties.setClipToBounds(false);
+        SkPaint paint;
+        paint.setColor(SK_ColorDKGRAY);
+        canvas.drawRect(-10, -10, 60, 60, paint);
+    });
+    auto child = TestUtils::createNode(0, 50, 100, 100,
+            [&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) {
+        SkPaint paint;
+        paint.setColor(SK_ColorBLUE);
+        canvas.drawRect(0, 0, 100, 50, paint);
+        canvas.drawRenderNode(projectingRipple.get());
+    });
+    auto parent = TestUtils::createNode(0, 0, 100, 100,
+            [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) {
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally)
+        canvas.drawRenderNode(receiverBackground.get());
+        canvas.drawRenderNode(child.get());
+        canvas.restore();
+    });
+
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 100), 100, 100,
+            createSyncedNodeList(parent), sLightCenter);
+    ProjectionReorderTestRenderer renderer;
+    reorderer.replayBakedOps<TestDispatcher>(renderer);
+    EXPECT_EQ(3, renderer.getIndex());
+}
+
 // creates a 100x100 shadow casting node with provided translationZ
 static sp<RenderNode> createWhiteRectShadowCaster(float translationZ) {
     return TestUtils::createNode(0, 0, 100, 100,
-            [translationZ] (RenderProperties& properties, RecordingCanvas& canvas) {
+            [translationZ](RenderProperties& properties, RecordingCanvas& canvas) {
         properties.setTranslationZ(translationZ);
         properties.mutableOutline().setRoundRect(0, 0, 100, 100, 0.0f, 1.0f);
         SkPaint paint;
@@ -630,8 +729,8 @@
         }
     };
 
-    sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200,
-            [] (RecordingCanvas& canvas) {
+    auto parent = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.insertReorderBarrier(true);
         canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get());
     });
@@ -666,8 +765,8 @@
         }
     };
 
-    sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200,
-            [] (RecordingCanvas& canvas) {
+    auto parent = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         // save/restore outside of reorderBarrier, so they don't get moved out of place
         canvas.translate(20, 10);
         int count = canvas.saveLayerAlpha(30, 50, 130, 150, 128, SkCanvas::kClipToLayer_SaveFlag);
@@ -706,7 +805,7 @@
         }
     };
 
-    sp<RenderNode> parent = TestUtils::createNode(50, 60, 150, 160,
+    auto parent = TestUtils::createNode(50, 60, 150, 160,
             [](RenderProperties& props, RecordingCanvas& canvas) {
         props.mutateLayerProperties().setType(LayerType::RenderLayer);
         canvas.insertReorderBarrier(true);
@@ -749,8 +848,8 @@
             EXPECT_TRUE(index == 2 || index == 3);
         }
     };
-    sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200,
-            [] (RecordingCanvas& canvas) {
+    auto parent = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.insertReorderBarrier(true);
         canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get());
         canvas.drawRenderNode(createWhiteRectShadowCaster(5.0001f).get());
@@ -954,7 +1053,7 @@
     SaveLayerAlphaData observedData;
     testSaveLayerAlphaClip(&observedData, [](RenderProperties& properties) {
         // Translate and rotate the view so that the only visible part is the top left corner of
-        // the view. It will form an isoceles right triangle with a long side length of 200 at the
+        // the view. It will form an isosceles right triangle with a long side length of 200 at the
         // bottom of the viewport.
         properties.setTranslationX(100);
         properties.setTranslationY(100);
diff --git a/libs/hwui/unit_tests/RecordingCanvasTests.cpp b/libs/hwui/unit_tests/RecordingCanvasTests.cpp
index c23d47e..ceb61af 100644
--- a/libs/hwui/unit_tests/RecordingCanvasTests.cpp
+++ b/libs/hwui/unit_tests/RecordingCanvasTests.cpp
@@ -313,6 +313,49 @@
     EXPECT_EQ(3, count);
 }
 
+TEST(RecordingCanvas, drawRenderNode_projection) {
+    sp<RenderNode> background = TestUtils::createNode(50, 50, 150, 150,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
+        SkPaint paint;
+        paint.setColor(SK_ColorWHITE);
+        canvas.drawRect(0, 0, 100, 100, paint);
+    });
+    {
+        background->mutateStagingProperties().setProjectionReceiver(false);
+
+        // NO RECEIVER PRESENT
+        auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200,
+                    [&background](RecordingCanvas& canvas) {
+            canvas.drawRect(0, 0, 100, 100, SkPaint());
+            canvas.drawRenderNode(background.get());
+            canvas.drawRect(0, 0, 100, 100, SkPaint());
+        });
+        EXPECT_EQ(-1, dl->projectionReceiveIndex)
+                << "no projection receiver should have been observed";
+    }
+    {
+        background->mutateStagingProperties().setProjectionReceiver(true);
+
+        // RECEIVER PRESENT
+        auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200,
+                    [&background](RecordingCanvas& canvas) {
+            canvas.drawRect(0, 0, 100, 100, SkPaint());
+            canvas.drawRenderNode(background.get());
+            canvas.drawRect(0, 0, 100, 100, SkPaint());
+        });
+
+        ASSERT_EQ(3u, dl->getOps().size()) << "Must be three ops";
+        auto op = dl->getOps()[1];
+        EXPECT_EQ(RecordedOpId::RenderNodeOp, op->opId);
+        EXPECT_EQ(1, dl->projectionReceiveIndex)
+                << "correct projection receiver not identified";
+
+        // verify the behavior works even though projection receiver hasn't been sync'd yet
+        EXPECT_TRUE(background->stagingProperties().isProjectionReceiver());
+        EXPECT_FALSE(background->properties().isProjectionReceiver());
+    }
+}
+
 TEST(RecordingCanvas, insertReorderBarrier) {
     auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
         canvas.drawRect(0, 0, 400, 400, SkPaint());
diff --git a/libs/hwui/utils/TestUtils.h b/libs/hwui/utils/TestUtils.h
index f9fa242..3a886f9 100644
--- a/libs/hwui/utils/TestUtils.h
+++ b/libs/hwui/utils/TestUtils.h
@@ -121,7 +121,7 @@
     }
 
     static sp<RenderNode> createNode(int left, int top, int right, int bottom,
-            std::function<void(RenderProperties& props, TestCanvas& canvas)> setup = nullptr) {
+            std::function<void(RenderProperties& props, TestCanvas& canvas)> setup) {
 #if HWUI_NULL_GPU
         // if RenderNodes are being sync'd/used, device info will be needed, since
         // DeviceInfo::maxTextureSize() affects layer property
@@ -140,22 +140,6 @@
         return node;
     }
 
-    static sp<RenderNode> createNode(int left, int top, int right, int bottom,
-            std::function<void(RenderProperties& props)> setup) {
-        return createNode(left, top, right, bottom,
-                [&setup](RenderProperties& props, TestCanvas& canvas) {
-            setup(props);
-        });
-    }
-
-    static sp<RenderNode> createNode(int left, int top, int right, int bottom,
-            std::function<void(TestCanvas& canvas)> setup) {
-        return createNode(left, top, right, bottom,
-                [&setup](RenderProperties& props, TestCanvas& canvas) {
-            setup(canvas);
-        });
-    }
-
     static void recordNode(RenderNode& node,
             std::function<void(TestCanvas&)> contentCallback) {
        TestCanvas canvas(node.stagingProperties().getWidth(),
@@ -164,6 +148,13 @@
        node.setStagingDisplayList(canvas.finishRecording());
     }
 
+    /**
+     * Forces a sync of a tree of RenderNode, such that every descendant will have its staging
+     * properties and DisplayList moved to the render copies.
+     *
+     * Note: does not check dirtiness bits, so any non-staging DisplayLists will be discarded.
+     * For this reason, this should generally only be called once on a tree.
+     */
     static void syncHierarchyPropertiesAndDisplayList(sp<RenderNode>& node) {
         syncHierarchyPropertiesAndDisplayListImpl(node.get());
     }