Work to support saveLayer in new pipeline

clipped SaveLayers will now be pulled to the beginning of the frame,
prior to drawing FBO 0. This will remove the need for switching FBOs
mid-frame.

Change-Id: I4d8dc1f845e84e9b49d5acdf4f4703eef4a9cb06
diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp
index 4d9f9b4..94806ca 100644
--- a/libs/hwui/BakedOpRenderer.cpp
+++ b/libs/hwui/BakedOpRenderer.cpp
@@ -74,53 +74,64 @@
 #endif
 }
 
-void BakedOpRenderer::onRenderNodeOp(Info*, const RenderNodeOp&, const BakedOpState&) {
+void BakedOpRenderer::onRenderNodeOp(Info&, const RenderNodeOp&, const BakedOpState&) {
     LOG_ALWAYS_FATAL("unsupported operation");
 }
 
-void BakedOpRenderer::onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) {
-    info->caches.textureState().activateTexture(0); // TODO: should this be automatic, and/or elsewhere?
-    Texture* texture = info->getTexture(op.bitmap);
+void BakedOpRenderer::onBitmapOp(Info& info, const BitmapOp& op, const BakedOpState& state) {
+    info.caches.textureState().activateTexture(0); // TODO: should this be automatic, and/or elsewhere?
+    Texture* texture = info.getTexture(op.bitmap);
     if (!texture) return;
     const AutoTexture autoCleanup(texture);
 
     const int textureFillFlags = (op.bitmap->colorType() == kAlpha_8_SkColorType)
             ? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
     Glop glop;
-    GlopBuilder(info->renderState, info->caches, &glop)
+    GlopBuilder(info.renderState, info.caches, &glop)
             .setRoundRectClipState(state.roundRectClipState)
             .setMeshTexturedUnitQuad(texture->uvMapper)
             .setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha)
             .setTransform(state.computedState.transform, TransformFlags::None)
             .setModelViewMapUnitToRectSnap(Rect(0, 0, texture->width, texture->height))
             .build();
-    info->renderGlop(state, glop);
+    info.renderGlop(state, glop);
 }
 
-void BakedOpRenderer::onRectOp(Info* info, const RectOp& op, const BakedOpState& state) {
+void BakedOpRenderer::onRectOp(Info& info, const RectOp& op, const BakedOpState& state) {
     Glop glop;
-    GlopBuilder(info->renderState, info->caches, &glop)
+    GlopBuilder(info.renderState, info.caches, &glop)
             .setRoundRectClipState(state.roundRectClipState)
             .setMeshUnitQuad()
             .setFillPaint(*op.paint, state.alpha)
             .setTransform(state.computedState.transform, TransformFlags::None)
             .setModelViewMapUnitToRect(op.unmappedBounds)
             .build();
-    info->renderGlop(state, glop);
+    info.renderGlop(state, glop);
 }
 
-void BakedOpRenderer::onSimpleRectsOp(Info* info, const SimpleRectsOp& op, const BakedOpState& state) {
+void BakedOpRenderer::onSimpleRectsOp(Info& info, const SimpleRectsOp& op, const BakedOpState& state) {
     Glop glop;
-    GlopBuilder(info->renderState, info->caches, &glop)
+    GlopBuilder(info.renderState, info.caches, &glop)
             .setRoundRectClipState(state.roundRectClipState)
             .setMeshIndexedQuads(&op.vertices[0], op.vertexCount / 4)
             .setFillPaint(*op.paint, state.alpha)
             .setTransform(state.computedState.transform, TransformFlags::None)
             .setModelViewOffsetRect(0, 0, op.unmappedBounds)
             .build();
-    info->renderGlop(state, glop);
+    info.renderGlop(state, glop);
 }
 
+void BakedOpRenderer::onBeginLayerOp(Info& info, const BeginLayerOp& op, const BakedOpState& state) {
+    LOG_ALWAYS_FATAL("unsupported operation");
+}
+
+void BakedOpRenderer::onEndLayerOp(Info& info, const EndLayerOp& op, const BakedOpState& state) {
+    LOG_ALWAYS_FATAL("unsupported operation");
+}
+
+void BakedOpRenderer::onLayerOp(Info& info, const LayerOp& op, const BakedOpState& state) {
+    LOG_ALWAYS_FATAL("unsupported operation");
+}
 
 } // namespace uirenderer
 } // namespace android
diff --git a/libs/hwui/BakedOpRenderer.h b/libs/hwui/BakedOpRenderer.h
index b8b4426..f45dbe4 100644
--- a/libs/hwui/BakedOpRenderer.h
+++ b/libs/hwui/BakedOpRenderer.h
@@ -65,7 +65,7 @@
      * These functions will perform the actual rendering of the individual operations in OpenGL,
      * given the transform/clip and other state built into the BakedOpState object passed in.
      */
-    #define BAKED_OP_RENDERER_METHOD(Type) static void on##Type(Info* info, const Type& op, const BakedOpState& state);
+    #define BAKED_OP_RENDERER_METHOD(Type) static void on##Type(Info& info, const Type& op, const BakedOpState& state);
     MAP_OPS(BAKED_OP_RENDERER_METHOD);
 };
 
diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h
index e2201ca..ddb8c84 100644
--- a/libs/hwui/BakedOpState.h
+++ b/libs/hwui/BakedOpState.h
@@ -68,7 +68,7 @@
         // resolvedClipRect = intersect(parentMatrix * localClip, parentClip)
         clipRect = recordedOp.localClipRect;
         snapshot.transform->mapRect(clipRect);
-        clipRect.doIntersect(snapshot.getClipRect());
+        clipRect.doIntersect(snapshot.getRenderTargetClip());
         clipRect.snapToPixelBoundaries();
 
         // resolvedClippedBounds = intersect(resolvedMatrix * opBounds, resolvedClipRect)
diff --git a/libs/hwui/CanvasState.cpp b/libs/hwui/CanvasState.cpp
index eca71c6..6a6cc42 100644
--- a/libs/hwui/CanvasState.cpp
+++ b/libs/hwui/CanvasState.cpp
@@ -259,7 +259,7 @@
     currentTransform()->mapRect(r);
     r.snapGeometryToPixelBoundaries(snapOut);
 
-    Rect clipRect(currentClipRect());
+    Rect clipRect(currentRenderTargetClip());
     clipRect.snapToPixelBoundaries();
 
     if (!clipRect.intersects(r)) return true;
@@ -287,7 +287,7 @@
     currentTransform()->mapRect(r);
     r.roundOut(); // rounded out to be conservative
 
-    Rect clipRect(currentClipRect());
+    Rect clipRect(currentRenderTargetClip());
     clipRect.snapToPixelBoundaries();
 
     if (!clipRect.intersects(r)) return true;
diff --git a/libs/hwui/CanvasState.h b/libs/hwui/CanvasState.h
index be57f44..4709ef4 100644
--- a/libs/hwui/CanvasState.h
+++ b/libs/hwui/CanvasState.h
@@ -147,7 +147,7 @@
     void setInvisible(bool value) { mSnapshot->invisible = value; }
 
     inline const mat4* currentTransform() const { return currentSnapshot()->transform; }
-    inline const Rect& currentClipRect() const { return currentSnapshot()->getClipRect(); }
+    inline const Rect& currentRenderTargetClip() const { return currentSnapshot()->getRenderTargetClip(); }
     inline Region* currentRegion() const { return currentSnapshot()->region; }
     inline int currentFlags() const { return currentSnapshot()->flags; }
     const Vector3& currentLightCenter() const { return currentSnapshot()->getRelativeLightCenter(); }
diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp
index 7c0e257..c1417c4 100644
--- a/libs/hwui/OpReorderer.cpp
+++ b/libs/hwui/OpReorderer.cpp
@@ -52,7 +52,8 @@
     const std::vector<BakedOpState*>& getOps() const { return mOps; }
 
     void dump() const {
-        ALOGD("    Batch %p, merging %d, bounds " RECT_STRING, this, mMerging, RECT_ARGS(mBounds));
+        ALOGD("    Batch %p, id %d, merging %d, count %d, bounds " RECT_STRING,
+                this, mBatchId, mMerging, mOps.size(), RECT_ARGS(mBounds));
     }
 protected:
     batchid_t mBatchId;
@@ -201,17 +202,106 @@
     Rect mClipRect;
 };
 
-class NullClient: public CanvasStateClient {
-    void onViewportInitialized() override {}
-    void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {}
-    GLuint getTargetFbo() const override { return 0; }
-};
-static NullClient sNullClient;
+// iterate back toward target to see if anything drawn since should overlap the new op
+// if no target, merging ops still interate to find similar batch to insert after
+void OpReorderer::LayerReorderer::locateInsertIndex(int batchId, const Rect& clippedBounds,
+        BatchBase** targetBatch, size_t* insertBatchIndex) const {
+    for (int i = mBatches.size() - 1; i >= 0; i--) {
+        BatchBase* overBatch = mBatches[i];
+
+        if (overBatch == *targetBatch) break;
+
+        // TODO: also consider shader shared between batch types
+        if (batchId == overBatch->getBatchId()) {
+            *insertBatchIndex = i + 1;
+            if (!*targetBatch) break; // found insert position, quit
+        }
+
+        if (overBatch->intersects(clippedBounds)) {
+            // NOTE: it may be possible to optimize for special cases where two operations
+            // of the same batch/paint could swap order, such as with a non-mergeable
+            // (clipped) and a mergeable text operation
+            *targetBatch = nullptr;
+            break;
+        }
+    }
+}
+
+void OpReorderer::LayerReorderer::deferUnmergeableOp(LinearAllocator& allocator,
+        BakedOpState* op, batchid_t batchId) {
+    OpBatch* targetBatch = mBatchLookup[batchId];
+
+    size_t insertBatchIndex = mBatches.size();
+    if (targetBatch) {
+        locateInsertIndex(batchId, op->computedState.clippedBounds,
+                (BatchBase**)(&targetBatch), &insertBatchIndex);
+    }
+
+    if (targetBatch) {
+        targetBatch->batchOp(op);
+    } else  {
+        // new non-merging batch
+        targetBatch = new (allocator) OpBatch(batchId, op);
+        mBatchLookup[batchId] = targetBatch;
+        mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch);
+    }
+}
+
+// insertion point of a new batch, will hopefully be immediately after similar batch
+// (generally, should be similar shader)
+void OpReorderer::LayerReorderer::deferMergeableOp(LinearAllocator& allocator,
+        BakedOpState* op, batchid_t batchId, mergeid_t mergeId) {
+    MergingOpBatch* targetBatch = nullptr;
+
+    // Try to merge with any existing batch with same mergeId
+    auto getResult = mMergingBatchLookup[batchId].find(mergeId);
+    if (getResult != mMergingBatchLookup[batchId].end()) {
+        targetBatch = getResult->second;
+        if (!targetBatch->canMergeWith(op)) {
+            targetBatch = nullptr;
+        }
+    }
+
+    size_t insertBatchIndex = mBatches.size();
+    locateInsertIndex(batchId, op->computedState.clippedBounds,
+            (BatchBase**)(&targetBatch), &insertBatchIndex);
+
+    if (targetBatch) {
+        targetBatch->mergeOp(op);
+    } else  {
+        // new merging batch
+        targetBatch = new (allocator) MergingOpBatch(batchId, op);
+        mMergingBatchLookup[batchId].insert(std::make_pair(mergeId, targetBatch));
+
+        mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch);
+    }
+}
+
+void OpReorderer::LayerReorderer::replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers) const {
+    for (const BatchBase* batch : mBatches) {
+        // TODO: different behavior based on batch->isMerging()
+        for (const BakedOpState* op : batch->getOps()) {
+            receivers[op->op->opId](arg, *op->op, *op);
+        }
+    }
+}
+
+void OpReorderer::LayerReorderer::dump() const {
+    for (const BatchBase* batch : mBatches) {
+        batch->dump();
+    }
+}
 
 OpReorderer::OpReorderer()
-        : mCanvasState(sNullClient) {
+        : mCanvasState(*this) {
+    mLayerReorderers.emplace_back();
+    mLayerStack.push_back(0);
 }
 
+void OpReorderer::onViewportInitialized() {}
+
+void OpReorderer::onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {}
+
 void OpReorderer::defer(const SkRect& clip, int viewportWidth, int viewportHeight,
         const std::vector< sp<RenderNode> >& nodes) {
     mCanvasState.initializeSaveStack(viewportWidth, viewportHeight,
@@ -244,11 +334,11 @@
  * 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_RECIEVER(Type) \
+#define OP_RECEIVER(Type) \
         [](OpReorderer& reorderer, const RecordedOp& op) { reorderer.on##Type(static_cast<const Type&>(op)); },
 void OpReorderer::deferImpl(const DisplayList& displayList) {
     static std::function<void(OpReorderer& reorderer, const RecordedOp&)> receivers[] = {
-        MAP_OPS(OP_RECIEVER)
+        MAP_OPS(OP_RECEIVER)
     };
     for (const DisplayList::Chunk& chunk : displayList.getChunks()) {
         for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) {
@@ -260,23 +350,18 @@
 
 void OpReorderer::replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers) {
     ATRACE_NAME("flush drawing commands");
-    for (const BatchBase* batch : mBatches) {
-        // TODO: different behavior based on batch->isMerging()
-        for (const BakedOpState* op : batch->getOps()) {
-            receivers[op->op->opId](arg, *op->op, *op);
-        }
+    // Relay through layers in reverse order, since layers
+    // later in the list will be drawn by earlier ones
+    for (int i = mLayerReorderers.size() - 1; i >= 0; i--) {
+        mLayerReorderers[i].replayBakedOpsImpl(arg, receivers);
     }
 }
 
-BakedOpState* OpReorderer::bakeOpState(const RecordedOp& recordedOp) {
-    return BakedOpState::tryConstruct(mAllocator, *mCanvasState.currentSnapshot(), recordedOp);
-}
-
 void OpReorderer::onRenderNodeOp(const RenderNodeOp& op) {
     if (op.renderNode->nothingToDraw()) {
         return;
     }
-    mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag);
+    int count = mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag);
 
     // apply state from RecordedOp
     mCanvasState.concatMatrix(op.localMatrix);
@@ -285,10 +370,10 @@
 
     // apply RenderProperties state
     if (op.renderNode->applyViewProperties(mCanvasState)) {
-        // not rejected do ops...
+        // if node not rejected based on properties, do ops...
         deferImpl(op.renderNode->getDisplayList());
     }
-    mCanvasState.restore();
+    mCanvasState.restoreToCount(count);
 }
 
 static batchid_t tessellatedBatchId(const SkPaint& paint) {
@@ -298,104 +383,70 @@
 }
 
 void OpReorderer::onBitmapOp(const BitmapOp& op) {
-    BakedOpState* bakedStateOp = bakeOpState(op);
+    BakedOpState* bakedStateOp = tryBakeOpState(op);
     if (!bakedStateOp) return; // quick rejected
 
     mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID();
     // TODO: AssetAtlas
-
-    deferMergeableOp(bakedStateOp, OpBatchType::Bitmap, mergeId);
+    currentLayer().deferMergeableOp(mAllocator, bakedStateOp, OpBatchType::Bitmap, mergeId);
 }
 
 void OpReorderer::onRectOp(const RectOp& op) {
-    BakedOpState* bakedStateOp = bakeOpState(op);
+    BakedOpState* bakedStateOp = tryBakeOpState(op);
     if (!bakedStateOp) return; // quick rejected
-    deferUnmergeableOp(bakedStateOp, tessellatedBatchId(*op.paint));
+    currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, tessellatedBatchId(*op.paint));
 }
 
 void OpReorderer::onSimpleRectsOp(const SimpleRectsOp& op) {
-    BakedOpState* bakedStateOp = bakeOpState(op);
+    BakedOpState* bakedStateOp = tryBakeOpState(op);
     if (!bakedStateOp) return; // quick rejected
-    deferUnmergeableOp(bakedStateOp, OpBatchType::Vertices);
+    currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, OpBatchType::Vertices);
 }
 
-// iterate back toward target to see if anything drawn since should overlap the new op
-// if no target, merging ops still interate to find similar batch to insert after
-void OpReorderer::locateInsertIndex(int batchId, const Rect& clippedBounds,
-        BatchBase** targetBatch, size_t* insertBatchIndex) const {
-    for (int i = mBatches.size() - 1; i >= mEarliestBatchIndex; i--) {
-        BatchBase* overBatch = mBatches[i];
+// TODO: test rejection at defer time, where the bounds become empty
+void OpReorderer::onBeginLayerOp(const BeginLayerOp& op) {
+    mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag);
+    mCanvasState.writableSnapshot()->transform->loadIdentity();
+    mCanvasState.writableSnapshot()->initializeViewport(
+            (int) op.unmappedBounds.getWidth(), (int) op.unmappedBounds.getHeight());
+    mCanvasState.writableSnapshot()->roundRectClipState = nullptr;
 
-        if (overBatch == *targetBatch) break;
+    // create a new layer, and push its index on the stack
+    mLayerStack.push_back(mLayerReorderers.size());
+    mLayerReorderers.emplace_back();
+    mLayerReorderers.back().beginLayerOp = &op;
+}
 
-        // TODO: also consider shader shared between batch types
-        if (batchId == overBatch->getBatchId()) {
-            *insertBatchIndex = i + 1;
-            if (!*targetBatch) break; // found insert position, quit
-        }
+void OpReorderer::onEndLayerOp(const EndLayerOp& /* ignored */) {
+    mCanvasState.restore();
 
-        if (overBatch->intersects(clippedBounds)) {
-            // NOTE: it may be possible to optimize for special cases where two operations
-            // of the same batch/paint could swap order, such as with a non-mergeable
-            // (clipped) and a mergeable text operation
-            *targetBatch = nullptr;
-            break;
-        }
+    const BeginLayerOp& beginLayerOp = *currentLayer().beginLayerOp;
+
+    // pop finished layer off of the stack
+    int finishedLayerIndex = mLayerStack.back();
+    mLayerStack.pop_back();
+
+    // record the draw operation into the previous layer's list of draw commands
+    // uses state from the associated beginLayerOp, since it has all the state needed for drawing
+    LayerOp* drawLayerOp = new (mAllocator) LayerOp(
+            beginLayerOp.unmappedBounds,
+            beginLayerOp.localMatrix,
+            beginLayerOp.localClipRect,
+            beginLayerOp.paint);
+    BakedOpState* bakedOpState = tryBakeOpState(*drawLayerOp);
+
+    if (bakedOpState) {
+        // Layer will be drawn into parent layer (which is now current, since we popped mLayerStack)
+        currentLayer().deferUnmergeableOp(mAllocator, bakedOpState, OpBatchType::Bitmap);
+    } else {
+        // Layer won't be drawn - delete its drawing batches to prevent it from doing any work
+        mLayerReorderers[finishedLayerIndex].clear();
+        return;
     }
 }
 
-void OpReorderer::deferUnmergeableOp(BakedOpState* op, batchid_t batchId) {
-    OpBatch* targetBatch = mBatchLookup[batchId];
-
-    size_t insertBatchIndex = mBatches.size();
-    if (targetBatch) {
-        locateInsertIndex(batchId, op->computedState.clippedBounds,
-                (BatchBase**)(&targetBatch), &insertBatchIndex);
-    }
-
-    if (targetBatch) {
-        targetBatch->batchOp(op);
-    } else  {
-        // new non-merging batch
-        targetBatch = new (mAllocator) OpBatch(batchId, op);
-        mBatchLookup[batchId] = targetBatch;
-        mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch);
-    }
-}
-
-// insertion point of a new batch, will hopefully be immediately after similar batch
-// (generally, should be similar shader)
-void OpReorderer::deferMergeableOp(BakedOpState* op, batchid_t batchId, mergeid_t mergeId) {
-    MergingOpBatch* targetBatch = nullptr;
-
-    // Try to merge with any existing batch with same mergeId
-    auto getResult = mMergingBatches[batchId].find(mergeId);
-    if (getResult != mMergingBatches[batchId].end()) {
-        targetBatch = getResult->second;
-        if (!targetBatch->canMergeWith(op)) {
-            targetBatch = nullptr;
-        }
-    }
-
-    size_t insertBatchIndex = mBatches.size();
-    locateInsertIndex(batchId, op->computedState.clippedBounds,
-            (BatchBase**)(&targetBatch), &insertBatchIndex);
-
-    if (targetBatch) {
-        targetBatch->mergeOp(op);
-    } else  {
-        // new merging batch
-        targetBatch = new (mAllocator) MergingOpBatch(batchId, op);
-        mMergingBatches[batchId].insert(std::make_pair(mergeId, targetBatch));
-
-        mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch);
-    }
-}
-
-void OpReorderer::dump() {
-    for (const BatchBase* batch : mBatches) {
-        batch->dump();
-    }
+void OpReorderer::onLayerOp(const LayerOp& op) {
+    LOG_ALWAYS_FATAL("unsupported");
 }
 
 } // namespace uirenderer
diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h
index 6776a3c..73dc9af 100644
--- a/libs/hwui/OpReorderer.h
+++ b/libs/hwui/OpReorderer.h
@@ -54,19 +54,63 @@
     };
 }
 
-class OpReorderer {
+class OpReorderer : public CanvasStateClient {
+    typedef std::function<void(void*, const RecordedOp&, const BakedOpState&)> BakedOpReceiver;
+
+    /**
+     * Stores the deferred render operations and state used to compute ordering
+     * for a single FBO/layer.
+     */
+    class LayerReorderer {
+    public:
+        // iterate back toward target to see if anything drawn since should overlap the new op
+        // if no target, merging ops still iterate to find similar batch to insert after
+        void locateInsertIndex(int batchId, const Rect& clippedBounds,
+                BatchBase** targetBatch, size_t* insertBatchIndex) const;
+
+        void deferUnmergeableOp(LinearAllocator& allocator, BakedOpState* op, batchid_t batchId);
+
+        // insertion point of a new batch, will hopefully be immediately after similar batch
+        // (generally, should be similar shader)
+        void deferMergeableOp(LinearAllocator& allocator,
+                BakedOpState* op, batchid_t batchId, mergeid_t mergeId);
+
+        void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers) const;
+
+        void clear() {
+            mBatches.clear();
+        }
+
+        void dump() const;
+
+        const BeginLayerOp* beginLayerOp = nullptr;
+
+    private:
+        std::vector<BatchBase*> mBatches;
+
+        /**
+         * Maps the mergeid_t returned by an op's getMergeId() to the most recently seen
+         * MergingDrawBatch of that id. These ids are unique per draw type and guaranteed to not
+         * collide, which avoids the need to resolve mergeid collisions.
+         */
+        std::unordered_map<mergeid_t, MergingOpBatch*> mMergingBatchLookup[OpBatchType::Count];
+
+        // Maps batch ids to the most recent *non-merging* batch of that id
+        OpBatch* mBatchLookup[OpBatchType::Count] = { nullptr };
+
+    };
 public:
     OpReorderer();
+    virtual ~OpReorderer() {}
 
     // TODO: not final, just presented this way for simplicity. Layers too?
     void defer(const SkRect& clip, int viewportWidth, int viewportHeight,
             const std::vector< sp<RenderNode> >& nodes);
 
     void defer(int viewportWidth, int viewportHeight, const DisplayList& displayList);
-    typedef std::function<void(void*, const RecordedOp&, const BakedOpState&)> BakedOpReceiver;
 
     /**
-     * replayBakedOps() is templated based on what class will recieve ops being replayed.
+     * replayBakedOps() is templated based on what class will receive ops being replayed.
      *
      * It constructs a lookup array of lambdas, which allows a recorded BakeOpState to use
      * state->op->opId to lookup a receiver that will be called when the op is replayed.
@@ -77,19 +121,37 @@
      */
 #define BAKED_OP_RECEIVER(Type) \
     [](void* internalArg, const RecordedOp& op, const BakedOpState& state) { \
-        StaticReceiver::on##Type(static_cast<Arg*>(internalArg), static_cast<const Type&>(op), state); \
+        StaticReceiver::on##Type(*(static_cast<Arg*>(internalArg)), static_cast<const Type&>(op), state); \
     },
     template <typename StaticReceiver, typename Arg>
-    void replayBakedOps(Arg* arg) {
+    void replayBakedOps(Arg& arg) {
         static BakedOpReceiver receivers[] = {
             MAP_OPS(BAKED_OP_RECEIVER)
         };
-        StaticReceiver::startFrame(*arg);
-        replayBakedOpsImpl((void*)arg, receivers);
-        StaticReceiver::endFrame(*arg);
+        StaticReceiver::startFrame(arg);
+        replayBakedOpsImpl((void*)&arg, receivers);
+        StaticReceiver::endFrame(arg);
     }
+
+    void dump() const {
+        for (auto&& layer : mLayerReorderers) {
+            layer.dump();
+        }
+    }
+
+    ///////////////////////////////////////////////////////////////////
+    /// CanvasStateClient interface
+    ///////////////////////////////////////////////////////////////////
+    virtual void onViewportInitialized() override;
+    virtual void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) override;
+    virtual GLuint getTargetFbo() const override { return 0; }
+
 private:
-    BakedOpState* bakeOpState(const RecordedOp& recordedOp);
+    LayerReorderer& currentLayer() { return mLayerReorderers[mLayerStack.back()]; }
+
+    BakedOpState* tryBakeOpState(const RecordedOp& recordedOp) {
+        return BakedOpState::tryConstruct(mAllocator, *mCanvasState.currentSnapshot(), recordedOp);
+    }
 
     void deferImpl(const DisplayList& displayList);
 
@@ -105,36 +167,27 @@
     void on##Type(const Type& op);
     MAP_OPS(INTERNAL_OP_HANDLER)
 
-    // iterate back toward target to see if anything drawn since should overlap the new op
-    // if no target, merging ops still iterate to find similar batch to insert after
-    void locateInsertIndex(int batchId, const Rect& clippedBounds,
-            BatchBase** targetBatch, size_t* insertBatchIndex) const;
+    // List of every deferred layer's render state. Replayed in reverse order to render a frame.
+    std::vector<LayerReorderer> mLayerReorderers;
 
-    void deferUnmergeableOp(BakedOpState* op, batchid_t batchId);
+    /*
+     * Stack of indices within mLayerReorderers representing currently active layers. If drawing
+     * layerA within a layerB, will contain, in order:
+     *  - 0 (representing FBO 0, always present)
+     *  - layerB's index
+     *  - layerA's index
+     *
+     * Note that this doesn't vector doesn't always map onto all values of mLayerReorderers. When a
+     * layer is finished deferring, it will still be represented in mLayerReorderers, but it's index
+     * won't be in mLayerStack. This is because it can be replayed, but can't have any more drawing
+     * ops added to it.
+    */
+    std::vector<size_t> mLayerStack;
 
-    // insertion point of a new batch, will hopefully be immediately after similar batch
-    // (generally, should be similar shader)
-    void deferMergeableOp(BakedOpState* op, batchid_t batchId, mergeid_t mergeId);
-
-    void dump();
-
-    std::vector<BatchBase*> mBatches;
-
-    /**
-     * Maps the mergeid_t returned by an op's getMergeId() to the most recently seen
-     * MergingDrawBatch of that id. These ids are unique per draw type and guaranteed to not
-     * collide, which avoids the need to resolve mergeid collisions.
-     */
-    std::unordered_map<mergeid_t, MergingOpBatch*> mMergingBatches[OpBatchType::Count];
-
-    // Maps batch ids to the most recent *non-merging* batch of that id
-    OpBatch* mBatchLookup[OpBatchType::Count] = { nullptr };
     CanvasState mCanvasState;
 
     // contains ResolvedOps and Batches
     LinearAllocator mAllocator;
-
-    int mEarliestBatchIndex = 0;
 };
 
 }; // namespace uirenderer
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index cd03ac4..d4f65b6 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -223,7 +223,7 @@
 void OpenGLRenderer::callDrawGLFunction(Functor* functor, Rect& dirty) {
     if (mState.currentlyIgnored()) return;
 
-    Rect clip(mState.currentClipRect());
+    Rect clip(mState.currentRenderTargetClip());
     clip.snapToPixelBoundaries();
 
     // Since we don't know what the functor will draw, let's dirty
@@ -488,7 +488,7 @@
     currentTransform()->mapRect(bounds);
 
     // Layers only make sense if they are in the framebuffer's bounds
-    bounds.doIntersect(mState.currentClipRect());
+    bounds.doIntersect(mState.currentRenderTargetClip());
     if (!bounds.isEmpty()) {
         // We cannot work with sub-pixels in this case
         bounds.snapToPixelBoundaries();
@@ -1036,7 +1036,7 @@
 }
 
 void OpenGLRenderer::dirtyLayerUnchecked(Rect& bounds, Region* region) {
-    bounds.doIntersect(mState.currentClipRect());
+    bounds.doIntersect(mState.currentRenderTargetClip());
     if (!bounds.isEmpty()) {
         bounds.snapToPixelBoundaries();
         android::Rect dirty(bounds.left, bounds.top, bounds.right, bounds.bottom);
@@ -1084,7 +1084,7 @@
                 .setMeshIndexedQuads(&mesh[0], quadCount)
                 .setFillClear()
                 .setTransform(*currentSnapshot(), transformFlags)
-                .setModelViewOffsetRect(0, 0, Rect(currentSnapshot()->getClipRect()))
+                .setModelViewOffsetRect(0, 0, Rect(currentSnapshot()->getRenderTargetClip()))
                 .build();
         renderGlop(glop, GlopRenderType::LayerClear);
 
@@ -1099,7 +1099,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 
 bool OpenGLRenderer::storeDisplayState(DeferredDisplayState& state, int stateDeferFlags) {
-    const Rect& currentClip = mState.currentClipRect();
+    const Rect& currentClip = mState.currentRenderTargetClip();
     const mat4* currentMatrix = currentTransform();
 
     if (stateDeferFlags & kStateDeferFlag_Draw) {
@@ -1187,7 +1187,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 
 void OpenGLRenderer::setScissorFromClip() {
-    Rect clip(mState.currentClipRect());
+    Rect clip(mState.currentRenderTargetClip());
     clip.snapToPixelBoundaries();
 
     if (mRenderState.scissor().set(clip.left, getViewportHeight() - clip.bottom,
@@ -1430,7 +1430,7 @@
             return;
         }
 
-        DeferredDisplayList deferredList(mState.currentClipRect());
+        DeferredDisplayList deferredList(mState.currentRenderTargetClip());
         DeferStateStruct deferStruct(deferredList, *this, replayFlags);
         renderNode->defer(deferStruct, 0);
 
@@ -1765,7 +1765,7 @@
     // No need to check against the clip, we fill the clip region
     if (mState.currentlyIgnored()) return;
 
-    Rect clip(mState.currentClipRect());
+    Rect clip(mState.currentRenderTargetClip());
     clip.snapToPixelBoundaries();
 
     SkPaint paint;
@@ -2030,7 +2030,7 @@
     }
     fontRenderer.setTextureFiltering(linearFilter);
 
-    const Rect& clip(pureTranslate ? writableSnapshot()->getClipRect() : writableSnapshot()->getLocalClip());
+    const Rect& clip(pureTranslate ? writableSnapshot()->getRenderTargetClip() : writableSnapshot()->getLocalClip());
     Rect bounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
 
     TextDrawFunctor functor(this, x, y, pureTranslate, alpha, mode, paint);
@@ -2191,7 +2191,7 @@
     fontRenderer.setTextureFiltering(linearFilter);
 
     // TODO: Implement better clipping for scaled/rotated text
-    const Rect* clip = !pureTranslate ? nullptr : &mState.currentClipRect();
+    const Rect* clip = !pureTranslate ? nullptr : &mState.currentRenderTargetClip();
     Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
 
     bool status;
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
index a69f030..dd01637 100644
--- a/libs/hwui/RecordedOp.h
+++ b/libs/hwui/RecordedOp.h
@@ -41,7 +41,10 @@
         OP_FN(BitmapOp) \
         OP_FN(RectOp) \
         OP_FN(RenderNodeOp) \
-        OP_FN(SimpleRectsOp)
+        OP_FN(SimpleRectsOp) \
+        OP_FN(BeginLayerOp) \
+        OP_FN(EndLayerOp) \
+        OP_FN(LayerOp)
 
 // Generate OpId enum
 #define IDENTITY_FN(Type) Type,
@@ -112,6 +115,31 @@
     const size_t vertexCount;
 };
 
+/**
+ * Stateful operation! denotes the creation of an off-screen layer,
+ * and that commands following will render into it.
+ */
+struct BeginLayerOp : RecordedOp {
+    BeginLayerOp(BASE_PARAMS)
+            : SUPER(BeginLayerOp) {}
+};
+
+/**
+ * Stateful operation! Denotes end of off-screen layer, and that
+ * commands since last BeginLayerOp should be drawn into parent FBO.
+ *
+ * State in this op is empty, it just serves to signal that a layer has been finished.
+ */
+struct EndLayerOp : RecordedOp {
+    EndLayerOp()
+            : RecordedOp(RecordedOpId::EndLayerOp, Rect(0, 0), Matrix4::identity(), Rect(0, 0), nullptr) {}
+};
+
+struct LayerOp : RecordedOp {
+    LayerOp(BASE_PARAMS)
+            : SUPER(LayerOp) {}
+};
+
 }; // namespace uirenderer
 }; // namespace android
 
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index 3b413aa..1f113bc 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -73,6 +73,20 @@
 }
 
 // ----------------------------------------------------------------------------
+// CanvasStateClient implementation
+// ----------------------------------------------------------------------------
+
+void RecordingCanvas::onViewportInitialized() {
+
+}
+
+void RecordingCanvas::onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {
+    if (removed.flags & Snapshot::kFlagIsFboLayer) {
+        addOp(new (alloc()) EndLayerOp());
+    }
+}
+
+// ----------------------------------------------------------------------------
 // android/graphics/Canvas state operations
 // ----------------------------------------------------------------------------
 // Save (layer)
@@ -97,8 +111,66 @@
 
 int RecordingCanvas::saveLayer(float left, float top, float right, float bottom, const SkPaint* paint,
         SkCanvas::SaveFlags flags) {
-    LOG_ALWAYS_FATAL("TODO");
-    return 0;
+    if (!(flags & SkCanvas::kClipToLayer_SaveFlag)) {
+        LOG_ALWAYS_FATAL("unclipped layers not supported");
+    }
+    // force matrix/clip isolation for layer
+    flags |= SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag;
+
+
+    const Snapshot& previous = *mState.currentSnapshot();
+
+    // initialize the snapshot as though it almost represents an FBO layer so deferred draw
+    // operations will be able to store and restore the current clip and transform info, and
+    // quick rejection will be correct (for display lists)
+
+    const Rect untransformedBounds(left, top, right, bottom);
+
+    // determine clipped bounds relative to previous viewport.
+    Rect visibleBounds = untransformedBounds;
+    previous.transform->mapRect(visibleBounds);
+
+
+    visibleBounds.doIntersect(previous.getRenderTargetClip());
+    visibleBounds.snapToPixelBoundaries();
+
+    Rect previousViewport(0, 0, previous.getViewportWidth(), previous.getViewportHeight());
+    visibleBounds.doIntersect(previousViewport);
+
+    // Map visible bounds back to layer space, and intersect with parameter bounds
+    Rect layerBounds = visibleBounds;
+    Matrix4 inverse;
+    inverse.loadInverse(*previous.transform);
+    inverse.mapRect(layerBounds);
+    layerBounds.doIntersect(untransformedBounds);
+
+    int saveValue = mState.save((int) flags);
+    Snapshot& snapshot = *mState.writableSnapshot();
+
+    // layerBounds is now original bounds, but with clipped to clip
+    // and viewport to ensure it's minimal size.
+    if (layerBounds.isEmpty() || untransformedBounds.isEmpty()) {
+        // Don't bother recording layer, since it's been rejected
+        snapshot.resetClip(0, 0, 0, 0);
+        return saveValue;
+    }
+
+    snapshot.flags |= Snapshot::kFlagFboTarget | Snapshot::kFlagIsFboLayer;
+    snapshot.initializeViewport(untransformedBounds.getWidth(), untransformedBounds.getHeight());
+    snapshot.resetTransform(-untransformedBounds.left, -untransformedBounds.top, 0.0f);
+
+    Rect clip = layerBounds;
+    clip.translate(-untransformedBounds.left, -untransformedBounds.top);
+    snapshot.resetClip(clip.left, clip.top, clip.right, clip.bottom);
+    snapshot.roundRectClipState = nullptr;
+
+    addOp(new (alloc()) BeginLayerOp(
+            Rect(left, top, right, bottom),
+            *previous.transform, // transform to *draw* with
+            previous.getRenderTargetClip(), // clip to *draw* with
+            refPaint(paint)));
+
+    return saveValue;
 }
 
 // Matrix
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index 2179e4c..9c32b1a 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -52,8 +52,8 @@
 // ----------------------------------------------------------------------------
 // CanvasStateClient interface
 // ----------------------------------------------------------------------------
-    virtual void onViewportInitialized() override {}
-    virtual void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) override {}
+    virtual void onViewportInitialized() override;
+    virtual void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) override;
     virtual GLuint getTargetFbo() const override { return -1; }
 
 // ----------------------------------------------------------------------------
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 894a2bd..351fbaa 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -929,7 +929,7 @@
     const RenderProperties& backgroundProps = backgroundOp->renderNode->properties();
     renderer.translate(backgroundProps.getTranslationX(), backgroundProps.getTranslationY());
 
-    // If the projection reciever has an outline, we mask projected content to it
+    // If the projection receiver has an outline, we mask projected content to it
     // (which we know, apriori, are all tessellated paths)
     renderer.setProjectionPathMask(alloc, projectionReceiverOutline);
 
diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h
index f824cc0..abef806 100644
--- a/libs/hwui/RenderProperties.h
+++ b/libs/hwui/RenderProperties.h
@@ -203,8 +203,8 @@
         return RP_SET(mPrimitiveFields.mProjectBackwards, shouldProject);
     }
 
-    bool setProjectionReceiver(bool shouldRecieve) {
-        return RP_SET(mPrimitiveFields.mProjectionReceiver, shouldRecieve);
+    bool setProjectionReceiver(bool shouldReceive) {
+        return RP_SET(mPrimitiveFields.mProjectionReceiver, shouldReceive);
     }
 
     bool isProjectionReceiver() const {
diff --git a/libs/hwui/Snapshot.h b/libs/hwui/Snapshot.h
index aeeda96..4789b33 100644
--- a/libs/hwui/Snapshot.h
+++ b/libs/hwui/Snapshot.h
@@ -158,13 +158,12 @@
     /**
      * Returns the current clip in render target coordinates.
      */
-    const Rect& getRenderTargetClip() { return mClipArea->getClipRect(); }
+    const Rect& getRenderTargetClip() const { return mClipArea->getClipRect(); }
 
     /*
      * Accessor functions so that the clip area can stay private
      */
     bool clipIsEmpty() const { return mClipArea->isEmpty(); }
-    const Rect& getClipRect() const { return mClipArea->getClipRect(); }
     const SkRegion& getClipRegion() const { return mClipArea->getClipRegion(); }
     bool clipIsSimple() const { return mClipArea->isSimple(); }
     const ClipArea& getClipArea() const { return *mClipArea; }
diff --git a/libs/hwui/microbench/OpReordererBench.cpp b/libs/hwui/microbench/OpReordererBench.cpp
index 4c8dedf..cf96d44 100644
--- a/libs/hwui/microbench/OpReordererBench.cpp
+++ b/libs/hwui/microbench/OpReordererBench.cpp
@@ -65,7 +65,7 @@
             MicroBench::DoNotOptimize(&reorderer);
 
             BakedOpRenderer::Info info(caches, renderState, 200, 200, true);
-            reorderer.replayBakedOps<BakedOpRenderer>(&info);
+            reorderer.replayBakedOps<BakedOpRenderer>(info);
         }
         StopBenchmarkTiming();
     });
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 238cf06..f571426 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -323,7 +323,7 @@
     BakedOpRenderer::Info info(Caches::getInstance(), mRenderThread.renderState(),
             frame.width(), frame.height(), mOpaque);
     // TODO: profiler().draw(mCanvas);
-    reorderer.replayBakedOps<BakedOpRenderer>(&info);
+    reorderer.replayBakedOps<BakedOpRenderer>(info);
 
     bool drew = info.didDraw;
 
diff --git a/libs/hwui/unit_tests/OpReordererTests.cpp b/libs/hwui/unit_tests/OpReordererTests.cpp
index e1249fb..d02f89d 100644
--- a/libs/hwui/unit_tests/OpReordererTests.cpp
+++ b/libs/hwui/unit_tests/OpReordererTests.cpp
@@ -27,26 +27,69 @@
 namespace android {
 namespace uirenderer {
 
-#define UNSUPPORTED_OP(Info, Type) \
-        static void on##Type(Info*, const Type&, const BakedOpState&) { FAIL(); }
+/**
+ * Class that redirects static operation dispatch to virtual methods on a Client class.
+ *
+ * The client is recreated for every op (so data cannot be persisted between operations), but the
+ * virtual dispatch allows for default behaviors to be specified without enumerating each operation
+ * for every test.
+ *
+ * onXXXOp methods fail by default - tests should override ops they expect
+ * startFrame/endFrame do nothing by default - tests should override to intercept
+ */
+template<class CustomClient, class Arg>
+class TestReceiver {
+public:
+#define CLIENT_METHOD(Type) \
+    virtual void on##Type(Arg&, const Type&, const BakedOpState&) { FAIL(); }
+    class Client {
+    public:
+        virtual ~Client() {};
+        MAP_OPS(CLIENT_METHOD)
+
+        virtual void startFrame(Arg& info) {}
+        virtual void endFrame(Arg& info) {}
+    };
+
+#define DISPATCHER_METHOD(Type) \
+    static void on##Type(Arg& arg, const Type& op, const BakedOpState& state) { \
+        CustomClient client; client.on##Type(arg, op, state); \
+    }
+    MAP_OPS(DISPATCHER_METHOD)
+
+    static void startFrame(Arg& info) {
+        CustomClient client;
+        client.startFrame(info);
+    }
+
+    static void endFrame(Arg& info) {
+        CustomClient client;
+        client.endFrame(info);
+    }
+};
 
 class Info {
 public:
     int index = 0;
 };
 
-class SimpleReceiver {
+// Receiver class which will fail if it receives any ops
+class FailReceiver : public TestReceiver<FailReceiver, Info>::Client {};
+
+class SimpleReceiver : public TestReceiver<SimpleReceiver, Info>::Client {
 public:
-    static void onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) {
-        EXPECT_EQ(1, info->index++);
+    void startFrame(Info& info) override {
+        EXPECT_EQ(0, info.index++);
     }
-    static void onRectOp(Info* info, const RectOp& op, const BakedOpState& state) {
-        EXPECT_EQ(0, info->index++);
+    void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override {
+        EXPECT_EQ(1, info.index++);
     }
-    UNSUPPORTED_OP(Info, RenderNodeOp)
-    UNSUPPORTED_OP(Info, SimpleRectsOp)
-    static void startFrame(Info& info) {}
-    static void endFrame(Info& info) {}
+    void onBitmapOp(Info& info, const BitmapOp& op, const BakedOpState& state) override {
+        EXPECT_EQ(2, info.index++);
+    }
+    void endFrame(Info& info) override {
+        EXPECT_EQ(3, info.index++);
+    }
 };
 TEST(OpReorderer, simple) {
     auto dl = TestUtils::createDisplayList<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
@@ -54,28 +97,39 @@
         canvas.drawRect(0, 0, 100, 200, SkPaint());
         canvas.drawBitmap(bitmap, 10, 10, nullptr);
     });
-
     OpReorderer reorderer;
     reorderer.defer(200, 200, *dl);
 
     Info info;
-    reorderer.replayBakedOps<SimpleReceiver>(&info);
+    reorderer.replayBakedOps<TestReceiver<SimpleReceiver, Info>>(info);
+    EXPECT_EQ(4, info.index); // 2 ops + start + end
+}
+
+
+TEST(OpReorderer, simpleRejection) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](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;
+    reorderer.defer(200, 200, *dl);
+
+    Info info;
+    reorderer.replayBakedOps<TestReceiver<FailReceiver, Info>>(info);
 }
 
 
 static int SIMPLE_BATCHING_LOOPS = 5;
-class SimpleBatchingReceiver {
+class SimpleBatchingReceiver : public TestReceiver<SimpleBatchingReceiver, Info>::Client {
 public:
-    static void onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) {
-        EXPECT_TRUE(info->index++ >= SIMPLE_BATCHING_LOOPS);
+    void onBitmapOp(Info& info, const BitmapOp& op, const BakedOpState& state) override {
+        EXPECT_TRUE(info.index++ >= SIMPLE_BATCHING_LOOPS);
     }
-    static void onRectOp(Info* info, const RectOp& op, const BakedOpState& state) {
-        EXPECT_TRUE(info->index++ < SIMPLE_BATCHING_LOOPS);
+    void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override {
+        EXPECT_TRUE(info.index++ < SIMPLE_BATCHING_LOOPS);
     }
-    UNSUPPORTED_OP(Info, RenderNodeOp)
-    UNSUPPORTED_OP(Info, SimpleRectsOp)
-    static void startFrame(Info& info) {}
-    static void endFrame(Info& info) {}
 };
 TEST(OpReorderer, simpleBatching) {
     auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
@@ -96,15 +150,14 @@
     reorderer.defer(200, 200, *dl);
 
     Info info;
-    reorderer.replayBakedOps<SimpleBatchingReceiver>(&info);
+    reorderer.replayBakedOps<TestReceiver<SimpleBatchingReceiver, Info>>(info);
     EXPECT_EQ(2 * SIMPLE_BATCHING_LOOPS, info.index); // 2 x loops ops, because no merging (TODO: force no merging)
 }
 
-class RenderNodeReceiver {
+class RenderNodeReceiver : public TestReceiver<RenderNodeReceiver, Info>::Client {
 public:
-    UNSUPPORTED_OP(Info, BitmapOp)
-    static void onRectOp(Info* info, const RectOp& op, const BakedOpState& state) {
-        switch(info->index++) {
+    void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override {
+        switch(info.index++) {
         case 0:
             EXPECT_EQ(Rect(0, 0, 200, 200), state.computedState.clippedBounds);
             EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor());
@@ -117,10 +170,6 @@
             FAIL();
         }
     }
-    UNSUPPORTED_OP(Info, RenderNodeOp)
-    UNSUPPORTED_OP(Info, SimpleRectsOp)
-    static void startFrame(Info& info) {}
-    static void endFrame(Info& info) {}
 };
 TEST(OpReorderer, renderNode) {
     sp<RenderNode> child = TestUtils::createNode<RecordingCanvas>(10, 10, 110, 110, [](RecordingCanvas& canvas) {
@@ -151,22 +200,17 @@
     reorderer.defer(SkRect::MakeWH(200, 200), 200, 200, nodes);
 
     Info info;
-    reorderer.replayBakedOps<RenderNodeReceiver>(&info);
+    reorderer.replayBakedOps<TestReceiver<RenderNodeReceiver, Info>>(info);
 }
 
-class ClippedReceiver {
+class ClippedReceiver : public TestReceiver<ClippedReceiver, Info>::Client {
 public:
-    static void onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) {
-        EXPECT_EQ(0, info->index++);
+    void onBitmapOp(Info& info, const BitmapOp& op, const BakedOpState& state) override {
+        EXPECT_EQ(0, info.index++);
         EXPECT_EQ(Rect(10, 20, 30, 40), state.computedState.clippedBounds);
         EXPECT_EQ(Rect(10, 20, 30, 40), state.computedState.clipRect);
         EXPECT_TRUE(state.computedState.transform.isIdentity());
     }
-    UNSUPPORTED_OP(Info, RectOp)
-    UNSUPPORTED_OP(Info, RenderNodeOp)
-    UNSUPPORTED_OP(Info, SimpleRectsOp)
-    static void startFrame(Info& info) {}
-    static void endFrame(Info& info) {}
 };
 TEST(OpReorderer, clipped) {
     sp<RenderNode> node = TestUtils::createNode<RecordingCanvas>(0, 0, 200, 200, [](RecordingCanvas& canvas) {
@@ -182,8 +226,106 @@
             200, 200, nodes);
 
     Info info;
-    reorderer.replayBakedOps<ClippedReceiver>(&info);
+    reorderer.replayBakedOps<TestReceiver<ClippedReceiver, Info>>(info);
 }
 
+
+class SaveLayerSimpleReceiver : public TestReceiver<SaveLayerSimpleReceiver, Info>::Client {
+public:
+    void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override {
+        EXPECT_EQ(0, info.index++);
+        EXPECT_EQ(Rect(10, 10, 190, 190), op.unmappedBounds);
+        EXPECT_EQ(Rect(0, 0, 180, 180), state.computedState.clippedBounds);
+        EXPECT_EQ(Rect(0, 0, 180, 180), state.computedState.clipRect);
+
+        Matrix4 expectedTransform;
+        expectedTransform.loadTranslate(-10, -10, 0);
+        EXPECT_MATRIX_APPROX_EQ(expectedTransform, state.computedState.transform);
+    }
+    void onLayerOp(Info& info, const LayerOp& op, const BakedOpState& state) override {
+        EXPECT_EQ(1, info.index++);
+        EXPECT_EQ(Rect(10, 10, 190, 190), state.computedState.clippedBounds);
+        EXPECT_EQ(Rect(0, 0, 200, 200), state.computedState.clipRect);
+        EXPECT_TRUE(state.computedState.transform.isIdentity());
+    }
+};
+TEST(OpReorderer, saveLayerSimple) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        canvas.saveLayerAlpha(10, 10, 190, 190, 128, SkCanvas::kClipToLayer_SaveFlag);
+        canvas.drawRect(10, 10, 190, 190, SkPaint());
+        canvas.restore();
+    });
+
+    OpReorderer reorderer;
+    reorderer.defer(200, 200, *dl);
+
+    Info info;
+    reorderer.replayBakedOps<TestReceiver<SaveLayerSimpleReceiver, Info>>(info);
+    EXPECT_EQ(2, info.index);
 }
+
+
+// saveLayer1 {rect1, saveLayer2 { rect2 } } will play back as rect2, rect1, layerOp2, layerOp1
+class SaveLayerNestedReceiver : public TestReceiver<SaveLayerNestedReceiver, Info>::Client {
+public:
+    void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override {
+        const int index = info.index++;
+        if (index == 0) {
+            EXPECT_EQ(Rect(0, 0, 400, 400), op.unmappedBounds); // inner rect
+        } else if (index == 1) {
+            EXPECT_EQ(Rect(0, 0, 800, 800), op.unmappedBounds); // outer rect
+        } else { FAIL(); }
+    }
+    void onLayerOp(Info& info, const LayerOp& op, const BakedOpState& state) override {
+        const int index = info.index++;
+        if (index == 2) {
+            EXPECT_EQ(Rect(0, 0, 400, 400), op.unmappedBounds); // inner layer
+        } else if (index == 3) {
+            EXPECT_EQ(Rect(0, 0, 800, 800), op.unmappedBounds); // outer layer
+        } else { FAIL(); }
+    }
+};
+TEST(OpReorderer, saveLayerNested) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(800, 800, [](RecordingCanvas& canvas) {
+        canvas.saveLayerAlpha(0, 0, 800, 800, 128, SkCanvas::kClipToLayer_SaveFlag);
+        {
+            canvas.drawRect(0, 0, 800, 800, SkPaint());
+            canvas.saveLayerAlpha(0, 0, 400, 400, 128, SkCanvas::kClipToLayer_SaveFlag);
+            {
+                canvas.drawRect(0, 0, 400, 400, SkPaint());
+            }
+            canvas.restore();
+        }
+        canvas.restore();
+    });
+
+    OpReorderer reorderer;
+    reorderer.defer(800, 800, *dl);
+
+    Info info;
+    reorderer.replayBakedOps<TestReceiver<SaveLayerNestedReceiver, Info>>(info);
+    EXPECT_EQ(4, info.index);
 }
+
+TEST(OpReorderer, saveLayerContentRejection) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](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);
+
+        // draw within save layer may still be recorded, but shouldn't be drawn
+        canvas.drawRect(200, 200, 400, 400, SkPaint());
+
+        canvas.restore();
+        canvas.restore();
+    });
+    OpReorderer reorderer;
+    reorderer.defer(200, 200, *dl);
+    Info info;
+
+    // should see no ops, even within the layer, since the layer should be rejected
+    reorderer.replayBakedOps<TestReceiver<FailReceiver, Info>>(info);
+}
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/unit_tests/RecordingCanvasTests.cpp b/libs/hwui/unit_tests/RecordingCanvasTests.cpp
index ce25fc6..c023123 100644
--- a/libs/hwui/unit_tests/RecordingCanvasTests.cpp
+++ b/libs/hwui/unit_tests/RecordingCanvasTests.cpp
@@ -24,11 +24,11 @@
 namespace uirenderer {
 
 static void playbackOps(const DisplayList& displayList,
-        std::function<void(const RecordedOp&)> opReciever) {
+        std::function<void(const RecordedOp&)> opReceiver) {
     for (const DisplayList::Chunk& chunk : displayList.getChunks()) {
         for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) {
             RecordedOp* op = displayList.getOps()[opIndex];
-            opReciever(*op);
+            opReceiver(*op);
         }
     }
 }
@@ -109,5 +109,123 @@
     ASSERT_EQ(2, count); // two draws observed
 }
 
+TEST(RecordingCanvas, saveLayerSimple) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        canvas.saveLayerAlpha(10, 20, 190, 180, 128, SkCanvas::kARGB_ClipLayer_SaveFlag);
+        canvas.drawRect(10, 20, 190, 180, SkPaint());
+        canvas.restore();
+    });
+    int count = 0;
+    playbackOps(*dl, [&count](const RecordedOp& op) {
+        Matrix4 expectedMatrix;
+        switch(count++) {
+        case 0:
+            EXPECT_EQ(RecordedOpId::BeginLayerOp, op.opId);
+            // TODO: add asserts
+            break;
+        case 1:
+            EXPECT_EQ(RecordedOpId::RectOp, op.opId);
+            EXPECT_EQ(Rect(0, 0, 180, 160), op.localClipRect);
+            EXPECT_EQ(Rect(10, 20, 190, 180), op.unmappedBounds);
+            expectedMatrix.loadTranslate(-10, -20, 0);
+            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix);
+            break;
+        case 2:
+            EXPECT_EQ(RecordedOpId::EndLayerOp, op.opId);
+            // TODO: add asserts
+            break;
+        default:
+            FAIL();
+        }
+    });
+    EXPECT_EQ(3, count);
 }
+
+TEST(RecordingCanvas, saveLayerViewportCrop) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        // shouldn't matter, since saveLayer will clip to its bounds
+        canvas.clipRect(-1000, -1000, 1000, 1000, SkRegion::kReplace_Op);
+
+        canvas.saveLayerAlpha(100, 100, 300, 300, 128, SkCanvas::kARGB_ClipLayer_SaveFlag);
+        canvas.drawRect(0, 0, 400, 400, SkPaint());
+        canvas.restore();
+    });
+    int count = 0;
+    playbackOps(*dl, [&count](const RecordedOp& op) {
+        if (count++ == 1) {
+            Matrix4 expectedMatrix;
+            EXPECT_EQ(RecordedOpId::RectOp, op.opId);
+
+            // recorded clip rect should be intersection of
+            // viewport and saveLayer bounds, in layer space
+            EXPECT_EQ(Rect(0, 0, 100, 100), op.localClipRect);
+            EXPECT_EQ(Rect(0, 0, 400, 400), op.unmappedBounds);
+            expectedMatrix.loadTranslate(-100, -100, 0);
+            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix);
+        }
+    });
+    EXPECT_EQ(3, count);
 }
+
+TEST(RecordingCanvas, saveLayerRotateUnclipped) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        canvas.translate(100, 100);
+        canvas.rotate(45);
+        canvas.translate(-50, -50);
+
+        canvas.saveLayerAlpha(0, 0, 100, 100, 128, SkCanvas::kARGB_ClipLayer_SaveFlag);
+        canvas.drawRect(0, 0, 100, 100, SkPaint());
+        canvas.restore();
+
+        canvas.restore();
+    });
+    int count = 0;
+    playbackOps(*dl, [&count](const RecordedOp& op) {
+        if (count++ == 1) {
+            Matrix4 expectedMatrix;
+            EXPECT_EQ(RecordedOpId::RectOp, op.opId);
+
+            // recorded rect doesn't see rotate, since recorded relative to saveLayer bounds
+            EXPECT_EQ(Rect(0, 0, 100, 100), op.localClipRect);
+            EXPECT_EQ(Rect(0, 0, 100, 100), op.unmappedBounds);
+            expectedMatrix.loadIdentity();
+            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix);
+        }
+    });
+    EXPECT_EQ(3, count);
+}
+
+TEST(RecordingCanvas, saveLayerRotateClipped) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        canvas.translate(100, 100);
+        canvas.rotate(45);
+        canvas.translate(-200, -200);
+
+        // area of saveLayer will be clipped to parent viewport, so we ask for 400x400...
+        canvas.saveLayerAlpha(0, 0, 400, 400, 128, SkCanvas::kARGB_ClipLayer_SaveFlag);
+        canvas.drawRect(0, 0, 400, 400, SkPaint());
+        canvas.restore();
+
+        canvas.restore();
+    });
+    int count = 0;
+    playbackOps(*dl, [&count](const RecordedOp& op) {
+        if (count++ == 1) {
+            Matrix4 expectedMatrix;
+            EXPECT_EQ(RecordedOpId::RectOp, op.opId);
+
+            // ...and get about 58.6, 58.6, 341.4 341.4, because the bounds are clipped by
+            // the parent 200x200 viewport, but prior to rotation
+            EXPECT_RECT_APPROX_EQ(Rect(58.57864, 58.57864, 341.42136, 341.42136), op.localClipRect);
+            EXPECT_EQ(Rect(0, 0, 400, 400), op.unmappedBounds);
+            expectedMatrix.loadIdentity();
+            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix);
+        }
+    });
+    EXPECT_EQ(3, count);
+}
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/unit_tests/TestUtils.h b/libs/hwui/unit_tests/TestUtils.h
index 80d83a2..99ecc9b 100644
--- a/libs/hwui/unit_tests/TestUtils.h
+++ b/libs/hwui/unit_tests/TestUtils.h
@@ -31,6 +31,12 @@
 #define EXPECT_MATRIX_APPROX_EQ(a, b) \
     EXPECT_TRUE(TestUtils::matricesAreApproxEqual(a, b))
 
+#define EXPECT_RECT_APPROX_EQ(a, b) \
+    EXPECT_TRUE(MathUtils::areEqual(a.left, b.left) \
+            && MathUtils::areEqual(a.top, b.top) \
+            && MathUtils::areEqual(a.right, b.right) \
+            && MathUtils::areEqual(a.bottom, b.bottom));
+
 class TestUtils {
 public:
     static bool matricesAreApproxEqual(const Matrix4& a, const Matrix4& b) {