diff --git a/engine/src/flutter/display_list/BUILD.gn b/engine/src/flutter/display_list/BUILD.gn index d4ac72b67d..ce48a7a359 100644 --- a/engine/src/flutter/display_list/BUILD.gn +++ b/engine/src/flutter/display_list/BUILD.gn @@ -130,6 +130,7 @@ if (enable_unittests) { ":display_list", ":display_list_fixtures", "//flutter/display_list/testing:display_list_testing", + "//flutter/impeller/typographer/backends/skia:typographer_skia_backend", "//flutter/testing", "//flutter/testing:skia", ] diff --git a/engine/src/flutter/display_list/display_list.cc b/engine/src/flutter/display_list/display_list.cc index 0937c70f15..ccfd97e7a9 100644 --- a/engine/src/flutter/display_list/display_list.cc +++ b/engine/src/flutter/display_list/display_list.cc @@ -26,6 +26,7 @@ DisplayList::DisplayList() is_ui_thread_safe_(true), modifies_transparent_black_(false), root_has_backdrop_filter_(false), + root_is_unbounded_(false), max_root_blend_mode_(DlBlendMode::kClear) {} DisplayList::DisplayList(DisplayListStorage&& storage, @@ -40,6 +41,7 @@ DisplayList::DisplayList(DisplayListStorage&& storage, bool modifies_transparent_black, DlBlendMode max_root_blend_mode, bool root_has_backdrop_filter, + bool root_is_unbounded, sk_sp rtree) : storage_(std::move(storage)), byte_count_(byte_count), @@ -53,6 +55,7 @@ DisplayList::DisplayList(DisplayListStorage&& storage, is_ui_thread_safe_(is_ui_thread_safe), modifies_transparent_black_(modifies_transparent_black), root_has_backdrop_filter_(root_has_backdrop_filter), + root_is_unbounded_(root_is_unbounded), max_root_blend_mode_(max_root_blend_mode), rtree_(std::move(rtree)) {} diff --git a/engine/src/flutter/display_list/display_list.h b/engine/src/flutter/display_list/display_list.h index bb67c435cc..5c3ef29625 100644 --- a/engine/src/flutter/display_list/display_list.h +++ b/engine/src/flutter/display_list/display_list.h @@ -216,6 +216,13 @@ class SaveLayerOptions { return options; } + bool content_is_unbounded() const { return fContentIsUnbounded; } + SaveLayerOptions with_content_is_unbounded() const { + SaveLayerOptions options(this); + options.fContentIsUnbounded = true; + return options; + } + SaveLayerOptions& operator=(const SaveLayerOptions& other) { flags_ = other.flags_; return *this; @@ -235,6 +242,7 @@ class SaveLayerOptions { unsigned fBoundsFromCaller : 1; unsigned fContentIsClipped : 1; unsigned fHasBackdropFilter : 1; + unsigned fContentIsUnbounded : 1; }; uint32_t flags_; }; @@ -331,6 +339,18 @@ class DisplayList : public SkRefCnt { /// be required for the backdrop filter to do its work. bool root_has_backdrop_filter() const { return root_has_backdrop_filter_; } + /// @brief Indicates if a rendering operation at the root level of the + /// DisplayList had an unbounded result, not otherwise limited by + /// a clip operation. + /// + /// This condition can occur in a number of situations. The most common + /// situation is when there is a drawPaint or drawColor rendering + /// operation which fills out the entire drawable surface unless it is + /// bounded by a clip. Other situations include an operation rendered + /// through an ImageFilter that cannot compute the resulting bounds or + /// when an unclipped backdrop filter is applied by a save layer. + bool root_is_unbounded() const { return root_is_unbounded_; } + /// @brief Indicates the maximum DlBlendMode used on any rendering op /// in the root surface of the DisplayList. /// @@ -353,6 +373,7 @@ class DisplayList : public SkRefCnt { bool modifies_transparent_black, DlBlendMode max_root_blend_mode, bool root_has_backdrop_filter, + bool root_is_unbounded, sk_sp rtree); static uint32_t next_unique_id(); @@ -375,6 +396,7 @@ class DisplayList : public SkRefCnt { const bool is_ui_thread_safe_; const bool modifies_transparent_black_; const bool root_has_backdrop_filter_; + const bool root_is_unbounded_; const DlBlendMode max_root_blend_mode_; const sk_sp rtree_; diff --git a/engine/src/flutter/display_list/display_list_unittests.cc b/engine/src/flutter/display_list/display_list_unittests.cc index d25f729ac3..96a16dbd4d 100644 --- a/engine/src/flutter/display_list/display_list_unittests.cc +++ b/engine/src/flutter/display_list/display_list_unittests.cc @@ -18,6 +18,7 @@ #include "flutter/display_list/utils/dl_receiver_utils.h" #include "flutter/fml/logging.h" #include "flutter/fml/math.h" +#include "flutter/impeller/typographer/backends/skia/text_frame_skia.h" #include "flutter/testing/assertions_skia.h" #include "flutter/testing/display_list_testing.h" #include "flutter/testing/testing.h" @@ -1309,28 +1310,80 @@ TEST_F(DisplayListTest, SaveLayerBoundsSnapshotsImageFilter) { EXPECT_EQ(bounds, SkRect::MakeLTRB(50, 50, 100, 100)); } +#define SAVE_LAYER_EXPECTOR(name) SaveLayerExpector name(__FILE__, __LINE__) + +using SaveLayerOptionsTester = + std::function; + +struct SaveLayerExpectations { + SaveLayerExpectations() {} + + // NOLINTNEXTLINE(google-explicit-constructor) + SaveLayerExpectations(const SaveLayerOptions& o) : options(o) {} + // NOLINTNEXTLINE(google-explicit-constructor) + SaveLayerExpectations(const SaveLayerOptionsTester& t) : tester(t) {} + // NOLINTNEXTLINE(google-explicit-constructor) + SaveLayerExpectations(DlBlendMode mode) : max_blend_mode(mode) {} + + std::optional options; + std::optional tester; + std::optional max_blend_mode; +}; + +::std::ostream& operator<<(::std::ostream& os, + const SaveLayerExpectations& expect) { + os << "SaveLayerExpectation("; + if (expect.options.has_value()) { + os << "options: " << expect.options.value(); + } + if (expect.tester.has_value()) { + os << "option tester: " << &expect.tester.value(); + } + if (expect.max_blend_mode.has_value()) { + os << "max_blend: " << expect.max_blend_mode.value(); + } + os << ")"; + return os; +} + class SaveLayerExpector : public virtual DlOpReceiver, public IgnoreAttributeDispatchHelper, public IgnoreClipDispatchHelper, public IgnoreTransformDispatchHelper, public IgnoreDrawDispatchHelper { public: - struct Expectations { - // NOLINTNEXTLINE(google-explicit-constructor) - Expectations(SaveLayerOptions o) : options(o) {} - // NOLINTNEXTLINE(google-explicit-constructor) - Expectations(DlBlendMode mode) : max_blend_mode(mode) {} + SaveLayerExpector(const std::string& file, int line) + : file_(file), line_(line), detail_("") {} - std::optional options; - std::optional max_blend_mode; - }; - - explicit SaveLayerExpector(const Expectations& expected) { - expected_.push_back(expected); + ~SaveLayerExpector() { // + EXPECT_EQ(save_layer_count_, expected_.size()) << label(); + while (save_layer_count_ < expected_.size()) { + auto expect = expected_[save_layer_count_]; + FML_LOG(ERROR) << "leftover expectation[" << save_layer_count_ + << "] = " << expect; + save_layer_count_++; + } } - explicit SaveLayerExpector(std::vector expected) - : expected_(std::move(expected)) {} + SaveLayerExpector& addDetail(const std::string& detail) { + detail_ = detail; + return *this; + } + + SaveLayerExpector& addExpectation(const SaveLayerExpectations& expected) { + expected_.push_back(expected); + return *this; + } + + SaveLayerExpector& addExpectation(const SaveLayerOptionsTester& tester) { + expected_.push_back(SaveLayerExpectations(tester)); + return *this; + } + + SaveLayerExpector& addOpenExpectation() { + expected_.push_back(SaveLayerExpectations()); + return *this; + } void saveLayer(const SkRect& bounds, const SaveLayerOptions options, @@ -1343,15 +1396,19 @@ class SaveLayerExpector : public virtual DlOpReceiver, uint32_t total_content_depth, DlBlendMode max_content_blend_mode, const DlImageFilter* backdrop = nullptr) { - auto label = "index " + std::to_string(save_layer_count_); - ASSERT_LT(save_layer_count_, expected_.size()); - auto expect = expected_[save_layer_count_++]; + ASSERT_LT(save_layer_count_, expected_.size()) << label(); + auto expect = expected_[save_layer_count_]; if (expect.options.has_value()) { - EXPECT_EQ(options, expect.options.value()) << label; + EXPECT_EQ(options, expect.options.value()) << label(); + } + if (expect.tester.has_value()) { + EXPECT_TRUE(expect.tester.value()(options)) << label(); } if (expect.max_blend_mode.has_value()) { - EXPECT_EQ(max_content_blend_mode, expect.max_blend_mode.value()) << label; + EXPECT_EQ(max_content_blend_mode, expect.max_blend_mode.value()) + << label(); } + save_layer_count_++; } bool all_expectations_checked() const { @@ -1359,14 +1416,29 @@ class SaveLayerExpector : public virtual DlOpReceiver, } private: - std::vector expected_; + // mutable allows the copy constructor to leave no expectations behind + mutable std::vector expected_; size_t save_layer_count_ = 0; + + const std::string file_; + const int line_; + std::string detail_; + + std::string label() { + std::string label = "at index " + std::to_string(save_layer_count_) + // + ", from " + file_ + // + ":" + std::to_string(line_); + if (detail_.length() > 0) { + label = label + " (" + detail_ + ")"; + } + return label; + } }; TEST_F(DisplayListTest, SaveLayerOneSimpleOpInheritsOpacity) { - SaveLayerOptions expected = - SaveLayerOptions::kWithAttributes.with_can_distribute_opacity(); - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation( + SaveLayerOptions::kWithAttributes.with_can_distribute_opacity()); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1380,9 +1452,9 @@ TEST_F(DisplayListTest, SaveLayerOneSimpleOpInheritsOpacity) { } TEST_F(DisplayListTest, SaveLayerNoAttributesInheritsOpacity) { - SaveLayerOptions expected = - SaveLayerOptions::kNoAttributes.with_can_distribute_opacity(); - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation( + SaveLayerOptions::kNoAttributes.with_can_distribute_opacity()); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1395,8 +1467,8 @@ TEST_F(DisplayListTest, SaveLayerNoAttributesInheritsOpacity) { } TEST_F(DisplayListTest, SaveLayerTwoOverlappingOpsDoesNotInheritOpacity) { - SaveLayerOptions expected = SaveLayerOptions::kWithAttributes; - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation(SaveLayerOptions::kWithAttributes); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1411,12 +1483,13 @@ TEST_F(DisplayListTest, SaveLayerTwoOverlappingOpsDoesNotInheritOpacity) { } TEST_F(DisplayListTest, NestedSaveLayersMightInheritOpacity) { - SaveLayerOptions expected1 = - SaveLayerOptions::kWithAttributes.with_can_distribute_opacity(); - SaveLayerOptions expected2 = SaveLayerOptions::kWithAttributes; - SaveLayerOptions expected3 = - SaveLayerOptions::kWithAttributes.with_can_distribute_opacity(); - SaveLayerExpector expector({expected1, expected2, expected3}); + SAVE_LAYER_EXPECTOR(expector); + expector // + .addExpectation( + SaveLayerOptions::kWithAttributes.with_can_distribute_opacity()) + .addExpectation(SaveLayerOptions::kWithAttributes) + .addExpectation( + SaveLayerOptions::kWithAttributes.with_can_distribute_opacity()); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1435,11 +1508,12 @@ TEST_F(DisplayListTest, NestedSaveLayersMightInheritOpacity) { } TEST_F(DisplayListTest, NestedSaveLayersCanBothSupportOpacityOptimization) { - SaveLayerOptions expected1 = - SaveLayerOptions::kWithAttributes.with_can_distribute_opacity(); - SaveLayerOptions expected2 = - SaveLayerOptions::kNoAttributes.with_can_distribute_opacity(); - SaveLayerExpector expector({expected1, expected2}); + SAVE_LAYER_EXPECTOR(expector); + expector // + .addExpectation( + SaveLayerOptions::kWithAttributes.with_can_distribute_opacity()) + .addExpectation( + SaveLayerOptions::kNoAttributes.with_can_distribute_opacity()); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1455,8 +1529,8 @@ TEST_F(DisplayListTest, NestedSaveLayersCanBothSupportOpacityOptimization) { } TEST_F(DisplayListTest, SaveLayerImageFilterDoesNotInheritOpacity) { - SaveLayerOptions expected = SaveLayerOptions::kWithAttributes; - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation(SaveLayerOptions::kWithAttributes); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1472,8 +1546,8 @@ TEST_F(DisplayListTest, SaveLayerImageFilterDoesNotInheritOpacity) { } TEST_F(DisplayListTest, SaveLayerColorFilterDoesNotInheritOpacity) { - SaveLayerOptions expected = SaveLayerOptions::kWithAttributes; - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation(SaveLayerOptions::kWithAttributes); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1489,8 +1563,8 @@ TEST_F(DisplayListTest, SaveLayerColorFilterDoesNotInheritOpacity) { } TEST_F(DisplayListTest, SaveLayerSrcBlendDoesNotInheritOpacity) { - SaveLayerOptions expected = SaveLayerOptions::kWithAttributes; - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation(SaveLayerOptions::kWithAttributes); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1506,9 +1580,9 @@ TEST_F(DisplayListTest, SaveLayerSrcBlendDoesNotInheritOpacity) { } TEST_F(DisplayListTest, SaveLayerImageFilterOnChildInheritsOpacity) { - SaveLayerOptions expected = - SaveLayerOptions::kWithAttributes.with_can_distribute_opacity(); - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation( + SaveLayerOptions::kWithAttributes.with_can_distribute_opacity()); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1523,8 +1597,8 @@ TEST_F(DisplayListTest, SaveLayerImageFilterOnChildInheritsOpacity) { } TEST_F(DisplayListTest, SaveLayerColorFilterOnChildDoesNotInheritOpacity) { - SaveLayerOptions expected = SaveLayerOptions::kWithAttributes; - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation(SaveLayerOptions::kWithAttributes); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -1539,8 +1613,8 @@ TEST_F(DisplayListTest, SaveLayerColorFilterOnChildDoesNotInheritOpacity) { } TEST_F(DisplayListTest, SaveLayerSrcBlendOnChildDoesNotInheritOpacity) { - SaveLayerOptions expected = SaveLayerOptions::kWithAttributes; - SaveLayerExpector expector(expected); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation(SaveLayerOptions::kWithAttributes); DisplayListBuilder builder; DlOpReceiver& receiver = ToReceiver(builder); @@ -4156,7 +4230,8 @@ TEST_F(DisplayListTest, MaxBlendModeInsideSaveLayer) { builder.Restore(); auto dl = builder.Build(); EXPECT_EQ(dl->max_root_blend_mode(), DlBlendMode::kSrcOver); - SaveLayerExpector expector(DlBlendMode::kModulate); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation(DlBlendMode::kModulate); dl->Dispatch(expector); EXPECT_TRUE(expector.all_expectations_checked()); } @@ -4173,7 +4248,8 @@ TEST_F(DisplayListTest, MaxBlendModeInsideNonDefaultBlendedSaveLayer) { builder.Restore(); auto dl = builder.Build(); EXPECT_EQ(dl->max_root_blend_mode(), DlBlendMode::kScreen); - SaveLayerExpector expector(DlBlendMode::kModulate); + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation(DlBlendMode::kModulate); dl->Dispatch(expector); EXPECT_TRUE(expector.all_expectations_checked()); } @@ -4237,7 +4313,10 @@ TEST_F(DisplayListTest, MaxBlendModeInsideComplexSaveLayers) { auto dl = builder.Build(); EXPECT_EQ(dl->max_root_blend_mode(), DlBlendMode::kSrcOver); - SaveLayerExpector expector({DlBlendMode::kModulate, DlBlendMode::kScreen}); + SAVE_LAYER_EXPECTOR(expector); + expector // + .addExpectation(DlBlendMode::kModulate) + .addExpectation(DlBlendMode::kScreen); dl->Dispatch(expector); EXPECT_TRUE(expector.all_expectations_checked()); } @@ -4266,7 +4345,8 @@ TEST_F(DisplayListTest, BackdropDetectionSimpleSaveLayer) { EXPECT_TRUE(dl->root_has_backdrop_filter()); // The saveLayer itself, though, does not have the contains backdrop // flag set because its content does not contain a saveLayer with backdrop - SaveLayerExpector expector( + SAVE_LAYER_EXPECTOR(expector); + expector.addExpectation( SaveLayerOptions::kNoAttributes.with_can_distribute_opacity()); dl->Dispatch(expector); EXPECT_TRUE(expector.all_expectations_checked()); @@ -4289,10 +4369,13 @@ TEST_F(DisplayListTest, BackdropDetectionNestedSaveLayer) { auto dl = builder.Build(); EXPECT_FALSE(dl->root_has_backdrop_filter()); - SaveLayerExpector expector({ - SaveLayerOptions::kNoAttributes.with_contains_backdrop_filter(), - SaveLayerOptions::kNoAttributes.with_can_distribute_opacity(), - }); + SAVE_LAYER_EXPECTOR(expector); + expector // + .addExpectation(SaveLayerOptions::kNoAttributes // + .with_contains_backdrop_filter() + .with_content_is_unbounded()) + .addExpectation( + SaveLayerOptions::kNoAttributes.with_can_distribute_opacity()); dl->Dispatch(expector); EXPECT_TRUE(expector.all_expectations_checked()); } @@ -5179,5 +5262,423 @@ TEST_F(DisplayListTest, ClipOvalRRectPathPromoteToClipOval) { DisplayListsEQ_Verbose(dl, expect_dl); } +TEST_F(DisplayListTest, BoundedRenderOpsDoNotReportUnbounded) { + static const SkRect root_cull = SkRect::MakeLTRB(100, 100, 200, 200); + static const SkRect draw_rect = SkRect::MakeLTRB(110, 110, 190, 190); + + using Renderer = const std::function; + auto test_bounded = [](const std::string& label, const Renderer& renderer) { + { + DisplayListBuilder builder(root_cull); + renderer(builder); + auto display_list = builder.Build(); + + EXPECT_EQ(display_list->bounds(), draw_rect) << label; + EXPECT_FALSE(display_list->root_is_unbounded()) << label; + } + + { + DisplayListBuilder builder(root_cull); + builder.SaveLayer(nullptr, nullptr); + renderer(builder); + builder.Restore(); + auto display_list = builder.Build(); + + EXPECT_EQ(display_list->bounds(), draw_rect) << label; + EXPECT_FALSE(display_list->root_is_unbounded()) << label; + + SAVE_LAYER_EXPECTOR(expector); + expector // + .addDetail(label) + .addExpectation([](const SaveLayerOptions& options) { + return !options.content_is_unbounded(); + }); + display_list->Dispatch(expector); + } + }; + + test_bounded("DrawLine", [](DlCanvas& builder) { + builder.DrawLine( + {draw_rect.left() + 1.0f, draw_rect.top() + 1.0f}, + {draw_rect.right() - 1.0f, draw_rect.top() + 1.0f}, + DlPaint().setStrokeWidth(2.0f).setStrokeCap(DlStrokeCap::kSquare)); + builder.DrawLine( + {draw_rect.left() + 1.0f, draw_rect.bottom() - 1.0f}, + {draw_rect.right() - 1.0f, draw_rect.bottom() - 1.0f}, + DlPaint().setStrokeWidth(2.0f).setStrokeCap(DlStrokeCap::kSquare)); + }); + + test_bounded("DrawDashedLine", [](DlCanvas& builder) { + builder.DrawDashedLine( + {draw_rect.left() + 1.0f, draw_rect.top() + 1.0f}, + {draw_rect.right() - 1.0f, draw_rect.top() + 1.0f}, + // must fill 80 x 80 square with on dashes at both + // ends - 40 + 25 + 40 == 105 so it will be on + // at both ends + 40.0f, 25.0f, + DlPaint().setStrokeWidth(2.0f).setStrokeCap(DlStrokeCap::kSquare)); + builder.DrawDashedLine( + {draw_rect.left() + 1.0f, draw_rect.bottom() - 1.0f}, + {draw_rect.right() - 1.0f, draw_rect.bottom() - 1.0f}, + // must fill 80 x 80 square with on dashes at both + // ends - 40 + 25 + 40 == 105 so it will be on + // at both ends + 40.0f, 25.0f, + DlPaint().setStrokeWidth(2.0f).setStrokeCap(DlStrokeCap::kSquare)); + }); + + test_bounded("DrawRect", [](DlCanvas& builder) { + builder.DrawRect(draw_rect, DlPaint()); + }); + + test_bounded("DrawOval", [](DlCanvas& builder) { + builder.DrawOval(draw_rect, DlPaint()); + }); + + test_bounded("DrawCircle", [](DlCanvas& builder) { + builder.DrawCircle(draw_rect.center(), draw_rect.width() * 0.5f, DlPaint()); + }); + + test_bounded("DrawRRect", [](DlCanvas& builder) { + builder.DrawRRect(SkRRect::MakeRectXY(draw_rect, 5.0f, 5.0f), DlPaint()); + }); + + test_bounded("DrawDRRect", [](DlCanvas& builder) { + builder.DrawDRRect( + SkRRect::MakeRectXY(draw_rect, 5.0f, 5.0f), + SkRRect::MakeRectXY(draw_rect.makeInset(10.0f, 10.0f), 5.0f, 5.0f), + DlPaint()); + }); + + test_bounded("DrawArc", [](DlCanvas& builder) { + builder.DrawArc(draw_rect, 45.0f, 355.0f, false, DlPaint()); + }); + + test_bounded("DrawPathEvenOdd", [](DlCanvas& builder) { + SkPath path = SkPath::Rect(draw_rect); + path.setFillType(SkPathFillType::kEvenOdd); + builder.DrawPath(path, DlPaint()); + }); + + test_bounded("DrawPathWinding", [](DlCanvas& builder) { + SkPath path = SkPath::Rect(draw_rect); + path.setFillType(SkPathFillType::kWinding); + builder.DrawPath(path, DlPaint()); + }); + + auto test_draw_points = [&test_bounded](PointMode mode) { + std::stringstream ss; + ss << "DrawPoints(" << mode << ")"; + test_bounded(ss.str(), [mode](DlCanvas& builder) { + SkPoint points[4] = { + {draw_rect.left() + 1.0f, draw_rect.top() + 1.0f}, + {draw_rect.right() - 1.0f, draw_rect.top() + 1.0f}, + {draw_rect.right() - 1.0f, draw_rect.bottom() - 1.0f}, + {draw_rect.left() + 1.0f, draw_rect.bottom() - 1.0f}, + }; + DlPaint paint; + paint.setStrokeWidth(2.0f); + paint.setDrawStyle(DlDrawStyle::kStroke); + // bounds accumulation doesn't examine the points to see if they + // have diagonals so Square caps may have their corners accumulated + // but Round caps will always pad by only half the stroke width. + paint.setStrokeCap(DlStrokeCap::kRound); + + builder.DrawPoints(mode, 4, points, paint); + }); + }; + + test_draw_points(PointMode::kPoints); + test_draw_points(PointMode::kLines); + test_draw_points(PointMode::kPolygon); + + test_bounded("DrawVerticesTriangles", [](DlCanvas& builder) { + SkPoint points[6] = { + {draw_rect.left(), draw_rect.top()}, + {draw_rect.right(), draw_rect.top()}, + {draw_rect.right(), draw_rect.bottom()}, + {draw_rect.right(), draw_rect.bottom()}, + {draw_rect.left(), draw_rect.bottom()}, + {draw_rect.left(), draw_rect.top()}, + }; + DlVertices::Builder vertices(DlVertexMode::kTriangles, 6, + DlVertices::Builder::kNone, 0); + vertices.store_vertices(points); + builder.DrawVertices(vertices.build(), DlBlendMode::kSrcOver, DlPaint()); + }); + + test_bounded("DrawVerticesTriangleStrip", [](DlCanvas& builder) { + SkPoint points[6] = { + {draw_rect.left(), draw_rect.top()}, + {draw_rect.right(), draw_rect.top()}, + {draw_rect.right(), draw_rect.bottom()}, + {draw_rect.left(), draw_rect.bottom()}, + {draw_rect.left(), draw_rect.top()}, + {draw_rect.right(), draw_rect.top()}, + }; + DlVertices::Builder vertices(DlVertexMode::kTriangleStrip, 6, + DlVertices::Builder::kNone, 0); + vertices.store_vertices(points); + builder.DrawVertices(vertices.build(), DlBlendMode::kSrcOver, DlPaint()); + }); + + test_bounded("DrawVerticesTriangleFan", [](DlCanvas& builder) { + SkPoint points[6] = { + draw_rect.center(), + {draw_rect.left(), draw_rect.top()}, + {draw_rect.right(), draw_rect.top()}, + {draw_rect.right(), draw_rect.bottom()}, + {draw_rect.left(), draw_rect.bottom()}, + }; + DlVertices::Builder vertices(DlVertexMode::kTriangleFan, 5, + DlVertices::Builder::kNone, 0); + vertices.store_vertices(points); + builder.DrawVertices(vertices.build(), DlBlendMode::kSrcOver, DlPaint()); + }); + + test_bounded("DrawImage", [](DlCanvas& builder) { + auto image = MakeTestImage(draw_rect.width(), draw_rect.height(), 5); + builder.DrawImage(image, {draw_rect.left(), draw_rect.top()}, + DlImageSampling::kLinear); + }); + + test_bounded("DrawImageRect", [](DlCanvas& builder) { + auto image = MakeTestImage(root_cull.width(), root_cull.height(), 5); + builder.DrawImageRect(image, draw_rect, DlImageSampling::kLinear); + }); + + test_bounded("DrawImageNine", [](DlCanvas& builder) { + auto image = MakeTestImage(root_cull.width(), root_cull.height(), 5); + SkIRect center = image->bounds().makeInset(10, 10); + builder.DrawImageNine(image, center, draw_rect, DlFilterMode::kLinear); + }); + + test_bounded("DrawTextBlob", [](DlCanvas& builder) { + SkFont font = CreateTestFontOfSize(20.0f); + sk_sp face = font.refTypeface(); + ASSERT_TRUE(face); + ASSERT_TRUE(face->countGlyphs() > 0) << "No glyphs in font"; + auto blob = SkTextBlob::MakeFromText("Hello", 5, font); + + // Make sure the blob fits within the draw_rect bounds. + ASSERT_LT(blob->bounds().width(), draw_rect.width()); + ASSERT_LT(blob->bounds().height(), draw_rect.height()); + + // Draw once at upper left and again at lower right to fill the bounds. + builder.DrawTextBlob(blob, draw_rect.left() - blob->bounds().left(), + draw_rect.top() - blob->bounds().top(), DlPaint()); + builder.DrawTextBlob(blob, draw_rect.right() - blob->bounds().right(), + draw_rect.bottom() - blob->bounds().bottom(), + DlPaint()); + }); + + test_bounded("DrawTextFrame", [](DlCanvas& builder) { + SkFont font = CreateTestFontOfSize(20.0f); + sk_sp face = font.refTypeface(); + ASSERT_TRUE(face); + ASSERT_TRUE(face->countGlyphs() > 0) << "No glyphs in font"; + auto blob = SkTextBlob::MakeFromText("Hello", 5, font); + auto frame = impeller::MakeTextFrameFromTextBlobSkia(blob); + + // Make sure the blob fits within the draw_rect bounds. + ASSERT_LT(blob->bounds().width(), draw_rect.width()); + ASSERT_LT(blob->bounds().height(), draw_rect.height()); + + // Draw once at upper left and again at lower right to fill the bounds. + builder.DrawTextFrame(frame, draw_rect.left() - blob->bounds().left(), + draw_rect.top() - blob->bounds().top(), DlPaint()); + builder.DrawTextFrame(frame, draw_rect.right() - blob->bounds().right(), + draw_rect.bottom() - blob->bounds().bottom(), + DlPaint()); + }); + + test_bounded("DrawBoundedDisplayList", [](DlCanvas& builder) { + DisplayListBuilder nested_builder(root_cull); + nested_builder.DrawRect(draw_rect, DlPaint()); + auto nested_display_list = nested_builder.Build(); + + EXPECT_EQ(nested_display_list->bounds(), draw_rect); + ASSERT_FALSE(nested_display_list->root_is_unbounded()); + + builder.DrawDisplayList(nested_display_list); + }); + + test_bounded("DrawShadow", [](DlCanvas& builder) { + SkPath path = SkPath::Rect(draw_rect.makeInset(20, 20)); + DlScalar elevation = 2.0f; + DlScalar dpr = 1.0f; + auto shadow_bounds = + DlCanvas::ComputeShadowBounds(path, elevation, dpr, SkMatrix::I()); + + // Make sure the shadow fits within the draw_rect bounds. + ASSERT_LT(shadow_bounds.width(), draw_rect.width()); + ASSERT_LT(shadow_bounds.height(), draw_rect.height()); + + // Draw once at upper left and again at lower right to fill the bounds. + SkPath pathUL = // + SkPath(path).offset(draw_rect.left() - shadow_bounds.left(), + draw_rect.top() - shadow_bounds.top()); + builder.DrawShadow(pathUL, DlColor::kMagenta(), elevation, true, dpr); + SkPath pathLR = + SkPath(path).offset(draw_rect.right() - shadow_bounds.right(), + draw_rect.bottom() - shadow_bounds.bottom()); + builder.DrawShadow(pathLR, DlColor::kMagenta(), elevation, true, dpr); + }); + + for (int i = 0; i <= static_cast(DlBlendMode::kLastMode); i++) { + DlBlendMode mode = static_cast(i); + if (mode == DlBlendMode::kDst) { + // No way to make kDst a non-nop + continue; + } + std::stringstream ss; + ss << "DrawRectWith" << mode; + test_bounded(ss.str(), [mode](DlCanvas& builder) { + // alpha of 0x7f prevents kDstIn from being a nop + builder.DrawRect(draw_rect, DlPaint().setBlendMode(mode).setAlpha(0x7f)); + }); + } +} + +TEST_F(DisplayListTest, UnboundedRenderOpsAreReportedUnlessClipped) { + static const SkRect root_cull = SkRect::MakeLTRB(100, 100, 200, 200); + static const SkRect clip_rect = SkRect::MakeLTRB(120, 120, 180, 180); + static const SkRect draw_rect = SkRect::MakeLTRB(110, 110, 190, 190); + + using Renderer = const std::function; + auto test_unbounded = [](const std::string& label, // + const Renderer& renderer, + int extra_save_layers = 0) { + { + DisplayListBuilder builder(root_cull); + renderer(builder); + auto display_list = builder.Build(); + + EXPECT_EQ(display_list->bounds(), root_cull) << label; + EXPECT_TRUE(display_list->root_is_unbounded()) << label; + } + + { + DisplayListBuilder builder(root_cull); + builder.ClipRect(clip_rect); + renderer(builder); + auto display_list = builder.Build(); + + EXPECT_EQ(display_list->bounds(), clip_rect) << label; + EXPECT_FALSE(display_list->root_is_unbounded()) << label; + } + + { + DisplayListBuilder builder(root_cull); + builder.SaveLayer(nullptr, nullptr); + renderer(builder); + builder.Restore(); + auto display_list = builder.Build(); + + EXPECT_EQ(display_list->bounds(), root_cull) << label; + EXPECT_FALSE(display_list->root_is_unbounded()) << label; + + SAVE_LAYER_EXPECTOR(expector); + expector // + .addDetail(label) + .addExpectation([](const SaveLayerOptions& options) { + return options.content_is_unbounded(); + }); + for (int i = 0; i < extra_save_layers; i++) { + expector.addOpenExpectation(); + } + display_list->Dispatch(expector); + } + + { + DisplayListBuilder builder(root_cull); + builder.SaveLayer(nullptr, nullptr); + builder.ClipRect(clip_rect); + renderer(builder); + builder.Restore(); + auto display_list = builder.Build(); + + EXPECT_EQ(display_list->bounds(), clip_rect) << label; + EXPECT_FALSE(display_list->root_is_unbounded()) << label; + + SAVE_LAYER_EXPECTOR(expector); + expector // + .addDetail(label) + .addExpectation([](const SaveLayerOptions& options) { + return !options.content_is_unbounded(); + }); + for (int i = 0; i < extra_save_layers; i++) { + expector.addOpenExpectation(); + } + display_list->Dispatch(expector); + } + }; + + test_unbounded("DrawPaint", [](DlCanvas& builder) { // + builder.DrawPaint(DlPaint()); + }); + + test_unbounded("DrawColor", [](DlCanvas& builder) { + builder.DrawColor(DlColor::kMagenta(), DlBlendMode::kSrc); + }); + + test_unbounded("Clear", [](DlCanvas& builder) { // + builder.Clear(DlColor::kMagenta()); + }); + + test_unbounded("DrawPathEvenOddInverted", [](DlCanvas& builder) { + SkPath path = SkPath::Rect(draw_rect); + path.setFillType(SkPathFillType::kInverseEvenOdd); + builder.DrawPath(path, DlPaint()); + }); + + test_unbounded("DrawPathWindingInverted", [](DlCanvas& builder) { + SkPath path = SkPath::Rect(draw_rect); + path.setFillType(SkPathFillType::kInverseWinding); + builder.DrawPath(path, DlPaint()); + }); + + test_unbounded("DrawUnboundedDisplayList", [](DlCanvas& builder) { + DisplayListBuilder nested_builder(root_cull); + nested_builder.DrawPaint(DlPaint()); + auto nested_display_list = nested_builder.Build(); + + EXPECT_EQ(nested_display_list->bounds(), root_cull); + ASSERT_TRUE(nested_display_list->root_is_unbounded()); + + builder.DrawDisplayList(nested_display_list); + }); + + test_unbounded("DrawRectWithUnboundedImageFilter", [](DlCanvas& builder) { + // clang-format off + const DlScalar matrix[20] = { + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, + }; + // clang-format on + auto unbounded_cf = DlMatrixColorFilter::Make(matrix); + // ColorFilter must modify transparent black to be "unbounded" + ASSERT_TRUE(unbounded_cf->modifies_transparent_black()); + auto unbounded_if = DlColorFilterImageFilter::Make(unbounded_cf); + SkRect output_bounds; + // ImageFilter returns null from bounds queries if it is "unbounded" + ASSERT_EQ(unbounded_if->map_local_bounds(draw_rect, output_bounds), + nullptr); + + builder.DrawRect(draw_rect, DlPaint().setImageFilter(unbounded_if)); + }); + + test_unbounded( + "SaveLayerWithBackdropFilter", + [](DlCanvas& builder) { + auto filter = DlBlurImageFilter::Make(3.0f, 3.0f, DlTileMode::kMirror); + builder.SaveLayer(nullptr, nullptr, filter.get()); + builder.Restore(); + }, + 1); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/display_list/dl_builder.cc b/engine/src/flutter/display_list/dl_builder.cc index 748d07ebfe..833b637e9b 100644 --- a/engine/src/flutter/display_list/dl_builder.cc +++ b/engine/src/flutter/display_list/dl_builder.cc @@ -78,6 +78,7 @@ sk_sp DisplayListBuilder::Build() { bool is_safe = is_ui_thread_safe_; bool affects_transparency = current_layer().affects_transparent_layer; bool root_has_backdrop_filter = current_layer().contains_backdrop_filter; + bool root_is_unbounded = current_layer().is_unbounded; DlBlendMode max_root_blend_mode = current_layer().max_blend_mode; sk_sp rtree; @@ -111,7 +112,8 @@ sk_sp DisplayListBuilder::Build() { return sk_sp(new DisplayList( std::move(storage_), bytes, count, nested_bytes, nested_count, total_depth, bounds, opacity_compatible, is_safe, affects_transparency, - max_root_blend_mode, root_has_backdrop_filter, std::move(rtree))); + max_root_blend_mode, root_has_backdrop_filter, root_is_unbounded, + std::move(rtree))); } static constexpr DlRect kEmpty = DlRect(); @@ -629,6 +631,10 @@ void DisplayListBuilder::RestoreLayer() { layer_op->options = layer_op->options.with_can_distribute_opacity(); } + if (current_layer().is_unbounded) { + layer_op->options = layer_op->options.with_content_is_unbounded(); + } + // Ensure that the bounds transferred in the following call will be // attributed to the index of the restore op. FML_DCHECK(layer_op->restore_index == op_index_); @@ -1575,7 +1581,9 @@ void DisplayListBuilder::DrawDisplayList(const sk_sp display_list, const SkRect bounds = display_list->bounds(); bool accumulated; sk_sp rtree; - if (!rtree_data_.has_value() || !(rtree = display_list->rtree())) { + if (display_list->root_is_unbounded()) { + accumulated = AccumulateUnbounded(); + } else if (!rtree_data_.has_value() || !(rtree = display_list->rtree())) { accumulated = AccumulateOpBounds(bounds, kDrawDisplayListFlags); } else { std::list rects = @@ -1788,6 +1796,9 @@ bool DisplayListBuilder::AdjustBoundsForPaint(SkRect& bounds, } bool DisplayListBuilder::AccumulateUnbounded(const SaveInfo& save) { + if (!save.has_valid_clip) { + save.layer_info->is_unbounded = true; + } SkRect global_clip = save.global_state.device_cull_rect(); SkRect layer_clip = save.global_state.local_cull_rect(); if (global_clip.isEmpty() || !save.layer_state.mapAndClipRect(&layer_clip)) { diff --git a/engine/src/flutter/display_list/dl_builder.h b/engine/src/flutter/display_list/dl_builder.h index dc75e2d37f..99e46143f3 100644 --- a/engine/src/flutter/display_list/dl_builder.h +++ b/engine/src/flutter/display_list/dl_builder.h @@ -554,6 +554,7 @@ class DisplayListBuilder final : public virtual DlCanvas, bool opacity_incompatible_op_detected = false; bool affects_transparent_layer = false; bool contains_backdrop_filter = false; + bool is_unbounded = false; bool is_group_opacity_compatible() const { return !opacity_incompatible_op_detected && diff --git a/engine/src/flutter/testing/display_list_testing.cc b/engine/src/flutter/testing/display_list_testing.cc index ce831ca247..9b6a38b75d 100644 --- a/engine/src/flutter/testing/display_list_testing.cc +++ b/engine/src/flutter/testing/display_list_testing.cc @@ -144,6 +144,8 @@ std::ostream& operator<<(std::ostream& os, const SaveLayerOptions& options) { << "can_distribute_opacity: " << options.can_distribute_opacity() << ", " << "contains_backdrop: " << options.contains_backdrop_filter() + << ", " + << "is_unbounded: " << options.content_is_unbounded() << ")"; }