/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include <gtest/gtest.h> #include <BakedOpState.h> #include <DeferredLayerUpdater.h> #include <FrameBuilder.h> #include <LayerUpdateQueue.h> #include <RecordedOp.h> #include <RecordingCanvas.h> #include <tests/common/TestUtils.h> #include <unordered_map> namespace android { namespace uirenderer { const FrameBuilder::LightGeometry sLightGeometry = { {100, 100, 100}, 50}; /** * Virtual class implemented by each test to redirect static operation / state transitions to * virtual methods. * * Virtual dispatch allows for default behaviors to be specified (very common case in below tests), * and allows Renderer vs Dispatching behavior to be merged. * * onXXXOp methods fail by default - tests should override ops they expect * startRepaintLayer fails by default - tests should override if expected * startFrame/endFrame do nothing by default - tests should override to intercept */ class TestRendererBase { public: virtual ~TestRendererBase() {} virtual OffscreenBuffer* startTemporaryLayer(uint32_t, uint32_t) { ADD_FAILURE() << "Temporary layers not expected in this test"; return nullptr; } virtual void recycleTemporaryLayer(OffscreenBuffer*) { ADD_FAILURE() << "Temporary layers not expected in this test"; } virtual void startRepaintLayer(OffscreenBuffer*, const Rect& repaintRect) { ADD_FAILURE() << "Layer repaint not expected in this test"; } virtual void endLayer() { ADD_FAILURE() << "Layer updates not expected in this test"; } virtual void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) {} virtual void endFrame(const Rect& repaintRect) {} // define virtual defaults for single draw methods #define X(Type) \ virtual void on##Type(const Type&, const BakedOpState&) { \ ADD_FAILURE() << #Type " not expected in this test"; \ } MAP_RENDERABLE_OPS(X) #undef X // define virtual defaults for merged draw methods #define X(Type) \ virtual void onMerged##Type##s(const MergedBakedOpList& opList) { \ ADD_FAILURE() << "Merged " #Type "s not expected in this test"; \ } MAP_MERGEABLE_OPS(X) #undef X int getIndex() { return mIndex; } protected: int mIndex = 0; }; /** * Dispatches all static methods to similar formed methods on renderer, which fail by default but * are overridden by subclasses per test. */ class TestDispatcher { public: // define single op methods, which redirect to TestRendererBase #define X(Type) \ static void on##Type(TestRendererBase& renderer, const Type& op, const BakedOpState& state) { \ renderer.on##Type(op, state); \ } MAP_RENDERABLE_OPS(X); #undef X // define merged op methods, which redirect to TestRendererBase #define X(Type) \ static void onMerged##Type##s(TestRendererBase& renderer, const MergedBakedOpList& opList) { \ renderer.onMerged##Type##s(opList); \ } MAP_MERGEABLE_OPS(X); #undef X }; class FailRenderer : public TestRendererBase {}; RENDERTHREAD_TEST(FrameBuilder, simple) { class SimpleTestRenderer : public TestRendererBase { public: void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) override { EXPECT_EQ(0, mIndex++); EXPECT_EQ(100u, width); EXPECT_EQ(200u, height); } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); } void onBitmapOp(const BitmapOp& op, const BakedOpState& state) override { EXPECT_EQ(2, mIndex++); } void endFrame(const Rect& repaintRect) override { EXPECT_EQ(3, mIndex++); } }; auto node = TestUtils::createNode(0, 0, 100, 200, [](RenderProperties& props, RecordingCanvas& canvas) { SkBitmap bitmap = TestUtils::createSkBitmap(25, 25); canvas.drawRect(0, 0, 100, 200, SkPaint()); canvas.drawBitmap(bitmap, 10, 10, nullptr); }); FrameBuilder frameBuilder(SkRect::MakeWH(100, 200), 100, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SimpleTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(4, renderer.getIndex()); // 2 ops + start + end } RENDERTHREAD_TEST(FrameBuilder, simpleStroke) { class SimpleStrokeTestRenderer : public TestRendererBase { public: void onPointsOp(const PointsOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); // even though initial bounds are empty... EXPECT_TRUE(op.unmappedBounds.isEmpty()) << "initial bounds should be empty, since they're unstroked"; EXPECT_EQ(Rect(45, 45, 55, 55), state.computedState.clippedBounds) << "final bounds should account for stroke"; } }; auto node = TestUtils::createNode(0, 0, 100, 200, [](RenderProperties& props, RecordingCanvas& canvas) { SkPaint strokedPaint; strokedPaint.setStrokeWidth(10); canvas.drawPoint(50, 50, strokedPaint); }); FrameBuilder frameBuilder(SkRect::MakeWH(100, 200), 100, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SimpleStrokeTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, simpleRejection) { auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.save(SaveFlags::MatrixClip); canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op); // intersection should be empty canvas.drawRect(0, 0, 400, 400, SkPaint()); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); FailRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); } RENDERTHREAD_TEST(FrameBuilder, simpleBatching) { const int LOOPS = 5; class SimpleBatchingTestRenderer : public TestRendererBase { public: void onBitmapOp(const BitmapOp& op, const BakedOpState& state) override { EXPECT_TRUE(mIndex++ >= LOOPS) << "Bitmaps should be above all rects"; } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_TRUE(mIndex++ < LOOPS) << "Rects should be below all bitmaps"; } }; auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { SkBitmap bitmap = TestUtils::createSkBitmap(10, 10, kAlpha_8_SkColorType); // Disable merging by using alpha 8 bitmap // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects. // Rects don't overlap bitmaps, so bitmaps should be brought to front as a group. canvas.save(SaveFlags::MatrixClip); for (int i = 0; i < LOOPS; i++) { canvas.translate(0, 10); canvas.drawRect(0, 0, 10, 10, SkPaint()); canvas.drawBitmap(bitmap, 5, 0, nullptr); } canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SimpleBatchingTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2 * LOOPS, renderer.getIndex()) << "Expect number of ops = 2 * loop count"; } RENDERTHREAD_TEST(FrameBuilder, deferRenderNode_translateClip) { class DeferRenderNodeTranslateClipTestRenderer : public TestRendererBase { public: void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); EXPECT_EQ(Rect(5, 10, 55, 60), state.computedState.clippedBounds); EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.computedState.clipSideFlags); } }; auto node = TestUtils::createNode(0, 0, 100, 100, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.drawRect(0, 0, 100, 100, SkPaint()); }); FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(5, 10, Rect(50, 50), // translate + clip node *TestUtils::getSyncedNode(node)); DeferRenderNodeTranslateClipTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, deferRenderNodeScene) { class DeferRenderNodeSceneTestRenderer : public TestRendererBase { public: void onRectOp(const RectOp& op, const BakedOpState& state) override { const Rect& clippedBounds = state.computedState.clippedBounds; Matrix4 expected; switch (mIndex++) { case 0: // background - left side EXPECT_EQ(Rect(600, 100, 700, 500), clippedBounds); expected.loadTranslate(100, 100, 0); break; case 1: // background - top side EXPECT_EQ(Rect(100, 400, 600, 500), clippedBounds); expected.loadTranslate(100, 100, 0); break; case 2: // content EXPECT_EQ(Rect(100, 100, 700, 500), clippedBounds); expected.loadTranslate(-50, -50, 0); break; case 3: // overlay EXPECT_EQ(Rect(0, 0, 800, 200), clippedBounds); break; default: ADD_FAILURE() << "Too many rects observed"; } EXPECT_EQ(expected, state.computedState.transform); } }; std::vector<sp<RenderNode>> nodes; SkPaint transparentPaint; transparentPaint.setAlpha(128); // backdrop nodes.push_back(TestUtils::createNode(100, 100, 700, 500, // 600x400 [&transparentPaint](RenderProperties& props, RecordingCanvas& canvas) { canvas.drawRect(0, 0, 600, 400, transparentPaint); })); // content Rect contentDrawBounds(150, 150, 650, 450); // 500x300 nodes.push_back(TestUtils::createNode(0, 0, 800, 600, [&transparentPaint](RenderProperties& props, RecordingCanvas& canvas) { canvas.drawRect(0, 0, 800, 600, transparentPaint); })); // overlay nodes.push_back(TestUtils::createNode(0, 0, 800, 600, [&transparentPaint](RenderProperties& props, RecordingCanvas& canvas) { canvas.drawRect(0, 0, 800, 200, transparentPaint); })); for (auto& node : nodes) { TestUtils::syncHierarchyPropertiesAndDisplayList(node); } FrameBuilder frameBuilder(SkRect::MakeWH(800, 600), 800, 600, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNodeScene(nodes, contentDrawBounds); DeferRenderNodeSceneTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(4, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, empty_noFbo0) { class EmptyNoFbo0TestRenderer : public TestRendererBase { public: void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) override { ADD_FAILURE() << "Primary frame draw not expected in this test"; } void endFrame(const Rect& repaintRect) override { ADD_FAILURE() << "Primary frame draw not expected in this test"; } }; // Use layer update constructor, so no work is enqueued for Fbo0 LayerUpdateQueue emptyLayerUpdateQueue; FrameBuilder frameBuilder(emptyLayerUpdateQueue, sLightGeometry, Caches::getInstance()); EmptyNoFbo0TestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); } RENDERTHREAD_TEST(FrameBuilder, empty_withFbo0) { class EmptyWithFbo0TestRenderer : public TestRendererBase { public: void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) override { EXPECT_EQ(0, mIndex++); } void endFrame(const Rect& repaintRect) override { EXPECT_EQ(1, mIndex++); } }; auto node = TestUtils::createNode(10, 10, 110, 110, [](RenderProperties& props, RecordingCanvas& canvas) { // no drawn content }); // Draw, but pass node without draw content, so no work is done for primary frame FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); EmptyWithFbo0TestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2, renderer.getIndex()) << "No drawing content produced," " but fbo0 update lifecycle should still be observed"; } RENDERTHREAD_TEST(FrameBuilder, avoidOverdraw_rects) { class AvoidOverdrawRectsTestRenderer : public TestRendererBase { public: void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(mIndex++, 0) << "Should be one rect"; EXPECT_EQ(Rect(10, 10, 190, 190), op.unmappedBounds) << "Last rect should occlude others."; } }; auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.drawRect(0, 0, 200, 200, SkPaint()); canvas.drawRect(0, 0, 200, 200, SkPaint()); canvas.drawRect(10, 10, 190, 190, SkPaint()); }); // Damage (and therefore clip) is same as last draw, subset of renderable area. // This means last op occludes other contents, and they'll be rejected to avoid overdraw. FrameBuilder frameBuilder(SkRect::MakeLTRB(10, 10, 190, 190), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); EXPECT_EQ(3u, node->getDisplayList()->getOps().size()) << "Recording must not have rejected ops, in order for this test to be valid"; AvoidOverdrawRectsTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()) << "Expect exactly one op"; } RENDERTHREAD_TEST(FrameBuilder, avoidOverdraw_bitmaps) { static SkBitmap opaqueBitmap = TestUtils::createSkBitmap(50, 50, SkColorType::kRGB_565_SkColorType); static SkBitmap transpBitmap = TestUtils::createSkBitmap(50, 50, SkColorType::kAlpha_8_SkColorType); class AvoidOverdrawBitmapsTestRenderer : public TestRendererBase { public: void onBitmapOp(const BitmapOp& op, const BakedOpState& state) override { switch(mIndex++) { case 0: EXPECT_EQ(opaqueBitmap.pixelRef(), op.bitmap->pixelRef()); break; case 1: EXPECT_EQ(transpBitmap.pixelRef(), op.bitmap->pixelRef()); break; default: ADD_FAILURE() << "Only two ops expected."; } } }; auto node = TestUtils::createNode(0, 0, 50, 50, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.drawRect(0, 0, 50, 50, SkPaint()); canvas.drawRect(0, 0, 50, 50, SkPaint()); canvas.drawBitmap(transpBitmap, 0, 0, nullptr); // only the below draws should remain, since they're canvas.drawBitmap(opaqueBitmap, 0, 0, nullptr); canvas.drawBitmap(transpBitmap, 0, 0, nullptr); }); FrameBuilder frameBuilder(SkRect::MakeWH(50, 50), 50, 50, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); EXPECT_EQ(5u, node->getDisplayList()->getOps().size()) << "Recording must not have rejected ops, in order for this test to be valid"; AvoidOverdrawBitmapsTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2, renderer.getIndex()) << "Expect exactly two ops"; } RENDERTHREAD_TEST(FrameBuilder, clippedMerging) { class ClippedMergingTestRenderer : public TestRendererBase { public: void onMergedBitmapOps(const MergedBakedOpList& opList) override { EXPECT_EQ(0, mIndex); mIndex += opList.count; EXPECT_EQ(4u, opList.count); EXPECT_EQ(Rect(10, 10, 90, 90), opList.clip); EXPECT_EQ(OpClipSideFlags::Left | OpClipSideFlags::Top | OpClipSideFlags::Right, opList.clipSideFlags); } }; auto node = TestUtils::createNode(0, 0, 100, 100, [](RenderProperties& props, TestCanvas& canvas) { SkBitmap bitmap = TestUtils::createSkBitmap(20, 20); // left side clipped (to inset left half) canvas.clipRect(10, 0, 50, 100, SkRegion::kReplace_Op); canvas.drawBitmap(bitmap, 0, 40, nullptr); // top side clipped (to inset top half) canvas.clipRect(0, 10, 100, 50, SkRegion::kReplace_Op); canvas.drawBitmap(bitmap, 40, 0, nullptr); // right side clipped (to inset right half) canvas.clipRect(50, 0, 90, 100, SkRegion::kReplace_Op); canvas.drawBitmap(bitmap, 80, 40, nullptr); // bottom not clipped, just abutting (inset bottom half) canvas.clipRect(0, 50, 100, 90, SkRegion::kReplace_Op); canvas.drawBitmap(bitmap, 40, 70, nullptr); }); FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); ClippedMergingTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(4, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, textMerging) { class TextMergingTestRenderer : public TestRendererBase { public: void onMergedTextOps(const MergedBakedOpList& opList) override { EXPECT_EQ(0, mIndex); mIndex += opList.count; EXPECT_EQ(2u, opList.count); EXPECT_EQ(OpClipSideFlags::Top, opList.clipSideFlags); EXPECT_EQ(OpClipSideFlags::Top, opList.states[0]->computedState.clipSideFlags); EXPECT_EQ(OpClipSideFlags::None, opList.states[1]->computedState.clipSideFlags); } }; auto node = TestUtils::createNode(0, 0, 400, 400, [](RenderProperties& props, TestCanvas& canvas) { SkPaint paint; paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); paint.setAntiAlias(true); paint.setTextSize(50); TestUtils::drawUtf8ToCanvas(&canvas, "Test string1", paint, 100, 0); // will be top clipped TestUtils::drawUtf8ToCanvas(&canvas, "Test string1", paint, 100, 100); // not clipped }); FrameBuilder frameBuilder(SkRect::MakeWH(400, 400), 400, 400, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); TextMergingTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2, renderer.getIndex()) << "Expect 2 ops"; } RENDERTHREAD_TEST(FrameBuilder, textStrikethrough) { const int LOOPS = 5; class TextStrikethroughTestRenderer : public TestRendererBase { public: void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_TRUE(mIndex++ >= LOOPS) << "Strikethrough rects should be above all text"; } void onMergedTextOps(const MergedBakedOpList& opList) override { EXPECT_EQ(0, mIndex); mIndex += opList.count; EXPECT_EQ(5u, opList.count); } }; auto node = TestUtils::createNode(0, 0, 200, 2000, [](RenderProperties& props, RecordingCanvas& canvas) { SkPaint textPaint; textPaint.setAntiAlias(true); textPaint.setTextSize(20); textPaint.setStrikeThruText(true); textPaint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); for (int i = 0; i < LOOPS; i++) { TestUtils::drawUtf8ToCanvas(&canvas, "test text", textPaint, 10, 100 * (i + 1)); } }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 2000), 200, 2000, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); TextStrikethroughTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2 * LOOPS, renderer.getIndex()) << "Expect number of ops = 2 * loop count"; } static auto styles = { SkPaint::kFill_Style, SkPaint::kStroke_Style, SkPaint::kStrokeAndFill_Style }; RENDERTHREAD_TEST(FrameBuilder, textStyle) { class TextStyleTestRenderer : public TestRendererBase { public: void onMergedTextOps(const MergedBakedOpList& opList) override { ASSERT_EQ(0, mIndex); ASSERT_EQ(3u, opList.count); mIndex += opList.count; int index = 0; for (auto style : styles) { auto state = opList.states[index++]; ASSERT_EQ(style, state->op->paint->getStyle()) << "Remainder of validation relies upon stable merged order"; ASSERT_EQ(0, state->computedState.clipSideFlags) << "Clipped bounds validation requires unclipped ops"; } Rect fill = opList.states[0]->computedState.clippedBounds; Rect stroke = opList.states[1]->computedState.clippedBounds; EXPECT_EQ(stroke, opList.states[2]->computedState.clippedBounds) << "Stroke+Fill should be same as stroke"; EXPECT_TRUE(stroke.contains(fill)); EXPECT_FALSE(fill.contains(stroke)); // outset by half the stroke width Rect outsetFill(fill); outsetFill.outset(5); EXPECT_EQ(stroke, outsetFill); } }; auto node = TestUtils::createNode(0, 0, 400, 400, [](RenderProperties& props, TestCanvas& canvas) { SkPaint paint; paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); paint.setAntiAlias(true); paint.setTextSize(50); paint.setStrokeWidth(10); // draw 3 copies of the same text overlapping, each with a different style. // They'll get merged, but with for (auto style : styles) { paint.setStyle(style); TestUtils::drawUtf8ToCanvas(&canvas, "Test string1", paint, 100, 100); } }); FrameBuilder frameBuilder(SkRect::MakeWH(400, 400), 400, 400, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); TextStyleTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(3, renderer.getIndex()) << "Expect 3 ops"; } RENDERTHREAD_TEST(FrameBuilder, textureLayer_clipLocalMatrix) { class TextureLayerClipLocalMatrixTestRenderer : public TestRendererBase { public: void onTextureLayerOp(const TextureLayerOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); EXPECT_EQ(Rect(50, 50, 150, 150), state.computedState.clipRect()); EXPECT_EQ(Rect(50, 50, 105, 105), state.computedState.clippedBounds); Matrix4 expected; expected.loadTranslate(5, 5, 0); EXPECT_MATRIX_APPROX_EQ(expected, state.computedState.transform); } }; auto layerUpdater = TestUtils::createTextureLayerUpdater(renderThread, 100, 100, SkMatrix::MakeTrans(5, 5)); auto node = TestUtils::createNode(0, 0, 200, 200, [&layerUpdater](RenderProperties& props, RecordingCanvas& canvas) { canvas.save(SaveFlags::MatrixClip); canvas.clipRect(50, 50, 150, 150, SkRegion::kIntersect_Op); canvas.drawLayer(layerUpdater.get()); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); TextureLayerClipLocalMatrixTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, textureLayer_combineMatrices) { class TextureLayerCombineMatricesTestRenderer : public TestRendererBase { public: void onTextureLayerOp(const TextureLayerOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); Matrix4 expected; expected.loadTranslate(35, 45, 0); EXPECT_MATRIX_APPROX_EQ(expected, state.computedState.transform); } }; auto layerUpdater = TestUtils::createTextureLayerUpdater(renderThread, 100, 100, SkMatrix::MakeTrans(5, 5)); auto node = TestUtils::createNode(0, 0, 200, 200, [&layerUpdater](RenderProperties& props, RecordingCanvas& canvas) { canvas.save(SaveFlags::MatrixClip); canvas.translate(30, 40); canvas.drawLayer(layerUpdater.get()); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); TextureLayerCombineMatricesTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, textureLayer_reject) { auto layerUpdater = TestUtils::createTextureLayerUpdater(renderThread, 100, 100, SkMatrix::MakeTrans(5, 5)); layerUpdater->backingLayer()->setRenderTarget(GL_NONE); // Should be rejected auto node = TestUtils::createNode(0, 0, 200, 200, [&layerUpdater](RenderProperties& props, RecordingCanvas& canvas) { canvas.drawLayer(layerUpdater.get()); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); FailRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); } RENDERTHREAD_TEST(FrameBuilder, functor_reject) { class FunctorTestRenderer : public TestRendererBase { public: void onFunctorOp(const FunctorOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); } }; Functor noopFunctor; // 1 million pixel tall view, scrolled down 80% auto scrolledFunctorView = TestUtils::createNode(0, 0, 400, 1000000, [&noopFunctor](RenderProperties& props, RecordingCanvas& canvas) { canvas.translate(0, -800000); canvas.callDrawGLFunction(&noopFunctor, nullptr); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(scrolledFunctorView)); FunctorTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()) << "Functor should not be rejected"; } RENDERTHREAD_TEST(FrameBuilder, deferColorOp_unbounded) { class ColorTestRenderer : public TestRendererBase { public: void onColorOp(const ColorOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); EXPECT_EQ(Rect(200, 200), state.computedState.clippedBounds) << "Color op should be expanded to bounds of surrounding"; } }; auto unclippedColorView = TestUtils::createNode(0, 0, 10, 10, [](RenderProperties& props, RecordingCanvas& canvas) { props.setClipToBounds(false); canvas.drawColor(SK_ColorWHITE, SkXfermode::Mode::kSrcOver_Mode); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(unclippedColorView)); ColorTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()) << "ColorOp should not be rejected"; } TEST(FrameBuilder, renderNode) { class RenderNodeTestRenderer : public TestRendererBase { public: void onRectOp(const RectOp& op, const BakedOpState& state) override { switch(mIndex++) { case 0: EXPECT_EQ(Rect(200, 200), state.computedState.clippedBounds); EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor()); break; case 1: EXPECT_EQ(Rect(50, 50, 150, 150), state.computedState.clippedBounds); EXPECT_EQ(SK_ColorWHITE, op.paint->getColor()); break; default: ADD_FAILURE(); } } }; auto child = TestUtils::createNode(10, 10, 110, 110, [](RenderProperties& props, RecordingCanvas& canvas) { SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }); auto parent = TestUtils::createNode(0, 0, 200, 200, [&child](RenderProperties& props, RecordingCanvas& canvas) { SkPaint paint; paint.setColor(SK_ColorDKGRAY); canvas.drawRect(0, 0, 200, 200, paint); canvas.save(SaveFlags::MatrixClip); canvas.translate(40, 40); canvas.drawRenderNode(child.get()); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(parent)); RenderNodeTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, clipped) { class ClippedTestRenderer : public TestRendererBase { public: void onBitmapOp(const BitmapOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); 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()); } }; auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { SkBitmap bitmap = TestUtils::createSkBitmap(200, 200); canvas.drawBitmap(bitmap, 0, 0, nullptr); }); // clip to small area, should see in receiver FrameBuilder frameBuilder(SkRect::MakeLTRB(10, 20, 30, 40), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); ClippedTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); } RENDERTHREAD_TEST(FrameBuilder, saveLayer_simple) { class SaveLayerSimpleTestRenderer : public TestRendererBase { public: OffscreenBuffer* startTemporaryLayer(uint32_t width, uint32_t height) override { EXPECT_EQ(0, mIndex++); EXPECT_EQ(180u, width); EXPECT_EQ(180u, height); return nullptr; } void endLayer() override { EXPECT_EQ(2, mIndex++); } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); EXPECT_EQ(Rect(10, 10, 190, 190), op.unmappedBounds); EXPECT_EQ(Rect(180, 180), state.computedState.clippedBounds); EXPECT_EQ(Rect(180, 180), state.computedState.clipRect()); Matrix4 expectedTransform; expectedTransform.loadTranslate(-10, -10, 0); EXPECT_MATRIX_APPROX_EQ(expectedTransform, state.computedState.transform); } void onLayerOp(const LayerOp& op, const BakedOpState& state) override { EXPECT_EQ(3, mIndex++); EXPECT_EQ(Rect(10, 10, 190, 190), state.computedState.clippedBounds); EXPECT_EQ(Rect(200, 200), state.computedState.clipRect()); EXPECT_TRUE(state.computedState.transform.isIdentity()); } void recycleTemporaryLayer(OffscreenBuffer* offscreenBuffer) override { EXPECT_EQ(4, mIndex++); EXPECT_EQ(nullptr, offscreenBuffer); } }; auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.saveLayerAlpha(10, 10, 190, 190, 128, SaveFlags::ClipToLayer); canvas.drawRect(10, 10, 190, 190, SkPaint()); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SaveLayerSimpleTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(5, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, saveLayer_nested) { /* saveLayer1 { rect1, saveLayer2 { rect2 } } will play back as: * - startTemporaryLayer2, rect2 endLayer2 * - startTemporaryLayer1, rect1, drawLayer2, endLayer1 * - startFrame, layerOp1, endFrame */ class SaveLayerNestedTestRenderer : public TestRendererBase { public: OffscreenBuffer* startTemporaryLayer(uint32_t width, uint32_t height) override { const int index = mIndex++; if (index == 0) { EXPECT_EQ(400u, width); EXPECT_EQ(400u, height); return (OffscreenBuffer*) 0x400; } else if (index == 3) { EXPECT_EQ(800u, width); EXPECT_EQ(800u, height); return (OffscreenBuffer*) 0x800; } else { ADD_FAILURE(); } return (OffscreenBuffer*) nullptr; } void endLayer() override { int index = mIndex++; EXPECT_TRUE(index == 2 || index == 6); } void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) override { EXPECT_EQ(7, mIndex++); } void endFrame(const Rect& repaintRect) override { EXPECT_EQ(9, mIndex++); } void onRectOp(const RectOp& op, const BakedOpState& state) override { const int index = mIndex++; if (index == 1) { EXPECT_EQ(Rect(400, 400), op.unmappedBounds); // inner rect } else if (index == 4) { EXPECT_EQ(Rect(800, 800), op.unmappedBounds); // outer rect } else { ADD_FAILURE(); } } void onLayerOp(const LayerOp& op, const BakedOpState& state) override { const int index = mIndex++; if (index == 5) { EXPECT_EQ((OffscreenBuffer*)0x400, *op.layerHandle); EXPECT_EQ(Rect(400, 400), op.unmappedBounds); // inner layer } else if (index == 8) { EXPECT_EQ((OffscreenBuffer*)0x800, *op.layerHandle); EXPECT_EQ(Rect(800, 800), op.unmappedBounds); // outer layer } else { ADD_FAILURE(); } } void recycleTemporaryLayer(OffscreenBuffer* offscreenBuffer) override { const int index = mIndex++; // order isn't important, but we need to see both if (index == 10) { EXPECT_EQ((OffscreenBuffer*)0x400, offscreenBuffer); } else if (index == 11) { EXPECT_EQ((OffscreenBuffer*)0x800, offscreenBuffer); } else { ADD_FAILURE(); } } }; auto node = TestUtils::createNode(0, 0, 800, 800, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.saveLayerAlpha(0, 0, 800, 800, 128, SaveFlags::ClipToLayer); { canvas.drawRect(0, 0, 800, 800, SkPaint()); canvas.saveLayerAlpha(0, 0, 400, 400, 128, SaveFlags::ClipToLayer); { canvas.drawRect(0, 0, 400, 400, SkPaint()); } canvas.restore(); } canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(800, 800), 800, 800, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SaveLayerNestedTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(12, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, saveLayer_contentRejection) { auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.save(SaveFlags::MatrixClip); canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op); canvas.saveLayerAlpha(200, 200, 400, 400, 128, SaveFlags::ClipToLayer); // draw within save layer may still be recorded, but shouldn't be drawn canvas.drawRect(200, 200, 400, 400, SkPaint()); canvas.restore(); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); FailRenderer renderer; // should see no ops, even within the layer, since the layer should be rejected frameBuilder.replayBakedOps<TestDispatcher>(renderer); } RENDERTHREAD_TEST(FrameBuilder, 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_CLIP_RECT(Rect(200, 200), 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_CLIP_RECT(Rect(200, 200), 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, (SaveFlags::Flags)(0)); canvas.drawRect(0, 0, 200, 200, SkPaint()); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SaveLayerUnclippedSimpleTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(4, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, saveLayerUnclipped_round) { class SaveLayerUnclippedRoundTestRenderer : 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) << "Bounds rect should round out"; } void onSimpleRectsOp(const SimpleRectsOp& op, const BakedOpState& state) override {} void onRectOp(const RectOp& op, const BakedOpState& state) override {} void onCopyFromLayerOp(const CopyFromLayerOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); EXPECT_EQ(Rect(10, 10, 190, 190), state.computedState.clippedBounds) << "Bounds rect should round out"; } }; auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.saveLayerAlpha(10.95f, 10.5f, 189.75f, 189.25f, // values should all round out 128, (SaveFlags::Flags)(0)); canvas.drawRect(0, 0, 200, 200, SkPaint()); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SaveLayerUnclippedRoundTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, 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(SaveFlags::MatrixClip); canvas.scale(2, 2); canvas.saveLayerAlpha(0, 0, 5, 5, 128, SaveFlags::MatrixClip); canvas.saveLayerAlpha(95, 0, 100, 5, 128, SaveFlags::MatrixClip); canvas.saveLayerAlpha(0, 95, 5, 100, 128, SaveFlags::MatrixClip); canvas.saveLayerAlpha(95, 95, 100, 100, 128, SaveFlags::MatrixClip); canvas.drawRect(0, 0, 100, 100, SkPaint()); canvas.restoreToCount(restoreTo); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SaveLayerUnclippedMergedClearsTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(10, renderer.getIndex()) << "Expect 4 copyTos, 4 copyFroms, 1 clear SimpleRects, and 1 rect."; } RENDERTHREAD_TEST(FrameBuilder, saveLayerUnclipped_clearClip) { class SaveLayerUnclippedClearClipTestRenderer : public TestRendererBase { public: void onCopyToLayerOp(const CopyToLayerOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); } void onSimpleRectsOp(const SimpleRectsOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); ASSERT_NE(nullptr, op.paint); EXPECT_EQ(SkXfermode::kClear_Mode, PaintUtils::getXfermodeDirect(op.paint)); EXPECT_EQ(Rect(50, 50, 150, 150), state.computedState.clippedBounds) << "Expect dirty rect as clip"; ASSERT_NE(nullptr, state.computedState.clipState); EXPECT_EQ(Rect(50, 50, 150, 150), state.computedState.clipState->rect); EXPECT_EQ(ClipMode::Rectangle, state.computedState.clipState->mode); } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(2, mIndex++); } void onCopyFromLayerOp(const CopyFromLayerOp& op, const BakedOpState& state) override { EXPECT_EQ(3, mIndex++); } }; auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { // save smaller than clip, so we get unclipped behavior canvas.saveLayerAlpha(10, 10, 190, 190, 128, (SaveFlags::Flags)(0)); canvas.drawRect(0, 0, 200, 200, SkPaint()); canvas.restore(); }); // draw with partial screen dirty, and assert we see that rect later FrameBuilder frameBuilder(SkRect::MakeLTRB(50, 50, 150, 150), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SaveLayerUnclippedClearClipTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(4, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, saveLayerUnclipped_reject) { auto node = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { // unclipped savelayer + rect both in area that won't intersect with dirty canvas.saveLayerAlpha(100, 100, 200, 200, 128, (SaveFlags::Flags)(0)); canvas.drawRect(100, 100, 200, 200, SkPaint()); canvas.restore(); }); // draw with partial screen dirty that doesn't intersect with savelayer FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); FailRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); } /* saveLayerUnclipped { saveLayer { saveLayerUnclipped { rect } } } will play back as: * - startTemporaryLayer, onCopyToLayer, onSimpleRects, onRect, onCopyFromLayer, endLayer * - startFrame, onCopyToLayer, onSimpleRects, drawLayer, onCopyFromLayer, endframe */ RENDERTHREAD_TEST(FrameBuilder, 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++); EXPECT_EQ((OffscreenBuffer*)0xabcd, *op.layerHandle); } void endFrame(const Rect& repaintRect) override { EXPECT_EQ(11, mIndex++); } void recycleTemporaryLayer(OffscreenBuffer* offscreenBuffer) override { EXPECT_EQ(12, mIndex++); EXPECT_EQ((OffscreenBuffer*)0xabcd, offscreenBuffer); } }; auto node = TestUtils::createNode(0, 0, 600, 600, // 500x500 triggers clipping [](RenderProperties& props, RecordingCanvas& canvas) { canvas.saveLayerAlpha(0, 0, 500, 500, 128, (SaveFlags::Flags)0); // unclipped canvas.saveLayerAlpha(100, 100, 400, 400, 128, SaveFlags::ClipToLayer); // clipped canvas.saveLayerAlpha(200, 200, 300, 300, 128, (SaveFlags::Flags)0); // unclipped canvas.drawRect(200, 200, 300, 300, SkPaint()); canvas.restore(); canvas.restore(); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(600, 600), 600, 600, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); SaveLayerUnclippedComplexTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(13, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, hwLayer_simple) { class HwLayerSimpleTestRenderer : public TestRendererBase { public: void startRepaintLayer(OffscreenBuffer* offscreenBuffer, const Rect& repaintRect) override { EXPECT_EQ(0, mIndex++); EXPECT_EQ(100u, offscreenBuffer->viewportWidth); EXPECT_EQ(100u, offscreenBuffer->viewportHeight); EXPECT_EQ(Rect(25, 25, 75, 75), repaintRect); } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); EXPECT_TRUE(state.computedState.transform.isIdentity()) << "Transform should be reset within layer"; EXPECT_EQ(Rect(25, 25, 75, 75), state.computedState.clipRect()) << "Damage rect should be used to clip layer content"; } void endLayer() override { EXPECT_EQ(2, mIndex++); } void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) override { EXPECT_EQ(3, mIndex++); } void onLayerOp(const LayerOp& op, const BakedOpState& state) override { EXPECT_EQ(4, mIndex++); } void endFrame(const Rect& repaintRect) override { EXPECT_EQ(5, mIndex++); } }; auto node = TestUtils::createNode(10, 10, 110, 110, [](RenderProperties& props, RecordingCanvas& canvas) { props.mutateLayerProperties().setType(LayerType::RenderLayer); SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }); OffscreenBuffer** layerHandle = node->getLayerHandle(); // create RenderNode's layer here in same way prepareTree would OffscreenBuffer layer(renderThread.renderState(), Caches::getInstance(), 100, 100); *layerHandle = &layer; auto syncedNode = TestUtils::getSyncedNode(node); // only enqueue partial damage LayerUpdateQueue layerUpdateQueue; // Note: enqueue damage post-sync, so bounds are valid layerUpdateQueue.enqueueLayerWithDamage(node.get(), Rect(25, 25, 75, 75)); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferLayers(layerUpdateQueue); frameBuilder.deferRenderNode(*syncedNode); HwLayerSimpleTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(6, renderer.getIndex()); // clean up layer pointer, so we can safely destruct RenderNode *layerHandle = nullptr; } RENDERTHREAD_TEST(FrameBuilder, hwLayer_complex) { /* parentLayer { greyRect, saveLayer { childLayer { whiteRect } } } will play back as: * - startRepaintLayer(child), rect(grey), endLayer * - startTemporaryLayer, drawLayer(child), endLayer * - startRepaintLayer(parent), rect(white), drawLayer(saveLayer), endLayer * - startFrame, drawLayer(parent), endLayerb */ class HwLayerComplexTestRenderer : public TestRendererBase { public: OffscreenBuffer* startTemporaryLayer(uint32_t width, uint32_t height) { EXPECT_EQ(3, mIndex++); // savelayer first return (OffscreenBuffer*)0xabcd; } void startRepaintLayer(OffscreenBuffer* offscreenBuffer, const Rect& repaintRect) override { int index = mIndex++; if (index == 0) { // starting inner layer EXPECT_EQ(100u, offscreenBuffer->viewportWidth); EXPECT_EQ(100u, offscreenBuffer->viewportHeight); } else if (index == 6) { // starting outer layer EXPECT_EQ(200u, offscreenBuffer->viewportWidth); EXPECT_EQ(200u, offscreenBuffer->viewportHeight); } else { ADD_FAILURE(); } } void onRectOp(const RectOp& op, const BakedOpState& state) override { int index = mIndex++; if (index == 1) { // inner layer's rect (white) EXPECT_EQ(SK_ColorWHITE, op.paint->getColor()); } else if (index == 7) { // outer layer's rect (grey) EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor()); } else { ADD_FAILURE(); } } void endLayer() override { int index = mIndex++; EXPECT_TRUE(index == 2 || index == 5 || index == 9); } void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) override { EXPECT_EQ(10, mIndex++); } void onLayerOp(const LayerOp& op, const BakedOpState& state) override { OffscreenBuffer* layer = *op.layerHandle; int index = mIndex++; if (index == 4) { EXPECT_EQ(100u, layer->viewportWidth); EXPECT_EQ(100u, layer->viewportHeight); } else if (index == 8) { EXPECT_EQ((OffscreenBuffer*)0xabcd, *op.layerHandle); } else if (index == 11) { EXPECT_EQ(200u, layer->viewportWidth); EXPECT_EQ(200u, layer->viewportHeight); } else { ADD_FAILURE(); } } void endFrame(const Rect& repaintRect) override { EXPECT_EQ(12, mIndex++); } void recycleTemporaryLayer(OffscreenBuffer* offscreenBuffer) override { EXPECT_EQ(13, mIndex++); } }; auto child = TestUtils::createNode(50, 50, 150, 150, [](RenderProperties& props, RecordingCanvas& canvas) { props.mutateLayerProperties().setType(LayerType::RenderLayer); SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }); OffscreenBuffer childLayer(renderThread.renderState(), Caches::getInstance(), 100, 100); *(child->getLayerHandle()) = &childLayer; RenderNode* childPtr = child.get(); auto parent = TestUtils::createNode(0, 0, 200, 200, [childPtr](RenderProperties& props, RecordingCanvas& canvas) { props.mutateLayerProperties().setType(LayerType::RenderLayer); SkPaint paint; paint.setColor(SK_ColorDKGRAY); canvas.drawRect(0, 0, 200, 200, paint); canvas.saveLayerAlpha(50, 50, 150, 150, 128, SaveFlags::ClipToLayer); canvas.drawRenderNode(childPtr); canvas.restore(); }); OffscreenBuffer parentLayer(renderThread.renderState(), Caches::getInstance(), 200, 200); *(parent->getLayerHandle()) = &parentLayer; auto syncedNode = TestUtils::getSyncedNode(parent); LayerUpdateQueue layerUpdateQueue; // Note: enqueue damage post-sync, so bounds are valid layerUpdateQueue.enqueueLayerWithDamage(child.get(), Rect(100, 100)); layerUpdateQueue.enqueueLayerWithDamage(parent.get(), Rect(200, 200)); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferLayers(layerUpdateQueue); frameBuilder.deferRenderNode(*syncedNode); HwLayerComplexTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(14, renderer.getIndex()); // clean up layer pointers, so we can safely destruct RenderNodes *(child->getLayerHandle()) = nullptr; *(parent->getLayerHandle()) = nullptr; } RENDERTHREAD_TEST(FrameBuilder, buildLayer) { class BuildLayerTestRenderer : public TestRendererBase { public: void startRepaintLayer(OffscreenBuffer* offscreenBuffer, const Rect& repaintRect) override { EXPECT_EQ(0, mIndex++); EXPECT_EQ(100u, offscreenBuffer->viewportWidth); EXPECT_EQ(100u, offscreenBuffer->viewportHeight); EXPECT_EQ(Rect(25, 25, 75, 75), repaintRect); } void onColorOp(const ColorOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); EXPECT_TRUE(state.computedState.transform.isIdentity()) << "Transform should be reset within layer"; EXPECT_EQ(Rect(25, 25, 75, 75), state.computedState.clipRect()) << "Damage rect should be used to clip layer content"; } void endLayer() override { EXPECT_EQ(2, mIndex++); } void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) override { ADD_FAILURE() << "Primary frame draw not expected in this test"; } void endFrame(const Rect& repaintRect) override { ADD_FAILURE() << "Primary frame draw not expected in this test"; } }; auto node = TestUtils::createNode(10, 10, 110, 110, [](RenderProperties& props, RecordingCanvas& canvas) { props.mutateLayerProperties().setType(LayerType::RenderLayer); canvas.drawColor(SK_ColorWHITE, SkXfermode::Mode::kSrcOver_Mode); }); OffscreenBuffer** layerHandle = node->getLayerHandle(); // create RenderNode's layer here in same way prepareTree would OffscreenBuffer layer(renderThread.renderState(), Caches::getInstance(), 100, 100); *layerHandle = &layer; TestUtils::syncHierarchyPropertiesAndDisplayList(node); // only enqueue partial damage LayerUpdateQueue layerUpdateQueue; // Note: enqueue damage post-sync, so bounds are valid layerUpdateQueue.enqueueLayerWithDamage(node.get(), Rect(25, 25, 75, 75)); // Draw, but pass empty node list, so no work is done for primary frame FrameBuilder frameBuilder(layerUpdateQueue, sLightGeometry, Caches::getInstance()); BuildLayerTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(3, renderer.getIndex()); // clean up layer pointer, so we can safely destruct RenderNode *layerHandle = nullptr; } static void drawOrderedRect(RecordingCanvas* canvas, uint8_t expectedDrawOrder) { SkPaint paint; paint.setColor(SkColorSetARGB(256, 0, 0, expectedDrawOrder)); // order put in blue channel canvas->drawRect(0, 0, 100, 100, paint); } static void drawOrderedNode(RecordingCanvas* canvas, uint8_t expectedDrawOrder, float z) { auto node = TestUtils::createNode(0, 0, 100, 100, [expectedDrawOrder](RenderProperties& props, RecordingCanvas& canvas) { drawOrderedRect(&canvas, expectedDrawOrder); }); node->mutateStagingProperties().setTranslationZ(z); node->setPropertyFieldsDirty(RenderNode::TRANSLATION_Z); canvas->drawRenderNode(node.get()); // canvas takes reference/sole ownership } RENDERTHREAD_TEST(FrameBuilder, zReorder) { class ZReorderTestRenderer : public TestRendererBase { public: void onRectOp(const RectOp& op, const BakedOpState& state) override { int expectedOrder = SkColorGetB(op.paint->getColor()); // extract order from blue channel EXPECT_EQ(expectedOrder, mIndex++) << "An op was drawn out of order"; } }; auto parent = TestUtils::createNode(0, 0, 100, 100, [](RenderProperties& props, RecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, 10.0f); // in reorder=false at this point, so played inorder drawOrderedRect(&canvas, 1); canvas.insertReorderBarrier(true); drawOrderedNode(&canvas, 6, 2.0f); drawOrderedRect(&canvas, 3); drawOrderedNode(&canvas, 4, 0.0f); drawOrderedRect(&canvas, 5); drawOrderedNode(&canvas, 2, -2.0f); drawOrderedNode(&canvas, 7, 2.0f); canvas.insertReorderBarrier(false); drawOrderedRect(&canvas, 8); drawOrderedNode(&canvas, 9, -10.0f); // in reorder=false at this point, so played inorder }); FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(parent)); ZReorderTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(10, renderer.getIndex()); }; RENDERTHREAD_TEST(FrameBuilder, projectionReorder) { static const int scrollX = 5; static const int scrollY = 10; class ProjectionReorderTestRenderer : public TestRendererBase { public: void onRectOp(const RectOp& op, const BakedOpState& state) override { const int index = mIndex++; Matrix4 expectedMatrix; switch (index) { case 0: EXPECT_EQ(Rect(100, 100), op.unmappedBounds); EXPECT_EQ(SK_ColorWHITE, op.paint->getColor()); expectedMatrix.loadIdentity(); 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 - 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_EQ(expectedMatrix, state.computedState.transform); } }; /** * Construct a tree of nodes, where the root (A) has a receiver background (B), and a child (C) * with a projecting child (P) of its own. P would normally draw between B and C's "background" * draw, but because it is projected backwards, it's drawn in between B and C. * * The parent is scrolled by scrollX/scrollY, but this does not affect the background * (which isn't affected by scroll). */ auto receiverBackground = TestUtils::createNode(0, 0, 100, 100, [](RenderProperties& properties, RecordingCanvas& canvas) { properties.setProjectionReceiver(true); // scroll doesn't apply to background, so undone via translationX/Y // NOTE: translationX/Y only! no other transform properties may be set for a proj receiver! properties.setTranslationX(scrollX); properties.setTranslationY(scrollY); SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }); auto projectingRipple = TestUtils::createNode(50, 0, 100, 50, [](RenderProperties& properties, RecordingCanvas& canvas) { properties.setProjectBackwards(true); properties.setClipToBounds(false); SkPaint paint; paint.setColor(SK_ColorDKGRAY); canvas.drawRect(-10, -10, 60, 60, paint); }); auto child = TestUtils::createNode(0, 50, 100, 100, [&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) { SkPaint paint; paint.setColor(SK_ColorBLUE); canvas.drawRect(0, 0, 100, 50, paint); canvas.drawRenderNode(projectingRipple.get()); }); auto parent = TestUtils::createNode(0, 0, 100, 100, [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) { // 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()); canvas.drawRenderNode(child.get()); canvas.restore(); }); FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(parent)); ProjectionReorderTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); 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 syncedNode = TestUtils::getSyncedNode(parent); LayerUpdateQueue layerUpdateQueue; // Note: enqueue damage post-sync, so bounds are valid layerUpdateQueue.enqueueLayerWithDamage(child.get(), Rect(200, 200)); FrameBuilder frameBuilder(SkRect::MakeWH(400, 400), 400, 400, sLightGeometry, Caches::getInstance()); frameBuilder.deferLayers(layerUpdateQueue); frameBuilder.deferRenderNode(*syncedNode); ProjectionHwLayerTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(6, renderer.getIndex()); // clean up layer pointer, so we can safely destruct RenderNode *layerHandle = nullptr; } RENDERTHREAD_TEST(FrameBuilder, projectionChildScroll) { static const int scrollX = 500000; static const int scrollY = 0; class ProjectionChildScrollTestRenderer : public TestRendererBase { public: void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); EXPECT_TRUE(state.computedState.transform.isIdentity()); } void onOvalOp(const OvalOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); ASSERT_NE(nullptr, state.computedState.clipState); ASSERT_EQ(ClipMode::Rectangle, state.computedState.clipState->mode); ASSERT_EQ(Rect(400, 400), state.computedState.clipState->rect); EXPECT_TRUE(state.computedState.transform.isIdentity()); } }; auto receiverBackground = TestUtils::createNode(0, 0, 400, 400, [](RenderProperties& properties, RecordingCanvas& canvas) { properties.setProjectionReceiver(true); canvas.drawRect(0, 0, 400, 400, SkPaint()); }); auto projectingRipple = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& properties, RecordingCanvas& canvas) { // scroll doesn't apply to background, so undone via translationX/Y // NOTE: translationX/Y only! no other transform properties may be set for a proj receiver! properties.setTranslationX(scrollX); properties.setTranslationY(scrollY); properties.setProjectBackwards(true); properties.setClipToBounds(false); canvas.drawOval(0, 0, 200, 200, SkPaint()); }); auto child = TestUtils::createNode(0, 0, 400, 400, [&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) { // Record time clip will be ignored by projectee canvas.clipRect(100, 100, 300, 300, SkRegion::kIntersect_Op); canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally) canvas.drawRenderNode(projectingRipple.get()); }); auto parent = TestUtils::createNode(0, 0, 400, 400, [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) { canvas.drawRenderNode(receiverBackground.get()); canvas.drawRenderNode(child.get()); }); FrameBuilder frameBuilder(SkRect::MakeWH(400, 400), 400, 400, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(parent)); ProjectionChildScrollTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2, renderer.getIndex()); } // creates a 100x100 shadow casting node with provided translationZ static sp<RenderNode> createWhiteRectShadowCaster(float translationZ) { return TestUtils::createNode(0, 0, 100, 100, [translationZ](RenderProperties& properties, RecordingCanvas& canvas) { properties.setTranslationZ(translationZ); properties.mutableOutline().setRoundRect(0, 0, 100, 100, 0.0f, 1.0f); SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }); } RENDERTHREAD_TEST(FrameBuilder, shadow) { class ShadowTestRenderer : public TestRendererBase { public: void onShadowOp(const ShadowOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); EXPECT_FLOAT_EQ(1.0f, op.casterAlpha); EXPECT_TRUE(op.shadowTask->casterPerimeter.isRect(nullptr)); EXPECT_MATRIX_APPROX_EQ(Matrix4::identity(), op.shadowTask->transformXY); Matrix4 expectedZ; expectedZ.loadTranslate(0, 0, 5); EXPECT_MATRIX_APPROX_EQ(expectedZ, op.shadowTask->transformZ); } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); } }; auto parent = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.insertReorderBarrier(true); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get()); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(parent)); ShadowTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, shadowSaveLayer) { class ShadowSaveLayerTestRenderer : public TestRendererBase { public: OffscreenBuffer* startTemporaryLayer(uint32_t width, uint32_t height) override { EXPECT_EQ(0, mIndex++); return nullptr; } void onShadowOp(const ShadowOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); EXPECT_FLOAT_EQ(50, op.shadowTask->lightCenter.x); EXPECT_FLOAT_EQ(40, op.shadowTask->lightCenter.y); } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(2, mIndex++); } void endLayer() override { EXPECT_EQ(3, mIndex++); } void onLayerOp(const LayerOp& op, const BakedOpState& state) override { EXPECT_EQ(4, mIndex++); } void recycleTemporaryLayer(OffscreenBuffer* offscreenBuffer) override { EXPECT_EQ(5, mIndex++); } }; auto parent = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { // save/restore outside of reorderBarrier, so they don't get moved out of place canvas.translate(20, 10); int count = canvas.saveLayerAlpha(30, 50, 130, 150, 128, SaveFlags::ClipToLayer); canvas.insertReorderBarrier(true); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get()); canvas.insertReorderBarrier(false); canvas.restoreToCount(count); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, (FrameBuilder::LightGeometry) {{ 100, 100, 100 }, 50}, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(parent)); ShadowSaveLayerTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(6, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, shadowHwLayer) { class ShadowHwLayerTestRenderer : public TestRendererBase { public: void startRepaintLayer(OffscreenBuffer* offscreenBuffer, const Rect& repaintRect) override { EXPECT_EQ(0, mIndex++); } void onShadowOp(const ShadowOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); EXPECT_FLOAT_EQ(50, op.shadowTask->lightCenter.x); EXPECT_FLOAT_EQ(40, op.shadowTask->lightCenter.y); EXPECT_FLOAT_EQ(30, op.shadowTask->lightRadius); } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(2, mIndex++); } void endLayer() override { EXPECT_EQ(3, mIndex++); } void onLayerOp(const LayerOp& op, const BakedOpState& state) override { EXPECT_EQ(4, mIndex++); } }; auto parent = TestUtils::createNode(50, 60, 150, 160, [](RenderProperties& props, RecordingCanvas& canvas) { props.mutateLayerProperties().setType(LayerType::RenderLayer); canvas.insertReorderBarrier(true); canvas.save(SaveFlags::MatrixClip); canvas.translate(20, 10); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get()); canvas.restore(); }); OffscreenBuffer** layerHandle = parent->getLayerHandle(); // create RenderNode's layer here in same way prepareTree would, setting windowTransform OffscreenBuffer layer(renderThread.renderState(), Caches::getInstance(), 100, 100); Matrix4 windowTransform; windowTransform.loadTranslate(50, 60, 0); // total transform of layer's origin layer.setWindowTransform(windowTransform); *layerHandle = &layer; auto syncedNode = TestUtils::getSyncedNode(parent); LayerUpdateQueue layerUpdateQueue; // Note: enqueue damage post-sync, so bounds are valid layerUpdateQueue.enqueueLayerWithDamage(parent.get(), Rect(100, 100)); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, (FrameBuilder::LightGeometry) {{ 100, 100, 100 }, 30}, Caches::getInstance()); frameBuilder.deferLayers(layerUpdateQueue); frameBuilder.deferRenderNode(*syncedNode); ShadowHwLayerTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(5, renderer.getIndex()); // clean up layer pointer, so we can safely destruct RenderNode *layerHandle = nullptr; } RENDERTHREAD_TEST(FrameBuilder, shadowLayering) { class ShadowLayeringTestRenderer : public TestRendererBase { public: void onShadowOp(const ShadowOp& op, const BakedOpState& state) override { int index = mIndex++; EXPECT_TRUE(index == 0 || index == 1); } void onRectOp(const RectOp& op, const BakedOpState& state) override { int index = mIndex++; EXPECT_TRUE(index == 2 || index == 3); } }; auto parent = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.insertReorderBarrier(true); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get()); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0001f).get()); }); FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, (FrameBuilder::LightGeometry) {{ 100, 100, 100 }, 50}, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(parent)); ShadowLayeringTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(4, renderer.getIndex()); } RENDERTHREAD_TEST(FrameBuilder, shadowClipping) { class ShadowClippingTestRenderer : public TestRendererBase { public: void onShadowOp(const ShadowOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); EXPECT_EQ(Rect(25, 25, 75, 75), state.computedState.clipState->rect) << "Shadow must respect pre-barrier canvas clip value."; } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); } }; auto parent = TestUtils::createNode(0, 0, 100, 100, [](RenderProperties& props, RecordingCanvas& canvas) { // Apply a clip before the reorder barrier/shadow casting child is drawn. // This clip must be applied to the shadow cast by the child. canvas.clipRect(25, 25, 75, 75, SkRegion::kIntersect_Op); canvas.insertReorderBarrier(true); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get()); }); FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 100, 100, (FrameBuilder::LightGeometry) {{ 100, 100, 100 }, 50}, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(parent)); ShadowClippingTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2, renderer.getIndex()); } static void testProperty(std::function<void(RenderProperties&)> propSetupCallback, std::function<void(const RectOp&, const BakedOpState&)> opValidateCallback) { class PropertyTestRenderer : public TestRendererBase { public: PropertyTestRenderer(std::function<void(const RectOp&, const BakedOpState&)> callback) : mCallback(callback) {} void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(mIndex++, 0); mCallback(op, state); } std::function<void(const RectOp&, const BakedOpState&)> mCallback; }; auto node = TestUtils::createNode(0, 0, 100, 100, [propSetupCallback](RenderProperties& props, RecordingCanvas& canvas) { propSetupCallback(props); SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }); FrameBuilder frameBuilder(SkRect::MakeWH(100, 100), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); PropertyTestRenderer renderer(opValidateCallback); frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()) << "Should have seen one op"; } RENDERTHREAD_TEST(FrameBuilder, renderPropOverlappingRenderingAlpha) { testProperty([](RenderProperties& properties) { properties.setAlpha(0.5f); properties.setHasOverlappingRendering(false); }, [](const RectOp& op, const BakedOpState& state) { EXPECT_EQ(0.5f, state.alpha) << "Alpha should be applied directly to op"; }); } RENDERTHREAD_TEST(FrameBuilder, renderPropClipping) { testProperty([](RenderProperties& properties) { properties.setClipToBounds(true); properties.setClipBounds(Rect(10, 20, 300, 400)); }, [](const RectOp& op, const BakedOpState& state) { EXPECT_EQ(Rect(10, 20, 100, 100), state.computedState.clippedBounds) << "Clip rect should be intersection of node bounds and clip bounds"; }); } RENDERTHREAD_TEST(FrameBuilder, renderPropRevealClip) { testProperty([](RenderProperties& properties) { properties.mutableRevealClip().set(true, 50, 50, 25); }, [](const RectOp& op, const BakedOpState& state) { ASSERT_NE(nullptr, state.roundRectClipState); EXPECT_TRUE(state.roundRectClipState->highPriority); EXPECT_EQ(25, state.roundRectClipState->radius); EXPECT_EQ(Rect(50, 50, 50, 50), state.roundRectClipState->innerRect); }); } RENDERTHREAD_TEST(FrameBuilder, renderPropOutlineClip) { testProperty([](RenderProperties& properties) { properties.mutableOutline().setShouldClip(true); properties.mutableOutline().setRoundRect(10, 20, 30, 40, 5.0f, 0.5f); }, [](const RectOp& op, const BakedOpState& state) { ASSERT_NE(nullptr, state.roundRectClipState); EXPECT_FALSE(state.roundRectClipState->highPriority); EXPECT_EQ(5, state.roundRectClipState->radius); EXPECT_EQ(Rect(15, 25, 25, 35), state.roundRectClipState->innerRect); }); } RENDERTHREAD_TEST(FrameBuilder, renderPropTransform) { testProperty([](RenderProperties& properties) { properties.setLeftTopRightBottom(10, 10, 110, 110); SkMatrix staticMatrix = SkMatrix::MakeScale(1.2f, 1.2f); properties.setStaticMatrix(&staticMatrix); // ignored, since static overrides animation SkMatrix animationMatrix = SkMatrix::MakeTrans(15, 15); properties.setAnimationMatrix(&animationMatrix); properties.setTranslationX(10); properties.setTranslationY(20); properties.setScaleX(0.5f); properties.setScaleY(0.7f); }, [](const RectOp& op, const BakedOpState& state) { Matrix4 matrix; matrix.loadTranslate(10, 10, 0); // left, top matrix.scale(1.2f, 1.2f, 1); // static matrix // ignore animation matrix, since static overrides it // translation xy matrix.translate(10, 20); // scale xy (from default pivot - center) matrix.translate(50, 50); matrix.scale(0.5f, 0.7f, 1); matrix.translate(-50, -50); EXPECT_MATRIX_APPROX_EQ(matrix, state.computedState.transform) << "Op draw matrix must match expected combination of transformation properties"; }); } struct SaveLayerAlphaData { uint32_t layerWidth = 0; uint32_t layerHeight = 0; Rect rectClippedBounds; Matrix4 rectMatrix; Matrix4 drawLayerMatrix; }; /** * Constructs a view to hit the temporary layer alpha property implementation: * a) 0 < alpha < 1 * b) too big for layer (larger than maxTextureSize) * c) overlapping rendering content * returning observed data about layer size and content clip/transform. * * Used to validate clipping behavior of temporary layer, where requested layer size is reduced * (for efficiency, and to fit in layer size constraints) based on parent clip. */ void testSaveLayerAlphaClip(SaveLayerAlphaData* outObservedData, std::function<void(RenderProperties&)> propSetupCallback) { class SaveLayerAlphaClipTestRenderer : public TestRendererBase { public: SaveLayerAlphaClipTestRenderer(SaveLayerAlphaData* outData) : mOutData(outData) {} OffscreenBuffer* startTemporaryLayer(uint32_t width, uint32_t height) override { EXPECT_EQ(0, mIndex++); mOutData->layerWidth = width; mOutData->layerHeight = height; return nullptr; } void onRectOp(const RectOp& op, const BakedOpState& state) override { EXPECT_EQ(1, mIndex++); mOutData->rectClippedBounds = state.computedState.clippedBounds; mOutData->rectMatrix = state.computedState.transform; } void endLayer() override { EXPECT_EQ(2, mIndex++); } void onLayerOp(const LayerOp& op, const BakedOpState& state) override { EXPECT_EQ(3, mIndex++); mOutData->drawLayerMatrix = state.computedState.transform; } void recycleTemporaryLayer(OffscreenBuffer* offscreenBuffer) override { EXPECT_EQ(4, mIndex++); } private: SaveLayerAlphaData* mOutData; }; ASSERT_GT(10000, DeviceInfo::get()->maxTextureSize()) << "Node must be bigger than max texture size to exercise saveLayer codepath"; auto node = TestUtils::createNode(0, 0, 10000, 10000, [&propSetupCallback](RenderProperties& properties, RecordingCanvas& canvas) { properties.setHasOverlappingRendering(true); properties.setAlpha(0.5f); // force saveLayer, since too big for HW layer // apply other properties propSetupCallback(properties); SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 10000, 10000, paint); }); auto syncedNode = TestUtils::getSyncedNode(node); // sync before querying height FrameBuilder frameBuilder(SkRect::MakeWH(200, 200), 200, 200, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*syncedNode); SaveLayerAlphaClipTestRenderer renderer(outObservedData); frameBuilder.replayBakedOps<TestDispatcher>(renderer); // assert, since output won't be valid if we haven't seen a save layer triggered ASSERT_EQ(5, renderer.getIndex()) << "Test must trigger saveLayer alpha behavior."; } RENDERTHREAD_TEST(FrameBuilder, renderPropSaveLayerAlphaClipBig) { SaveLayerAlphaData observedData; testSaveLayerAlphaClip(&observedData, [](RenderProperties& properties) { properties.setTranslationX(10); // offset rendering content properties.setTranslationY(-2000); // offset rendering content }); EXPECT_EQ(190u, observedData.layerWidth); EXPECT_EQ(200u, observedData.layerHeight); EXPECT_EQ(Rect(190, 200), observedData.rectClippedBounds) << "expect content to be clipped to screen area"; Matrix4 expected; expected.loadTranslate(0, -2000, 0); EXPECT_MATRIX_APPROX_EQ(expected, observedData.rectMatrix) << "expect content to be translated as part of being clipped"; expected.loadTranslate(10, 0, 0); EXPECT_MATRIX_APPROX_EQ(expected, observedData.drawLayerMatrix) << "expect drawLayer to be translated as part of being clipped"; } RENDERTHREAD_TEST(FrameBuilder, renderPropSaveLayerAlphaRotate) { SaveLayerAlphaData observedData; testSaveLayerAlphaClip(&observedData, [](RenderProperties& properties) { // Translate and rotate the view so that the only visible part is the top left corner of // the view. It will form an isosceles right triangle with a long side length of 200 at the // bottom of the viewport. properties.setTranslationX(100); properties.setTranslationY(100); properties.setPivotX(0); properties.setPivotY(0); properties.setRotation(45); }); // ceil(sqrt(2) / 2 * 200) = 142 EXPECT_EQ(142u, observedData.layerWidth); EXPECT_EQ(142u, observedData.layerHeight); EXPECT_EQ(Rect(142, 142), observedData.rectClippedBounds); EXPECT_MATRIX_APPROX_EQ(Matrix4::identity(), observedData.rectMatrix); } RENDERTHREAD_TEST(FrameBuilder, renderPropSaveLayerAlphaScale) { SaveLayerAlphaData observedData; testSaveLayerAlphaClip(&observedData, [](RenderProperties& properties) { properties.setPivotX(0); properties.setPivotY(0); properties.setScaleX(2); properties.setScaleY(0.5f); }); EXPECT_EQ(100u, observedData.layerWidth); EXPECT_EQ(400u, observedData.layerHeight); EXPECT_EQ(Rect(100, 400), observedData.rectClippedBounds); EXPECT_MATRIX_APPROX_EQ(Matrix4::identity(), observedData.rectMatrix); } RENDERTHREAD_TEST(FrameBuilder, clip_replace) { class ClipReplaceTestRenderer : public TestRendererBase { public: void onColorOp(const ColorOp& op, const BakedOpState& state) override { EXPECT_EQ(0, mIndex++); EXPECT_TRUE(op.localClip->intersectWithRoot); EXPECT_EQ(Rect(20, 10, 30, 40), state.computedState.clipState->rect) << "Expect resolved clip to be intersection of viewport clip and clip op"; } }; auto node = TestUtils::createNode(20, 20, 30, 30, [](RenderProperties& props, RecordingCanvas& canvas) { canvas.clipRect(0, -20, 10, 30, SkRegion::kReplace_Op); canvas.drawColor(SK_ColorWHITE, SkXfermode::Mode::kSrcOver_Mode); }); FrameBuilder frameBuilder(SkRect::MakeLTRB(10, 10, 40, 40), 50, 50, sLightGeometry, Caches::getInstance()); frameBuilder.deferRenderNode(*TestUtils::getSyncedNode(node)); ClipReplaceTestRenderer renderer; frameBuilder.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(1, renderer.getIndex()); } } // namespace uirenderer } // namespace android