Clip projected ripples to outlines

bug:27343928

Also fixes positioning of ripples to a scrolled projection receiver.

Change-Id: I74b7233c46d7c15839ca8bf50e188ba6646d7432
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp
index 78764b5..bce583c 100644
--- a/libs/hwui/BakedOpDispatcher.cpp
+++ b/libs/hwui/BakedOpDispatcher.cpp
@@ -30,6 +30,7 @@
 #include <algorithm>
 #include <math.h>
 #include <SkPaintDefaults.h>
+#include <SkPathOps.h>
 
 namespace android {
 namespace uirenderer {
@@ -527,6 +528,12 @@
         SkPath path;
         SkRect rect = getBoundsOfFill(op);
         path.addOval(rect);
+
+        if (state.computedState.localProjectionPathMask != nullptr) {
+            // Mask the ripple path by the local space projection mask in local space.
+            // Note that this can create CCW paths.
+            Op(path, *state.computedState.localProjectionPathMask, kIntersect_SkPathOp, &path);
+        }
         renderConvexPath(renderer, state, path, *(op.paint));
     }
 }
diff --git a/libs/hwui/BakedOpState.cpp b/libs/hwui/BakedOpState.cpp
index 682bd04..26653f7 100644
--- a/libs/hwui/BakedOpState.cpp
+++ b/libs/hwui/BakedOpState.cpp
@@ -63,9 +63,22 @@
         clipState = nullptr;
         clippedBounds.setEmpty();
     } else {
-        // Not rejected! compute true clippedBounds and clipSideFlags
+        // Not rejected! compute true clippedBounds, clipSideFlags, and path mask
         clipSideFlags = computeClipSideFlags(clipRect, clippedBounds);
         clippedBounds.doIntersect(clipRect);
+
+        if (CC_UNLIKELY(snapshot.projectionPathMask)) {
+            // map projection path mask from render target space into op space,
+            // so intersection with op geometry is possible
+            Matrix4 inverseTransform;
+            inverseTransform.loadInverse(transform);
+            SkMatrix skInverseTransform;
+            inverseTransform.copyTo(skInverseTransform);
+
+            auto localMask = allocator.create<SkPath>();
+            snapshot.projectionPathMask->transform(skInverseTransform, localMask);
+            localProjectionPathMask = localMask;
+        }
     }
 }
 
@@ -73,13 +86,15 @@
         : transform(*snapshot.transform)
         , clipState(snapshot.mutateClipArea().serializeClip(allocator))
         , clippedBounds(clipState->rect)
-        , clipSideFlags(OpClipSideFlags::Full) {}
+        , clipSideFlags(OpClipSideFlags::Full)
+        , localProjectionPathMask(nullptr) {}
 
 ResolvedRenderState::ResolvedRenderState(const ClipRect* clipRect, const Rect& dstRect)
         : transform(Matrix4::identity())
         , clipState(clipRect)
         , clippedBounds(dstRect)
-        , clipSideFlags(computeClipSideFlags(clipRect->rect, dstRect)) {
+        , clipSideFlags(computeClipSideFlags(clipRect->rect, dstRect))
+        , localProjectionPathMask(nullptr) {
     clippedBounds.doIntersect(clipRect->rect);
 }
 
diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h
index 4365ef8..ffe2901 100644
--- a/libs/hwui/BakedOpState.h
+++ b/libs/hwui/BakedOpState.h
@@ -88,6 +88,7 @@
     const ClipBase* clipState = nullptr;
     Rect clippedBounds;
     int clipSideFlags = 0;
+    const SkPath* localProjectionPathMask = nullptr;
 };
 
 /**
@@ -154,7 +155,6 @@
     // simple state (straight pointer/value storage):
     const float alpha;
     const RoundRectClipState* roundRectClipState;
-    const ProjectionPathMask* projectionPathMask;
     const RecordedOp* op;
 
 private:
@@ -165,21 +165,18 @@
             : computedState(allocator, snapshot, recordedOp, expandForStroke)
             , alpha(snapshot.alpha)
             , roundRectClipState(snapshot.roundRectClipState)
-            , projectionPathMask(snapshot.projectionPathMask)
             , op(&recordedOp) {}
 
     BakedOpState(LinearAllocator& allocator, Snapshot& snapshot, const ShadowOp* shadowOpPtr)
             : computedState(allocator, snapshot)
             , alpha(snapshot.alpha)
             , roundRectClipState(snapshot.roundRectClipState)
-            , projectionPathMask(snapshot.projectionPathMask)
             , op(shadowOpPtr) {}
 
     BakedOpState(const ClipRect* clipRect, const Rect& dstRect, const RecordedOp& recordedOp)
             : computedState(clipRect, dstRect)
             , alpha(1.0f)
             , roundRectClipState(nullptr)
-            , projectionPathMask(nullptr)
             , op(&recordedOp) {}
 };
 
diff --git a/libs/hwui/FrameBuilder.cpp b/libs/hwui/FrameBuilder.cpp
index 04de98a..b586a01 100644
--- a/libs/hwui/FrameBuilder.cpp
+++ b/libs/hwui/FrameBuilder.cpp
@@ -389,34 +389,31 @@
 }
 
 void FrameBuilder::deferProjectedChildren(const RenderNode& renderNode) {
-    const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath();
     int count = mCanvasState.save(SaveFlags::MatrixClip);
+    const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath();
 
-    // can't be null, since DL=null node rejection happens before deferNodePropsAndOps
-    const DisplayList& displayList = *(renderNode.getDisplayList());
+    SkPath transformedMaskPath; // on stack, since BakedOpState makes a deep copy
+    if (projectionReceiverOutline) {
+        // transform the mask for this projector into render target space
+        // TODO: consider combining both transforms by stashing transform instead of applying
+        SkMatrix skCurrentTransform;
+        mCanvasState.currentTransform()->copyTo(skCurrentTransform);
+        projectionReceiverOutline->transform(
+                skCurrentTransform,
+                &transformedMaskPath);
+        mCanvasState.setProjectionPathMask(mAllocator, &transformedMaskPath);
+    }
 
-    const RecordedOp* op = (displayList.getOps()[displayList.projectionReceiveIndex]);
-    const RenderNodeOp* backgroundOp = static_cast<const RenderNodeOp*>(op);
-    const RenderProperties& backgroundProps = backgroundOp->renderNode->properties();
-
-    // Transform renderer to match background we're projecting onto
-    // (by offsetting canvas by translationX/Y of background rendernode, since only those are set)
-    mCanvasState.translate(backgroundProps.getTranslationX(), backgroundProps.getTranslationY());
-
-    // If the projection receiver has an outline, we mask projected content to it
-    // (which we know, apriori, are all tessellated paths)
-    mCanvasState.setProjectionPathMask(mAllocator, projectionReceiverOutline);
-
-    // draw projected nodes
     for (size_t i = 0; i < renderNode.mProjectedNodes.size(); i++) {
         RenderNodeOp* childOp = renderNode.mProjectedNodes[i];
-
         int restoreTo = mCanvasState.save(SaveFlags::Matrix);
+
+        // Apply transform between ancestor and projected descendant
         mCanvasState.concatMatrix(childOp->transformFromCompositingAncestor);
+
         deferRenderNodeOpImpl(*childOp);
         mCanvasState.restoreToCount(restoreTo);
     }
-
     mCanvasState.restoreToCount(count);
 }
 
diff --git a/libs/hwui/LayerBuilder.cpp b/libs/hwui/LayerBuilder.cpp
index bc39621..c5af279 100644
--- a/libs/hwui/LayerBuilder.cpp
+++ b/libs/hwui/LayerBuilder.cpp
@@ -140,7 +140,10 @@
         // Identical round rect clip state means both ops will clip in the same way, or not at all.
         // As the state objects are const, we can compare their pointers to determine mergeability
         if (lhs->roundRectClipState != rhs->roundRectClipState) return false;
-        if (lhs->projectionPathMask != rhs->projectionPathMask) return false;
+
+        // Local masks prevent merge, since they're potentially in different coordinate spaces
+        if (lhs->computedState.localProjectionPathMask
+                || rhs->computedState.localProjectionPathMask) return false;
 
         /* Clipping compatibility check
          *
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index b7a5923..7693fdc 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -1148,7 +1148,9 @@
 
     // always store/restore, since these are just pointers
     state.mRoundRectClipState = currentSnapshot()->roundRectClipState;
+#if !HWUI_NEW_OPS
     state.mProjectionPathMask = currentSnapshot()->projectionPathMask;
+#endif
     return false;
 }
 
@@ -1156,7 +1158,9 @@
     setGlobalMatrix(state.mMatrix);
     writableSnapshot()->alpha = state.mAlpha;
     writableSnapshot()->roundRectClipState = state.mRoundRectClipState;
+#if !HWUI_NEW_OPS
     writableSnapshot()->projectionPathMask = state.mProjectionPathMask;
+#endif
 
     if (state.mClipValid && !skipClipRestore) {
         writableSnapshot()->setClip(state.mClip.left, state.mClip.top,
@@ -1833,6 +1837,7 @@
         path.addCircle(x, y, radius);
     }
 
+#if !HWUI_NEW_OPS
     if (CC_UNLIKELY(currentSnapshot()->projectionPathMask != nullptr)) {
         // mask ripples with projection mask
         SkPath maskPath = *(currentSnapshot()->projectionPathMask->projectionMask);
@@ -1852,6 +1857,7 @@
         // in local space. Note that this can create CCW paths.
         Op(path, maskPath, kIntersect_SkPathOp, &path);
     }
+#endif
     drawConvexPath(path, p);
 }
 
diff --git a/libs/hwui/Snapshot.cpp b/libs/hwui/Snapshot.cpp
index 27fea1f..cf5e69a 100644
--- a/libs/hwui/Snapshot.cpp
+++ b/libs/hwui/Snapshot.cpp
@@ -146,6 +146,9 @@
 }
 
 void Snapshot::buildScreenSpaceTransform(Matrix4* outTransform) const {
+#if HWUI_NEW_OPS
+    LOG_ALWAYS_FATAL("not supported - not needed by new ops");
+#else
     // build (reverse ordered) list of the stack of snapshots, terminated with a NULL
     Vector<const Snapshot*> snapshotList;
     snapshotList.push(nullptr);
@@ -171,6 +174,7 @@
             outTransform->multiply(*(current->transform));
         }
     }
+#endif
 }
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -223,15 +227,19 @@
 }
 
 void Snapshot::setProjectionPathMask(LinearAllocator& allocator, const SkPath* path) {
+#if HWUI_NEW_OPS
+    // TODO: remove allocator param for HWUI_NEW_OPS
+    projectionPathMask = path;
+#else
     if (path) {
         ProjectionPathMask* mask = new (allocator) ProjectionPathMask;
         mask->projectionMask = path;
         buildScreenSpaceTransform(&(mask->projectionMaskTransform));
-
         projectionPathMask = mask;
     } else {
         projectionPathMask = nullptr;
     }
+#endif
 }
 
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/Snapshot.h b/libs/hwui/Snapshot.h
index b03643f..3a01d04 100644
--- a/libs/hwui/Snapshot.h
+++ b/libs/hwui/Snapshot.h
@@ -63,6 +63,7 @@
     float radius;
 };
 
+// TODO: remove for HWUI_NEW_OPS
 class ProjectionPathMask {
 public:
     static void* operator new(size_t size) = delete;
@@ -219,6 +220,7 @@
      * Fills outTransform with the current, total transform to screen space,
      * across layer boundaries.
      */
+    // TODO: remove for HWUI_NEW_OPS
     void buildScreenSpaceTransform(Matrix4* outTransform) const;
 
     /**
@@ -294,9 +296,13 @@
     const RoundRectClipState* roundRectClipState;
 
     /**
-     * Current projection masking path - used exclusively to mask tessellated circles.
+     * Current projection masking path - used exclusively to mask projected, tessellated circles.
      */
+#if HWUI_NEW_OPS
+    const SkPath* projectionPathMask;
+#else
     const ProjectionPathMask* projectionPathMask;
+#endif
 
     void dump() const;
 
diff --git a/libs/hwui/tests/unit/FrameBuilderTests.cpp b/libs/hwui/tests/unit/FrameBuilderTests.cpp
index f86898f..aedef53 100644
--- a/libs/hwui/tests/unit/FrameBuilderTests.cpp
+++ b/libs/hwui/tests/unit/FrameBuilderTests.cpp
@@ -990,21 +990,26 @@
                 EXPECT_EQ(Rect(100, 100), op.unmappedBounds);
                 EXPECT_EQ(SK_ColorWHITE, op.paint->getColor());
                 expectedMatrix.loadIdentity();
+                EXPECT_EQ(nullptr, state.computedState.localProjectionPathMask);
                 break;
             case 1:
                 EXPECT_EQ(Rect(-10, -10, 60, 60), op.unmappedBounds);
                 EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor());
-                expectedMatrix.loadTranslate(50, 50, 0); // TODO: should scroll be respected here?
+                expectedMatrix.loadTranslate(50 - scrollX, 50 - scrollY, 0);
+                ASSERT_NE(nullptr, state.computedState.localProjectionPathMask);
+                EXPECT_EQ(Rect(-35, -30, 45, 50),
+                        Rect(state.computedState.localProjectionPathMask->getBounds()));
                 break;
             case 2:
                 EXPECT_EQ(Rect(100, 50), op.unmappedBounds);
                 EXPECT_EQ(SK_ColorBLUE, op.paint->getColor());
                 expectedMatrix.loadTranslate(-scrollX, 50 - scrollY, 0);
+                EXPECT_EQ(nullptr, state.computedState.localProjectionPathMask);
                 break;
             default:
                 ADD_FAILURE();
             }
-            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, state.computedState.transform);
+            EXPECT_EQ(expectedMatrix, state.computedState.transform);
         }
     };
 
@@ -1045,6 +1050,9 @@
     });
     auto parent = TestUtils::createNode(0, 0, 100, 100,
             [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) {
+        // Set a rect outline for the projecting ripple to be masked against.
+        properties.mutableOutline().setRoundRect(10, 10, 90, 90, 5, 1.0f);
+
         canvas.save(SaveFlags::MatrixClip);
         canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally)
         canvas.drawRenderNode(receiverBackground.get());
@@ -1059,6 +1067,92 @@
     EXPECT_EQ(3, renderer.getIndex());
 }
 
+RENDERTHREAD_TEST(FrameBuilder, projectionHwLayer) {
+    static const int scrollX = 5;
+    static const int scrollY = 10;
+    class ProjectionHwLayerTestRenderer : public TestRendererBase {
+    public:
+        void startRepaintLayer(OffscreenBuffer* offscreenBuffer, const Rect& repaintRect) override {
+            EXPECT_EQ(0, mIndex++);
+        }
+        void onArcOp(const ArcOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(1, mIndex++);
+            ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask);
+        }
+        void endLayer() override {
+            EXPECT_EQ(2, mIndex++);
+        }
+        void onRectOp(const RectOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(3, mIndex++);
+            ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask);
+        }
+        void onOvalOp(const OvalOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(4, mIndex++);
+            ASSERT_NE(nullptr, state.computedState.localProjectionPathMask);
+            Matrix4 expected;
+            expected.loadTranslate(100 - scrollX, 100 - scrollY, 0);
+            EXPECT_EQ(expected, state.computedState.transform);
+            EXPECT_EQ(Rect(-85, -80, 295, 300),
+                    Rect(state.computedState.localProjectionPathMask->getBounds()));
+        }
+        void onLayerOp(const LayerOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(5, mIndex++);
+            ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask);
+        }
+    };
+    auto receiverBackground = TestUtils::createNode(0, 0, 400, 400,
+            [](RenderProperties& properties, RecordingCanvas& canvas) {
+        properties.setProjectionReceiver(true);
+        // scroll doesn't apply to background, so undone via translationX/Y
+        // NOTE: translationX/Y only! no other transform properties may be set for a proj receiver!
+        properties.setTranslationX(scrollX);
+        properties.setTranslationY(scrollY);
+
+        canvas.drawRect(0, 0, 400, 400, SkPaint());
+    });
+    auto projectingRipple = TestUtils::createNode(0, 0, 200, 200,
+            [](RenderProperties& properties, RecordingCanvas& canvas) {
+        properties.setProjectBackwards(true);
+        properties.setClipToBounds(false);
+        canvas.drawOval(100, 100, 300, 300, SkPaint()); // drawn mostly out of layer bounds
+    });
+    auto child = TestUtils::createNode(100, 100, 300, 300,
+            [&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) {
+        properties.mutateLayerProperties().setType(LayerType::RenderLayer);
+        canvas.drawRenderNode(projectingRipple.get());
+        canvas.drawArc(0, 0, 200, 200, 0.0f, 280.0f, true, SkPaint());
+    });
+    auto parent = TestUtils::createNode(0, 0, 400, 400,
+            [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) {
+        // Set a rect outline for the projecting ripple to be masked against.
+        properties.mutableOutline().setRoundRect(10, 10, 390, 390, 0, 1.0f);
+        canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally)
+        canvas.drawRenderNode(receiverBackground.get());
+        canvas.drawRenderNode(child.get());
+    });
+
+    OffscreenBuffer** layerHandle = child->getLayerHandle();
+
+    // create RenderNode's layer here in same way prepareTree would, setting windowTransform
+    OffscreenBuffer layer(renderThread.renderState(), Caches::getInstance(), 200, 200);
+    Matrix4 windowTransform;
+    windowTransform.loadTranslate(100, 100, 0); // total transform of layer's origin
+    layer.setWindowTransform(windowTransform);
+    *layerHandle = &layer;
+
+    auto syncedList = TestUtils::createSyncedNodeList(parent);
+    LayerUpdateQueue layerUpdateQueue; // Note: enqueue damage post-sync, so bounds are valid
+    layerUpdateQueue.enqueueLayerWithDamage(child.get(), Rect(200, 200));
+    FrameBuilder frameBuilder(layerUpdateQueue, SkRect::MakeWH(400, 400), 400, 400,
+            syncedList, sLightGeometry, nullptr);
+    ProjectionHwLayerTestRenderer renderer;
+    frameBuilder.replayBakedOps<TestDispatcher>(renderer);
+    EXPECT_EQ(6, renderer.getIndex());
+
+    // clean up layer pointer, so we can safely destruct RenderNode
+    *layerHandle = nullptr;
+}
+
 // creates a 100x100 shadow casting node with provided translationZ
 static sp<RenderNode> createWhiteRectShadowCaster(float translationZ) {
     return TestUtils::createNode(0, 0, 100, 100,
diff --git a/tests/HwAccelerationTest/res/layout/projection_clipping.xml b/tests/HwAccelerationTest/res/layout/projection_clipping.xml
index 1f2b939..1ea9f9c 100644
--- a/tests/HwAccelerationTest/res/layout/projection_clipping.xml
+++ b/tests/HwAccelerationTest/res/layout/projection_clipping.xml
@@ -3,24 +3,32 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
-    <FrameLayout
+    <ScrollView
+        android:orientation="vertical"
         android:translationX="50dp"
         android:translationY="50dp"
         android:elevation="30dp"
         android:layout_width="200dp"
         android:layout_height="200dp"
         android:background="@drawable/round_rect_background">
-        <View
-            android:id="@+id/clickable1"
-            android:layout_width="100dp"
-            android:layout_height="100dp"
-            android:background="?android:attr/selectableItemBackgroundBorderless"/>
-        <View
-            android:id="@+id/clickable2"
-            android:translationX="50dp"
-            android:translationY="10dp"
-            android:layout_width="150dp"
-            android:layout_height="100dp"
-            android:background="?android:attr/selectableItemBackgroundBorderless"/>
-    </FrameLayout>
+        <FrameLayout
+            android:layout_width="200dp"
+            android:layout_height="wrap_content">
+            <View
+                android:layout_width="200dp"
+                android:layout_height="2000dp"/>
+            <View
+                android:id="@+id/clickable1"
+                android:layout_width="100dp"
+                android:layout_height="100dp"
+                android:background="?android:attr/selectableItemBackgroundBorderless"/>
+            <View
+                android:id="@+id/clickable2"
+                android:translationX="50dp"
+                android:translationY="10dp"
+                android:layout_width="150dp"
+                android:layout_height="100dp"
+                android:background="?android:attr/selectableItemBackgroundBorderless"/>
+       </FrameLayout>
+    </ScrollView>
 </LinearLayout>