Partial unclipped save layer support

Not yet implemented in renderer.

Change-Id: I491ec6e7886bfa313d1db71dd5981690d45b78a9
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp
index 097675a..f58ca31 100644
--- a/libs/hwui/BakedOpDispatcher.cpp
+++ b/libs/hwui/BakedOpDispatcher.cpp
@@ -753,5 +753,13 @@
     }
 }
 
+void BakedOpDispatcher::onCopyToLayerOp(BakedOpRenderer& renderer, const CopyToLayerOp& op, const BakedOpState& state) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+
+void BakedOpDispatcher::onCopyFromLayerOp(BakedOpRenderer& renderer, const CopyFromLayerOp& op, const BakedOpState& state) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+
 } // namespace uirenderer
 } // namespace android
diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp
index a0d5fae..4aebe3c 100644
--- a/libs/hwui/BakedOpRenderer.cpp
+++ b/libs/hwui/BakedOpRenderer.cpp
@@ -236,12 +236,13 @@
 }
 
 void BakedOpRenderer::prepareRender(const Rect* dirtyBounds, const ClipBase* clip) {
-    // prepare scissor / stencil
+    // Prepare scissor (done before stencil, to simplify filling stencil)
     mRenderState.scissor().setEnabled(clip != nullptr);
     if (clip) {
         mRenderState.scissor().set(mRenderTarget.viewportHeight, clip->rect);
     }
 
+    // If stencil may be used for clipping, enable it, fill it, or disable it as appropriate
     if (CC_LIKELY(!Properties::debugOverdraw)) {
         // only modify stencil mode and content when it's not used for overdraw visualization
         if (CC_UNLIKELY(clip && clip->mode != ClipMode::Rectangle)) {
diff --git a/libs/hwui/BakedOpState.cpp b/libs/hwui/BakedOpState.cpp
index e6b943a..b7c9281 100644
--- a/libs/hwui/BakedOpState.cpp
+++ b/libs/hwui/BakedOpState.cpp
@@ -75,5 +75,11 @@
     clipSideFlags = OpClipSideFlags::Full;
 }
 
+ResolvedRenderState::ResolvedRenderState(const Rect& dstRect)
+        : transform(Matrix4::identity())
+        , clipState(nullptr)
+        , clippedBounds(dstRect)
+        , clipSideFlags(OpClipSideFlags::None) {}
+
 } // namespace uirenderer
 } // namespace android
diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h
index 9df4e3a..70b0484 100644
--- a/libs/hwui/BakedOpState.h
+++ b/libs/hwui/BakedOpState.h
@@ -58,6 +58,10 @@
     // Constructor for unbounded ops without transform/clip (namely shadows)
     ResolvedRenderState(LinearAllocator& allocator, Snapshot& snapshot);
 
+    // Constructor for primitive ops without clip or transform
+    // NOTE: these ops can't be queried for RT clip / local clip
+    ResolvedRenderState(const Rect& dstRect);
+
     Rect computeLocalSpaceClip() const {
         Matrix4 inverse;
         inverse.loadInverse(transform);
@@ -67,10 +71,12 @@
         return outClip;
     }
 
-    Matrix4 transform;
+    // NOTE: Can only be used on clipped/snapshot based ops
     const Rect& clipRect() const {
         return clipState->rect;
     }
+
+    // NOTE: Can only be used on clipped/snapshot based ops
     bool requiresClip() const {
         return clipSideFlags != OpClipSideFlags::None
                 || CC_UNLIKELY(clipState->mode != ClipMode::Rectangle);
@@ -80,9 +86,11 @@
     const ClipBase* getClipIfNeeded() const {
         return requiresClip() ? clipState : nullptr;
     }
+
+    Matrix4 transform;
     const ClipBase* clipState = nullptr;
-    int clipSideFlags = 0;
     Rect clippedBounds;
+    int clipSideFlags = 0;
 };
 
 /**
@@ -135,12 +143,17 @@
         return new (allocator) BakedOpState(allocator, snapshot, shadowOpPtr);
     }
 
+    static BakedOpState* directConstruct(LinearAllocator& allocator,
+            const Rect& dstRect, const RecordedOp& recordedOp) {
+        return new (allocator) BakedOpState(dstRect, recordedOp);
+    }
+
     static void* operator new(size_t size, LinearAllocator& allocator) {
         return allocator.alloc(size);
     }
 
     // computed state:
-    const ResolvedRenderState computedState;
+    ResolvedRenderState computedState;
 
     // simple state (straight pointer/value storage):
     const float alpha;
@@ -163,6 +176,13 @@
             , roundRectClipState(snapshot.roundRectClipState)
             , projectionPathMask(snapshot.projectionPathMask)
             , op(shadowOpPtr) {}
+
+    BakedOpState(const Rect& dstRect, const RecordedOp& recordedOp)
+            : computedState(dstRect)
+            , alpha(1.0f)
+            , roundRectClipState(nullptr)
+            , projectionPathMask(nullptr)
+            , op(&recordedOp) {}
 };
 
 }; // namespace uirenderer
diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp
index 3f492d5..34c3d60 100644
--- a/libs/hwui/OpReorderer.cpp
+++ b/libs/hwui/OpReorderer.cpp
@@ -240,8 +240,50 @@
     }
 }
 
+void OpReorderer::LayerReorderer::deferLayerClear(const Rect& rect) {
+    mClearRects.push_back(rect);
+}
+
+void OpReorderer::LayerReorderer::flushLayerClears(LinearAllocator& allocator) {
+    if (CC_UNLIKELY(!mClearRects.empty())) {
+        const int vertCount = mClearRects.size() * 4;
+        // put the verts in the frame allocator, since
+        //     1) SimpleRectsOps needs verts, not rects
+        //     2) even if mClearRects stored verts, std::vectors will move their contents
+        Vertex* const verts = (Vertex*) allocator.alloc(vertCount * sizeof(Vertex));
+
+        Vertex* currentVert = verts;
+        Rect bounds = mClearRects[0];
+        for (auto&& rect : mClearRects) {
+            bounds.unionWith(rect);
+            Vertex::set(currentVert++, rect.left, rect.top);
+            Vertex::set(currentVert++, rect.right, rect.top);
+            Vertex::set(currentVert++, rect.left, rect.bottom);
+            Vertex::set(currentVert++, rect.right, rect.bottom);
+        }
+        mClearRects.clear(); // discard rects before drawing so this method isn't reentrant
+
+        // One or more unclipped saveLayers have been enqueued, with deferred clears.
+        // Flush all of these clears with a single draw
+        SkPaint* paint = allocator.create<SkPaint>();
+        paint->setXfermodeMode(SkXfermode::kClear_Mode);
+        SimpleRectsOp* op = new (allocator) SimpleRectsOp(bounds,
+                Matrix4::identity(), nullptr, paint,
+                verts, vertCount);
+        BakedOpState* bakedState = BakedOpState::directConstruct(allocator, bounds, *op);
+
+
+        deferUnmergeableOp(allocator, bakedState, OpBatchType::Vertices);
+    }
+}
+
 void OpReorderer::LayerReorderer::deferUnmergeableOp(LinearAllocator& allocator,
         BakedOpState* op, batchid_t batchId) {
+    if (batchId != OpBatchType::CopyToLayer) {
+        // if first op after one or more unclipped saveLayers, flush the layer clears
+        flushLayerClears(allocator);
+    }
+
     OpBatch* targetBatch = mBatchLookup[batchId];
 
     size_t insertBatchIndex = mBatches.size();
@@ -260,10 +302,12 @@
     }
 }
 
-// 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) {
+    if (batchId != OpBatchType::CopyToLayer) {
+        // if first op after one or more unclipped saveLayers, flush the layer clears
+        flushLayerClears(allocator);
+    }
     MergingOpBatch* targetBatch = nullptr;
 
     // Try to merge with any existing batch with same mergeId
@@ -726,6 +770,11 @@
     deferStrokeableOp(op, tessBatchId(op));
 }
 
+static bool hasMergeableClip(const BakedOpState& state) {
+    return state.computedState.clipState
+            || state.computedState.clipState->mode == ClipMode::Rectangle;
+}
+
 void OpReorderer::deferBitmapOp(const BitmapOp& op) {
     BakedOpState* bakedState = tryBakeOpState(op);
     if (!bakedState) return; // quick rejected
@@ -736,7 +785,8 @@
     if (bakedState->computedState.transform.isSimple()
             && bakedState->computedState.transform.positiveScale()
             && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode
-            && op.bitmap->colorType() != kAlpha_8_SkColorType) {
+            && op.bitmap->colorType() != kAlpha_8_SkColorType
+            && hasMergeableClip(*bakedState)) {
         mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID();
         // TODO: AssetAtlas in mergeId
         currentLayer().deferMergeableOp(mAllocator, bakedState, OpBatchType::Bitmap, mergeId);
@@ -792,7 +842,8 @@
     if (!bakedState) return; // quick rejected
 
     if (bakedState->computedState.transform.isPureTranslate()
-            && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode) {
+            && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode
+            && hasMergeableClip(*bakedState)) {
         mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID();
         // TODO: AssetAtlas in mergeId
 
@@ -849,7 +900,8 @@
 
     batchid_t batchId = textBatchId(*(op.paint));
     if (bakedState->computedState.transform.isPureTranslate()
-            && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode) {
+            && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode
+            && hasMergeableClip(*bakedState)) {
         mergeid_t mergeId = reinterpret_cast<mergeid_t>(op.paint->getColor());
         currentLayer().deferMergeableOp(mAllocator, bakedState, batchId, mergeId);
     } else {
@@ -894,7 +946,8 @@
     mLayerStack.pop_back();
 }
 
-// TODO: test rejection at defer time, where the bounds become empty
+// TODO: defer time rejection (when bounds become empty) + tests
+// Option - just skip layers with no bounds at playback + defer?
 void OpReorderer::deferBeginLayerOp(const BeginLayerOp& op) {
     uint32_t layerWidth = (uint32_t) op.unmappedBounds.getWidth();
     uint32_t layerHeight = (uint32_t) op.unmappedBounds.getHeight();
@@ -904,7 +957,7 @@
 
     // Combine all transforms used to present saveLayer content:
     // parent content transform * canvas transform * bounds offset
-    Matrix4 contentTransform(*previous->transform);
+    Matrix4 contentTransform(*(previous->transform));
     contentTransform.multiply(op.localMatrix);
     contentTransform.translate(op.unmappedBounds.left, op.unmappedBounds.top);
 
@@ -961,10 +1014,53 @@
         currentLayer().deferUnmergeableOp(mAllocator, bakedOpState, OpBatchType::Bitmap);
     } else {
         // Layer won't be drawn - delete its drawing batches to prevent it from doing any work
+        // TODO: need to prevent any render work from being done
+        // - create layerop earlier for reject purposes?
         mLayerReorderers[finishedLayerIndex].clear();
         return;
     }
 }
 
+void OpReorderer::deferBeginUnclippedLayerOp(const BeginUnclippedLayerOp& op) {
+    Matrix4 boundsTransform(*(mCanvasState.currentSnapshot()->transform));
+    boundsTransform.multiply(op.localMatrix);
+
+    Rect dstRect(op.unmappedBounds);
+    boundsTransform.mapRect(dstRect);
+    dstRect.doIntersect(mCanvasState.currentSnapshot()->getRenderTargetClip());
+
+    // Allocate a holding position for the layer object (copyTo will produce, copyFrom will consume)
+    OffscreenBuffer** layerHandle = mAllocator.create<OffscreenBuffer*>();
+
+    /**
+     * First, defer an operation to copy out the content from the rendertarget into a layer.
+     */
+    auto copyToOp = new (mAllocator) CopyToLayerOp(op, layerHandle);
+    BakedOpState* bakedState = BakedOpState::directConstruct(mAllocator, dstRect, *copyToOp);
+    currentLayer().deferUnmergeableOp(mAllocator, bakedState, OpBatchType::CopyToLayer);
+
+    /**
+     * Defer a clear rect, so that clears from multiple unclipped layers can be drawn
+     * both 1) simultaneously, and 2) as long after the copyToLayer executes as possible
+     */
+    currentLayer().deferLayerClear(dstRect);
+
+    /**
+     * And stash an operation to copy that layer back under the rendertarget until
+     * a balanced EndUnclippedLayerOp is seen
+     */
+    auto copyFromOp = new (mAllocator) CopyFromLayerOp(op, layerHandle);
+    bakedState = BakedOpState::directConstruct(mAllocator, dstRect, *copyFromOp);
+    currentLayer().activeUnclippedSaveLayers.push_back(bakedState);
+}
+
+void OpReorderer::deferEndUnclippedLayerOp(const EndUnclippedLayerOp& op) {
+    LOG_ALWAYS_FATAL_IF(currentLayer().activeUnclippedSaveLayers.empty(), "no layer to end!");
+
+    BakedOpState* copyFromLayerOp = currentLayer().activeUnclippedSaveLayers.back();
+    currentLayer().deferUnmergeableOp(mAllocator, copyFromLayerOp, OpBatchType::CopyFromLayer);
+    currentLayer().activeUnclippedSaveLayers.pop_back();
+}
+
 } // namespace uirenderer
 } // namespace android
diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h
index 429913f..b824d02 100644
--- a/libs/hwui/OpReorderer.h
+++ b/libs/hwui/OpReorderer.h
@@ -53,6 +53,8 @@
         Shadow,
         TextureLayer,
         Functor,
+        CopyToLayer,
+        CopyFromLayer,
 
         Count // must be last
     };
@@ -91,6 +93,8 @@
 
         void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers, MergedOpReceiver*) const;
 
+        void deferLayerClear(const Rect& dstRect);
+
         bool empty() const {
             return mBatches.empty();
         }
@@ -107,7 +111,12 @@
         OffscreenBuffer* offscreenBuffer;
         const BeginLayerOp* beginLayerOp;
         const RenderNode* renderNode;
+
+        // list of deferred CopyFromLayer ops, to be deferred upon encountering EndUnclippedLayerOps
+        std::vector<BakedOpState*> activeUnclippedSaveLayers;
     private:
+        void flushLayerClears(LinearAllocator& allocator);
+
         std::vector<BatchBase*> mBatches;
 
         /**
@@ -119,6 +128,8 @@
 
         // Maps batch ids to the most recent *non-merging* batch of that id
         OpBatch* mBatchLookup[OpBatchType::Count] = { nullptr };
+
+        std::vector<Rect> mClearRects;
     };
 
 public:
@@ -147,7 +158,8 @@
          */
         #define X(Type) \
                 [](void* renderer, const BakedOpState& state) { \
-                    StaticDispatcher::on##Type(*(static_cast<Renderer*>(renderer)), static_cast<const Type&>(*(state.op)), state); \
+                    StaticDispatcher::on##Type(*(static_cast<Renderer*>(renderer)), \
+                            static_cast<const Type&>(*(state.op)), state); \
                 },
         static BakedOpReceiver unmergedReceivers[] = BUILD_RENDERABLE_OP_LUT(X);
         #undef X
@@ -233,8 +245,7 @@
     void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers);
 
     SkPath* createFrameAllocatedPath() {
-        mFrameAllocatedPaths.emplace_back(new SkPath);
-        return mFrameAllocatedPaths.back().get();
+        return mAllocator.create<SkPath>();
     }
 
     void deferStrokeableOp(const RecordedOp& op, batchid_t batchId,
@@ -250,8 +261,6 @@
     MAP_DEFERRABLE_OPS(X)
 #undef X
 
-    std::vector<std::unique_ptr<SkPath> > mFrameAllocatedPaths;
-
     // List of every deferred layer's render state. Replayed in reverse order to render a frame.
     std::vector<LayerReorderer> mLayerReorderers;
 
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
index b243f99..30d5c29 100644
--- a/libs/hwui/RecordedOp.h
+++ b/libs/hwui/RecordedOp.h
@@ -43,12 +43,28 @@
  * the functions to which they dispatch. Parameter macros are executed for each op,
  * in order, based on the op's type.
  *
- * There are 4 types of op:
+ * There are 4 types of op, which defines dispatch/LUT capability:
  *
- * Pre render - not directly consumed by renderer, reorder stage resolves this into renderable type
- * Render only - generated renderable ops - never passed to a reorderer
- * Unmergeable - reorderable, renderable (but not mergeable)
- * Mergeable - reorderable, renderable (and mergeable)
+ *              | DisplayList |   Render    |    Merge    |
+ * -------------|-------------|-------------|-------------|
+ * PRE RENDER   |     Yes     |             |             |
+ * RENDER ONLY  |             |     Yes     |             |
+ * UNMERGEABLE  |     Yes     |     Yes     |             |
+ * MERGEABLE    |     Yes     |     Yes     |     Yes     |
+ *
+ * PRE RENDER - These ops are recorded into DisplayLists, but can't be directly rendered. This
+ *      may be because they need to be transformed into other op types (e.g. CirclePropsOp),
+ *      be traversed to access multiple renderable ops within (e.g. RenderNodeOp), or because they
+ *      modify renderbuffer lifecycle, instead of directly rendering content (the various LayerOps).
+ *
+ * RENDER ONLY - These ops cannot be recorded into DisplayLists, and are instead implicitly
+ *      constructed from other commands/RenderNode properties. They cannot be merged.
+ *
+ * UNMERGEABLE - These ops can be recorded into DisplayLists and rendered directly, but do not
+ *      support merged rendering.
+ *
+ * MERGEABLE - These ops can be recorded into DisplayLists and rendered individually, or merged
+ *      under certain circumstances.
  */
 #define MAP_OPS_BASED_ON_TYPE(PRE_RENDER_OP_FN, RENDER_ONLY_OP_FN, UNMERGEABLE_OP_FN, MERGEABLE_OP_FN) \
         PRE_RENDER_OP_FN(RenderNodeOp) \
@@ -56,9 +72,13 @@
         PRE_RENDER_OP_FN(RoundRectPropsOp) \
         PRE_RENDER_OP_FN(BeginLayerOp) \
         PRE_RENDER_OP_FN(EndLayerOp) \
+        PRE_RENDER_OP_FN(BeginUnclippedLayerOp) \
+        PRE_RENDER_OP_FN(EndUnclippedLayerOp) \
         \
         RENDER_ONLY_OP_FN(ShadowOp) \
         RENDER_ONLY_OP_FN(LayerOp) \
+        RENDER_ONLY_OP_FN(CopyToLayerOp) \
+        RENDER_ONLY_OP_FN(CopyFromLayerOp) \
         \
         UNMERGEABLE_OP_FN(ArcOp) \
         UNMERGEABLE_OP_FN(BitmapMeshOp) \
@@ -99,15 +119,15 @@
  */
 #define NULL_OP_FN(Type)
 
+#define MAP_DEFERRABLE_OPS(OP_FN) \
+        MAP_OPS_BASED_ON_TYPE(OP_FN, NULL_OP_FN, OP_FN, OP_FN)
+
 #define MAP_MERGEABLE_OPS(OP_FN) \
         MAP_OPS_BASED_ON_TYPE(NULL_OP_FN, NULL_OP_FN, NULL_OP_FN, OP_FN)
 
 #define MAP_RENDERABLE_OPS(OP_FN) \
         MAP_OPS_BASED_ON_TYPE(NULL_OP_FN, OP_FN, OP_FN, OP_FN)
 
-#define MAP_DEFERRABLE_OPS(OP_FN) \
-        MAP_OPS_BASED_ON_TYPE(OP_FN, NULL_OP_FN, OP_FN, OP_FN)
-
 // Generate OpId enum
 #define IDENTITY_FN(Type) Type,
 namespace RecordedOpId {
@@ -407,6 +427,46 @@
             : RecordedOp(RecordedOpId::EndLayerOp, Rect(), Matrix4::identity(), nullptr, nullptr) {}
 };
 
+struct BeginUnclippedLayerOp : RecordedOp {
+    BeginUnclippedLayerOp(BASE_PARAMS)
+            : SUPER(BeginUnclippedLayerOp) {}
+};
+
+struct EndUnclippedLayerOp : RecordedOp {
+    EndUnclippedLayerOp()
+            : RecordedOp(RecordedOpId::EndUnclippedLayerOp, Rect(), Matrix4::identity(), nullptr, nullptr) {}
+};
+
+struct CopyToLayerOp : RecordedOp {
+    CopyToLayerOp(const RecordedOp& op, OffscreenBuffer** layerHandle)
+            : RecordedOp(RecordedOpId::CopyToLayerOp,
+                    op.unmappedBounds,
+                    op.localMatrix,
+                    nullptr, // clip intentionally ignored
+                    op.paint)
+            , layerHandle(layerHandle) {}
+
+    // Records a handle to the Layer object, since the Layer itself won't be
+    // constructed until after this operation is constructed.
+    OffscreenBuffer** layerHandle;
+};
+
+
+// draw the parameter layer underneath
+struct CopyFromLayerOp : RecordedOp {
+    CopyFromLayerOp(const RecordedOp& op, OffscreenBuffer** layerHandle)
+            : RecordedOp(RecordedOpId::CopyFromLayerOp,
+                    op.unmappedBounds,
+                    op.localMatrix,
+                    nullptr, // clip intentionally ignored
+                    op.paint)
+            , layerHandle(layerHandle) {}
+
+    // Records a handle to the Layer object, since the Layer itself won't be
+    // constructed until after this operation is constructed.
+    OffscreenBuffer** layerHandle;
+};
+
 /**
  * Draws an OffscreenBuffer.
  *
@@ -424,12 +484,12 @@
             , destroy(true) {}
 
     LayerOp(RenderNode& node)
-        : RecordedOp(RecordedOpId::LayerOp, Rect(node.getWidth(), node.getHeight()), Matrix4::identity(), nullptr, nullptr)
-        , layerHandle(node.getLayerHandle())
-        , alpha(node.properties().layerProperties().alpha() / 255.0f)
-        , mode(node.properties().layerProperties().xferMode())
-        , colorFilter(node.properties().layerProperties().colorFilter())
-        , destroy(false) {}
+            : RecordedOp(RecordedOpId::LayerOp, Rect(node.getWidth(), node.getHeight()), Matrix4::identity(), nullptr, nullptr)
+            , layerHandle(node.getLayerHandle())
+            , alpha(node.properties().layerProperties().alpha() / 255.0f)
+            , mode(node.properties().layerProperties().xferMode())
+            , colorFilter(node.properties().layerProperties().colorFilter())
+            , destroy(false) {}
 
     // Records a handle to the Layer object, since the Layer itself won't be
     // constructed until after this operation is constructed.
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index f7f6caf..78855e5 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -43,10 +43,10 @@
 
     mDeferredBarrierType = DeferredBarrierType::InOrder;
     mState.setDirtyClip(false);
-    mRestoreSaveCount = -1;
 }
 
 DisplayList* RecordingCanvas::finishRecording() {
+    restoreToCount(1);
     mPaintMap.clear();
     mRegionMap.clear();
     mPathMap.clear();
@@ -83,6 +83,8 @@
 void RecordingCanvas::onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {
     if (removed.flags & Snapshot::kFlagIsFboLayer) {
         addOp(new (alloc()) EndLayerOp());
+    } else if (removed.flags & Snapshot::kFlagIsLayer) {
+        addOp(new (alloc()) EndUnclippedLayerOp());
     }
 }
 
@@ -95,28 +97,18 @@
 }
 
 void RecordingCanvas::RecordingCanvas::restore() {
-    if (mRestoreSaveCount < 0) {
-        restoreToCount(getSaveCount() - 1);
-        return;
-    }
-
-    mRestoreSaveCount--;
     mState.restore();
 }
 
 void RecordingCanvas::restoreToCount(int saveCount) {
-    mRestoreSaveCount = saveCount;
     mState.restoreToCount(saveCount);
 }
 
-int RecordingCanvas::saveLayer(float left, float top, float right, float bottom, const SkPaint* paint,
-        SkCanvas::SaveFlags flags) {
-    if (!(flags & SkCanvas::kClipToLayer_SaveFlag)) {
-        LOG_ALWAYS_FATAL("unclipped layers not supported");
-    }
+int RecordingCanvas::saveLayer(float left, float top, float right, float bottom,
+        const SkPaint* paint, SkCanvas::SaveFlags flags) {
     // force matrix/clip isolation for layer
     flags |= SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag;
-
+    bool clippedLayer = flags & SkCanvas::kClipToLayer_SaveFlag;
 
     const Snapshot& previous = *mState.currentSnapshot();
 
@@ -124,53 +116,70 @@
     // 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);
+    const Rect unmappedBounds(left, top, right, bottom);
 
     // determine clipped bounds relative to previous viewport.
-    Rect visibleBounds = untransformedBounds;
+    Rect visibleBounds = unmappedBounds;
     previous.transform->mapRect(visibleBounds);
 
+    if (CC_UNLIKELY(!clippedLayer
+            && previous.transform->rectToRect()
+            && visibleBounds.contains(previous.getRenderTargetClip()))) {
+        // unlikely case where an unclipped savelayer is recorded with a clip it can use,
+        // as none of its unaffected/unclipped area is visible
+        clippedLayer = true;
+        flags |= SkCanvas::kClipToLayer_SaveFlag;
+    }
 
     visibleBounds.doIntersect(previous.getRenderTargetClip());
     visibleBounds.snapToPixelBoundaries();
-
-    Rect previousViewport(0, 0, previous.getViewportWidth(), previous.getViewportHeight());
-    visibleBounds.doIntersect(previousViewport);
+    visibleBounds.doIntersect(Rect(previous.getViewportWidth(), previous.getViewportHeight()));
 
     // 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);
+    layerBounds.doIntersect(unmappedBounds);
 
     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()) {
+    // layerBounds is in original bounds space, but clipped by current recording clip
+    if (layerBounds.isEmpty() || unmappedBounds.isEmpty()) {
         // Don't bother recording layer, since it's been rejected
-        snapshot.resetClip(0, 0, 0, 0);
+        if (CC_LIKELY(clippedLayer)) {
+            snapshot.resetClip(0, 0, 0, 0);
+        }
         return saveValue;
     }
 
-    auto previousClip = getRecordedClip(); // note: done while snapshot == previous
+    if (CC_LIKELY(clippedLayer)) {
+        auto previousClip = getRecordedClip(); // note: done before new snapshot's clip has changed
 
-    snapshot.flags |= Snapshot::kFlagFboTarget | Snapshot::kFlagIsFboLayer;
-    snapshot.initializeViewport(untransformedBounds.getWidth(), untransformedBounds.getHeight());
-    snapshot.transform->loadTranslate(-untransformedBounds.left, -untransformedBounds.top, 0.0f);
+        snapshot.flags |= Snapshot::kFlagIsLayer | Snapshot::kFlagIsFboLayer;
+        snapshot.initializeViewport(unmappedBounds.getWidth(), unmappedBounds.getHeight());
+        snapshot.transform->loadTranslate(-unmappedBounds.left, -unmappedBounds.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;
+        Rect clip = layerBounds;
+        clip.translate(-unmappedBounds.left, -unmappedBounds.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
-            previousClip, // clip to *draw* with
-            refPaint(paint)));
+        addOp(new (alloc()) BeginLayerOp(
+                unmappedBounds,
+                *previous.transform, // transform to *draw* with
+                previousClip, // clip to *draw* with
+                refPaint(paint)));
+    } else {
+        snapshot.flags |= Snapshot::kFlagIsLayer;
+
+        addOp(new (alloc()) BeginUnclippedLayerOp(
+                unmappedBounds,
+                *mState.currentSnapshot()->transform,
+                getRecordedClip(),
+                refPaint(paint)));
+    }
 
     return saveValue;
 }
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index 1a2ac97f..8aa7506 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -314,7 +314,6 @@
     DisplayList* mDisplayList = nullptr;
     bool mHighContrastText = false;
     SkAutoTUnref<SkDrawFilter> mDrawFilter;
-    int mRestoreSaveCount = -1;
 }; // class RecordingCanvas
 
 }; // namespace uirenderer
diff --git a/libs/hwui/Snapshot.h b/libs/hwui/Snapshot.h
index 5fac3a1..dbaa905 100644
--- a/libs/hwui/Snapshot.h
+++ b/libs/hwui/Snapshot.h
@@ -116,7 +116,7 @@
          * Indicates that this snapshot or an ancestor snapshot is
          * an FBO layer.
          */
-        kFlagFboTarget = 0x8,
+        kFlagFboTarget = 0x8, // TODO: remove for HWUI_NEW_OPS
     };
 
     /**
diff --git a/libs/hwui/tests/unit/OpReordererTests.cpp b/libs/hwui/tests/unit/OpReordererTests.cpp
index 66dccb4..0ed70a0 100644
--- a/libs/hwui/tests/unit/OpReordererTests.cpp
+++ b/libs/hwui/tests/unit/OpReordererTests.cpp
@@ -423,7 +423,7 @@
     reorderer.replayBakedOps<TestDispatcher>(renderer);
 }
 
-TEST(OpReorderer, saveLayerSimple) {
+TEST(OpReorderer, saveLayer_simple) {
     class SaveLayerSimpleTestRenderer : public TestRendererBase {
     public:
         OffscreenBuffer* startTemporaryLayer(uint32_t width, uint32_t height) override {
@@ -466,7 +466,7 @@
     EXPECT_EQ(4, renderer.getIndex());
 }
 
-TEST(OpReorderer, saveLayerNested) {
+TEST(OpReorderer, saveLayer_nested) {
     /* saveLayer1 { rect1, saveLayer2 { rect2 } } will play back as:
      * - startTemporaryLayer2, rect2 endLayer2
      * - startTemporaryLayer1, rect1, drawLayer2, endLayer1
@@ -538,7 +538,7 @@
     EXPECT_EQ(10, renderer.getIndex());
 }
 
-TEST(OpReorderer, saveLayerContentRejection) {
+TEST(OpReorderer, saveLayer_contentRejection) {
         auto node = TestUtils::createNode(0, 0, 200, 200,
                 [](RenderProperties& props, RecordingCanvas& canvas) {
         canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
@@ -559,7 +559,165 @@
     reorderer.replayBakedOps<TestDispatcher>(renderer);
 }
 
-RENDERTHREAD_TEST(OpReorderer, hwLayerSimple) {
+TEST(OpReorderer, saveLayerUnclipped_simple) {
+    class SaveLayerUnclippedSimpleTestRenderer : public TestRendererBase {
+    public:
+        void onCopyToLayerOp(const CopyToLayerOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(0, mIndex++);
+            EXPECT_EQ(Rect(10, 10, 190, 190), state.computedState.clippedBounds);
+            EXPECT_EQ(nullptr, state.computedState.clipState);
+            EXPECT_TRUE(state.computedState.transform.isIdentity());
+        }
+        void onSimpleRectsOp(const SimpleRectsOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(1, mIndex++);
+            ASSERT_NE(nullptr, op.paint);
+            ASSERT_EQ(SkXfermode::kClear_Mode, PaintUtils::getXfermodeDirect(op.paint));
+        }
+        void onRectOp(const RectOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(2, mIndex++);
+            EXPECT_EQ(Rect(200, 200), op.unmappedBounds);
+            EXPECT_EQ(Rect(200, 200), state.computedState.clippedBounds);
+            EXPECT_EQ(Rect(200, 200), state.computedState.clipRect());
+            EXPECT_TRUE(state.computedState.transform.isIdentity());
+        }
+        void onCopyFromLayerOp(const CopyFromLayerOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(3, mIndex++);
+            EXPECT_EQ(Rect(10, 10, 190, 190), state.computedState.clippedBounds);
+            EXPECT_EQ(nullptr, state.computedState.clipState);
+            EXPECT_TRUE(state.computedState.transform.isIdentity());
+        }
+    };
+
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
+        canvas.saveLayerAlpha(10, 10, 190, 190, 128, SkCanvas::kMatrixClip_SaveFlag);
+        canvas.drawRect(0, 0, 200, 200, SkPaint());
+        canvas.restore();
+    });
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
+    SaveLayerUnclippedSimpleTestRenderer renderer;
+    reorderer.replayBakedOps<TestDispatcher>(renderer);
+    EXPECT_EQ(4, renderer.getIndex());
+}
+
+TEST(OpReorderer, saveLayerUnclipped_mergedClears) {
+    class SaveLayerUnclippedMergedClearsTestRenderer : public TestRendererBase {
+    public:
+        void onCopyToLayerOp(const CopyToLayerOp& op, const BakedOpState& state) override {
+            int index = mIndex++;
+            EXPECT_GT(4, index);
+            EXPECT_EQ(5, op.unmappedBounds.getWidth());
+            EXPECT_EQ(5, op.unmappedBounds.getHeight());
+            if (index == 0) {
+                EXPECT_EQ(Rect(10, 10), state.computedState.clippedBounds);
+            } else if (index == 1) {
+                EXPECT_EQ(Rect(190, 0, 200, 10), state.computedState.clippedBounds);
+            } else if (index == 2) {
+                EXPECT_EQ(Rect(0, 190, 10, 200), state.computedState.clippedBounds);
+            } else if (index == 3) {
+                EXPECT_EQ(Rect(190, 190, 200, 200), state.computedState.clippedBounds);
+            }
+        }
+        void onSimpleRectsOp(const SimpleRectsOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(4, mIndex++);
+            ASSERT_EQ(op.vertexCount, 16u);
+            for (size_t i = 0; i < op.vertexCount; i++) {
+                auto v = op.vertices[i];
+                EXPECT_TRUE(v.x == 0 || v.x == 10 || v.x == 190 || v.x == 200);
+                EXPECT_TRUE(v.y == 0 || v.y == 10 || v.y == 190 || v.y == 200);
+            }
+        }
+        void onRectOp(const RectOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(5, mIndex++);
+        }
+        void onCopyFromLayerOp(const CopyFromLayerOp& op, const BakedOpState& state) override {
+            EXPECT_LT(5, mIndex++);
+        }
+    };
+
+    auto node = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
+
+        int restoreTo = canvas.save(SkCanvas::kMatrixClip_SaveFlag);
+        canvas.scale(2, 2);
+        canvas.saveLayerAlpha(0, 0, 5, 5, 128, SkCanvas::kMatrixClip_SaveFlag);
+        canvas.saveLayerAlpha(95, 0, 100, 5, 128, SkCanvas::kMatrixClip_SaveFlag);
+        canvas.saveLayerAlpha(0, 95, 5, 100, 128, SkCanvas::kMatrixClip_SaveFlag);
+        canvas.saveLayerAlpha(95, 95, 100, 100, 128, SkCanvas::kMatrixClip_SaveFlag);
+        canvas.drawRect(0, 0, 100, 100, SkPaint());
+        canvas.restoreToCount(restoreTo);
+    });
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200,
+            createSyncedNodeList(node), sLightCenter);
+    SaveLayerUnclippedMergedClearsTestRenderer renderer;
+    reorderer.replayBakedOps<TestDispatcher>(renderer);
+    EXPECT_EQ(10, renderer.getIndex())
+            << "Expect 4 copyTos, 4 copyFroms, 1 clear SimpleRects, and 1 rect.";
+}
+
+/* saveLayerUnclipped { saveLayer { saveLayerUnclipped { rect } } } will play back as:
+ * - startTemporaryLayer, onCopyToLayer, onSimpleRects, onRect, onCopyFromLayer, endLayer
+ * - startFrame, onCopyToLayer, onSimpleRects, drawLayer, onCopyFromLayer, endframe
+ */
+TEST(OpReorderer, saveLayerUnclipped_complex) {
+    class SaveLayerUnclippedComplexTestRenderer : public TestRendererBase {
+    public:
+        OffscreenBuffer* startTemporaryLayer(uint32_t width, uint32_t height) {
+            EXPECT_EQ(0, mIndex++); // savelayer first
+            return (OffscreenBuffer*)0xabcd;
+        }
+        void onCopyToLayerOp(const CopyToLayerOp& op, const BakedOpState& state) override {
+            int index = mIndex++;
+            EXPECT_TRUE(index == 1 || index == 7);
+        }
+        void onSimpleRectsOp(const SimpleRectsOp& op, const BakedOpState& state) override {
+            int index = mIndex++;
+            EXPECT_TRUE(index == 2 || index == 8);
+        }
+        void onRectOp(const RectOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(3, mIndex++);
+            Matrix4 expected;
+            expected.loadTranslate(-100, -100, 0);
+            EXPECT_EQ(Rect(100, 100, 200, 200), state.computedState.clippedBounds);
+            EXPECT_MATRIX_APPROX_EQ(expected, state.computedState.transform);
+        }
+        void onCopyFromLayerOp(const CopyFromLayerOp& op, const BakedOpState& state) override {
+            int index = mIndex++;
+            EXPECT_TRUE(index == 4 || index == 10);
+        }
+        void endLayer() override {
+            EXPECT_EQ(5, mIndex++);
+        }
+        void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) override {
+            EXPECT_EQ(6, mIndex++);
+        }
+        void onLayerOp(const LayerOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(9, mIndex++);
+        }
+        void endFrame(const Rect& repaintRect) override {
+            EXPECT_EQ(11, mIndex++);
+        }
+    };
+
+    auto node = TestUtils::createNode(0, 0, 600, 600, // 500x500 triggers clipping
+            [](RenderProperties& props, RecordingCanvas& canvas) {
+        canvas.saveLayerAlpha(0, 0, 500, 500, 128, (SkCanvas::SaveFlags)0); // unclipped
+        canvas.saveLayerAlpha(100, 100, 400, 400, 128, SkCanvas::kClipToLayer_SaveFlag); // clipped
+        canvas.saveLayerAlpha(200, 200, 300, 300, 128, (SkCanvas::SaveFlags)0); // unclipped
+        canvas.drawRect(200, 200, 300, 300, SkPaint());
+        canvas.restore();
+        canvas.restore();
+        canvas.restore();
+    });
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(600, 600), 600, 600,
+            createSyncedNodeList(node), sLightCenter);
+    SaveLayerUnclippedComplexTestRenderer renderer;
+    reorderer.replayBakedOps<TestDispatcher>(renderer);
+    EXPECT_EQ(12, renderer.getIndex());
+}
+
+RENDERTHREAD_TEST(OpReorderer, hwLayer_simple) {
     class HwLayerSimpleTestRenderer : public TestRendererBase {
     public:
         void startRepaintLayer(OffscreenBuffer* offscreenBuffer, const Rect& repaintRect) override {
@@ -620,7 +778,7 @@
     *layerHandle = nullptr;
 }
 
-RENDERTHREAD_TEST(OpReorderer, hwLayerComplex) {
+RENDERTHREAD_TEST(OpReorderer, hwLayer_complex) {
     /* parentLayer { greyRect, saveLayer { childLayer { whiteRect } } } will play back as:
      * - startRepaintLayer(child), rect(grey), endLayer
      * - startTemporaryLayer, drawLayer(child), endLayer
diff --git a/libs/hwui/tests/unit/RecordingCanvasTests.cpp b/libs/hwui/tests/unit/RecordingCanvasTests.cpp
index a63cb18..795ac30 100644
--- a/libs/hwui/tests/unit/RecordingCanvasTests.cpp
+++ b/libs/hwui/tests/unit/RecordingCanvasTests.cpp
@@ -232,7 +232,7 @@
 
 TEST(RecordingCanvas, saveLayer_simple) {
     auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
-        canvas.saveLayerAlpha(10, 20, 190, 180, 128, SkCanvas::kARGB_ClipLayer_SaveFlag);
+        canvas.saveLayerAlpha(10, 20, 190, 180, 128, SkCanvas::kClipToLayer_SaveFlag);
         canvas.drawRect(10, 20, 190, 180, SkPaint());
         canvas.restore();
     });
@@ -264,12 +264,78 @@
     EXPECT_EQ(3, count);
 }
 
+TEST(RecordingCanvas, saveLayer_missingRestore) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        canvas.saveLayerAlpha(0, 0, 200, 200, 128, SkCanvas::kClipToLayer_SaveFlag);
+        canvas.drawRect(0, 0, 200, 200, SkPaint());
+        // Note: restore omitted, shouldn't result in unmatched save
+    });
+    int count = 0;
+    playbackOps(*dl, [&count](const RecordedOp& op) {
+        if (count++ == 2) {
+            EXPECT_EQ(RecordedOpId::EndLayerOp, op.opId);
+        }
+    });
+    EXPECT_EQ(3, count) << "Missing a restore shouldn't result in an unmatched saveLayer";
+}
+
+TEST(RecordingCanvas, saveLayer_simpleUnclipped) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        canvas.saveLayerAlpha(10, 20, 190, 180, 128, (SkCanvas::SaveFlags)0); // unclipped
+        canvas.drawRect(10, 20, 190, 180, SkPaint());
+        canvas.restore();
+    });
+    int count = 0;
+    playbackOps(*dl, [&count](const RecordedOp& op) {
+        switch(count++) {
+        case 0:
+            EXPECT_EQ(RecordedOpId::BeginUnclippedLayerOp, op.opId);
+            EXPECT_EQ(Rect(10, 20, 190, 180), op.unmappedBounds);
+            EXPECT_EQ(nullptr, op.localClip);
+            EXPECT_TRUE(op.localMatrix.isIdentity());
+            break;
+        case 1:
+            EXPECT_EQ(RecordedOpId::RectOp, op.opId);
+            EXPECT_EQ(nullptr, op.localClip);
+            EXPECT_EQ(Rect(10, 20, 190, 180), op.unmappedBounds);
+            EXPECT_TRUE(op.localMatrix.isIdentity());
+            break;
+        case 2:
+            EXPECT_EQ(RecordedOpId::EndUnclippedLayerOp, op.opId);
+            // Don't bother asserting recording state data - it's not used
+            break;
+        default:
+            ADD_FAILURE();
+        }
+    });
+    EXPECT_EQ(3, count);
+}
+
+TEST(RecordingCanvas, saveLayer_addClipFlag) {
+    auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        canvas.save(SkCanvas::kMatrixClip_SaveFlag);
+        canvas.clipRect(10, 20, 190, 180, SkRegion::kIntersect_Op);
+        canvas.saveLayerAlpha(10, 20, 190, 180, 128, (SkCanvas::SaveFlags)0); // unclipped
+        canvas.drawRect(10, 20, 190, 180, SkPaint());
+        canvas.restore();
+        canvas.restore();
+    });
+    int count = 0;
+    playbackOps(*dl, [&count](const RecordedOp& op) {
+        if (count++ == 0) {
+            EXPECT_EQ(RecordedOpId::BeginLayerOp, op.opId)
+                    << "Clip + unclipped saveLayer should result in a clipped layer";
+        }
+    });
+    EXPECT_EQ(3, count);
+}
+
 TEST(RecordingCanvas, saveLayer_viewportCrop) {
     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.saveLayerAlpha(100, 100, 300, 300, 128, SkCanvas::kClipToLayer_SaveFlag);
         canvas.drawRect(0, 0, 400, 400, SkPaint());
         canvas.restore();
     });
@@ -295,7 +361,7 @@
         canvas.rotate(45);
         canvas.translate(-50, -50);
 
-        canvas.saveLayerAlpha(0, 0, 100, 100, 128, SkCanvas::kARGB_ClipLayer_SaveFlag);
+        canvas.saveLayerAlpha(0, 0, 100, 100, 128, SkCanvas::kClipToLayer_SaveFlag);
         canvas.drawRect(0, 0, 100, 100, SkPaint());
         canvas.restore();
 
@@ -322,7 +388,7 @@
         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.saveLayerAlpha(0, 0, 400, 400, 128, SkCanvas::kClipToLayer_SaveFlag);
         canvas.drawRect(0, 0, 400, 400, SkPaint());
         canvas.restore();