From 7f453eb08912b9dcab4f71e02eb15e20b4e15759 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Wed, 27 Mar 2024 17:42:48 -0700 Subject: [PATCH] [Impeller] Reland: Use the scissor to limit all draws by clip coverage. (flutter/engine#51731) Reland https://github.com/flutter/engine/pull/51698. This reverts commit https://github.com/flutter/engine/commit/79227401843f1368e31955bae849ec5d4f95002b. Attempts to improve https://github.com/flutter/flutter/issues/145274. Our new clipping technique paints walls on the depth buffer "in front" of the Entities that will be affected by said clips. So when an intersect clip is drawn (the common case), the clip will cover the whole framebuffer. Depth is divvied up such that deeper clips get drawn _behind_ shallower clips, and so many clips actually don't end up drawing depth across the whole framebuffer. However, if the app does a lot of transitioning from a huge clips to a small clips, a lot of unnecessary depth churn occurs (very common in Flutter -- this happens with both the app bar and floating action button in the counter template, for example). Since progressively deeper layers in the clip coverage stack always subset their parent layers, we can reduce waste for small intersect clips by setting the scissor to the clip coverage rect instead of drawing the clip to the whole screen. Note that this change _does not_ help much with huge/fullscreen clips. Also, we could potentially improve this further by computing much stricter bounds. Rather than just using clip coverage for the scissor, we could intersect it with the union of all draws affected by the clip at the cost of a bit more CPU churn per draw. I don't think that's enough juice for the squeeze though. Before (`Play/AiksTest.CanRenderNestedClips/Metal`): https://github.com/flutter/engine/assets/919017/7858400f-793a-4f7b-a0e4-fa3581198beb After (`Play/AiksTest.CanRenderNestedClips/Metal`): https://github.com/flutter/engine/assets/919017/b2f7c96d-a820-454d-91df-f5fae4976e91 --- .../flutter/impeller/aiks/aiks_unittests.cc | 31 +++++++++ .../flutter/impeller/entity/entity_pass.cc | 61 +++++++++++----- .../impeller/entity/entity_pass_clip_stack.cc | 37 ++++++---- .../impeller/entity/entity_pass_clip_stack.h | 35 +++++++--- .../impeller/entity/entity_pass_unittests.cc | 69 +++++++++++++------ .../testing/impeller_golden_tests_output.txt | 3 + 6 files changed, 176 insertions(+), 60 deletions(-) diff --git a/engine/src/flutter/impeller/aiks/aiks_unittests.cc b/engine/src/flutter/impeller/aiks/aiks_unittests.cc index c48a8bfc362..39ad0e7fed7 100644 --- a/engine/src/flutter/impeller/aiks/aiks_unittests.cc +++ b/engine/src/flutter/impeller/aiks/aiks_unittests.cc @@ -30,6 +30,7 @@ #include "impeller/geometry/path.h" #include "impeller/geometry/path_builder.h" #include "impeller/geometry/rect.h" +#include "impeller/geometry/size.h" #include "impeller/playground/widgets.h" #include "impeller/renderer/command_buffer.h" #include "impeller/renderer/snapshot.h" @@ -3497,6 +3498,36 @@ TEST_P(AiksTest, CanDrawPerspectiveTransformWithClips) { ASSERT_TRUE(OpenPlaygroundHere(callback)); } +TEST_P(AiksTest, CanRenderClippedBackdropFilter) { + Canvas canvas; + Paint paint; + + canvas.Scale(GetContentScale()); + + // Draw something interesting in the background. + std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, + Color{0.1294, 0.5882, 0.9529, 1.0}}; + std::vector stops = { + 0.0, + 1.0, + }; + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {100, 100}, std::move(colors), std::move(stops), + Entity::TileMode::kRepeat, {}); + canvas.DrawPaint(paint); + + Rect clip_rect = Rect::MakeLTRB(50, 50, 400, 300); + + // Draw a clipped SaveLayer, where the clip coverage and SaveLayer size are + // the same. + canvas.ClipRRect(clip_rect, Size(100, 100), + Entity::ClipOperation::kIntersect); + canvas.SaveLayer({}, clip_rect, + ImageFilter::MakeFromColorFilter(*ColorFilter::MakeBlend( + BlendMode::kExclusion, Color::Red()))); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + } // namespace testing } // namespace impeller diff --git a/engine/src/flutter/impeller/entity/entity_pass.cc b/engine/src/flutter/impeller/entity/entity_pass.cc index a3664ed816c..89016da4e52 100644 --- a/engine/src/flutter/impeller/entity/entity_pass.cc +++ b/engine/src/flutter/impeller/entity/entity_pass.cc @@ -25,6 +25,7 @@ #include "impeller/entity/inline_pass_context.h" #include "impeller/geometry/color.h" #include "impeller/geometry/rect.h" +#include "impeller/geometry/size.h" #include "impeller/renderer/command_buffer.h" #ifdef IMPELLER_DEBUG @@ -752,6 +753,25 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( FML_UNREACHABLE(); } +static void SetClipScissor(std::optional clip_coverage, + RenderPass& pass, + Point global_pass_position) { + if constexpr (!ContentContext::kEnableStencilThenCover) { + return; + } + // Set the scissor to the clip coverage area. We do this prior to rendering + // the clip itself and all its contents. + IRect scissor; + if (clip_coverage.has_value()) { + clip_coverage = clip_coverage->Shift(-global_pass_position); + scissor = IRect::RoundOut(clip_coverage.value()); + // The scissor rect must not exceed the size of the render target. + scissor = scissor.Intersection(IRect::MakeSize(pass.GetRenderTargetSize())) + .value_or(IRect()); + } + pass.SetScissor(scissor); +} + bool EntityPass::RenderElement(Entity& element_entity, size_t clip_depth_floor, InlinePassContext& pass_context, @@ -767,17 +787,6 @@ bool EntityPass::RenderElement(Entity& element_entity, return false; } - if (result.just_created) { - // Restore any clips that were recorded before the backdrop filter was - // applied. - auto& replay_entities = clip_coverage_stack.GetReplayEntities(); - for (const auto& entity : replay_entities) { - if (!entity.Render(renderer, *result.pass)) { - VALIDATION_LOG << "Failed to render entity for clip restore."; - } - } - } - // If the pass context returns a backdrop texture, we need to draw it to the // current pass. We do this because it's faster and takes significantly less // memory than storing/loading large MSAA textures. Also, it's not possible to @@ -801,6 +810,19 @@ bool EntityPass::RenderElement(Entity& element_entity, } } + if (result.just_created) { + // Restore any clips that were recorded before the backdrop filter was + // applied. + auto& replay_entities = clip_coverage_stack.GetReplayEntities(); + for (const auto& replay : replay_entities) { + SetClipScissor(clip_coverage_stack.CurrentClipCoverage(), *result.pass, + global_pass_position); + if (!replay.entity.Render(renderer, *result.pass)) { + VALIDATION_LOG << "Failed to render entity for clip restore."; + } + } + } + auto current_clip_coverage = clip_coverage_stack.CurrentClipCoverage(); if (current_clip_coverage.has_value()) { // Entity transforms are relative to the current pass position, so we need @@ -826,11 +848,18 @@ bool EntityPass::RenderElement(Entity& element_entity, element_entity.GetContents()->SetCoverageHint( Rect::Intersection(element_coverage_hint, current_clip_coverage)); - if (!clip_coverage_stack.AppendClipCoverage(clip_coverage, element_entity, - clip_depth_floor, - global_pass_position)) { - // If the entity's coverage change did not change the clip coverage, we - // don't need to render it. + EntityPassClipStack::ClipStateResult clip_state_result = + clip_coverage_stack.ApplyClipState(clip_coverage, element_entity, + clip_depth_floor, + global_pass_position); + + if (clip_state_result.clip_did_change) { + // We only need to update the pass scissor if the clip state has changed. + SetClipScissor(clip_coverage_stack.CurrentClipCoverage(), *result.pass, + global_pass_position); + } + + if (!clip_state_result.should_render) { return true; } diff --git a/engine/src/flutter/impeller/entity/entity_pass_clip_stack.cc b/engine/src/flutter/impeller/entity/entity_pass_clip_stack.cc index 5d126acdcbc..67bb02d8e8f 100644 --- a/engine/src/flutter/impeller/entity/entity_pass_clip_stack.cc +++ b/engine/src/flutter/impeller/entity/entity_pass_clip_stack.cc @@ -49,20 +49,23 @@ EntityPassClipStack::GetClipCoverageLayers() const { return subpass_state_.back().clip_coverage; } -bool EntityPassClipStack::AppendClipCoverage( - Contents::ClipCoverage clip_coverage, +EntityPassClipStack::ClipStateResult EntityPassClipStack::ApplyClipState( + Contents::ClipCoverage global_clip_coverage, Entity& entity, size_t clip_depth_floor, Point global_pass_position) { + ClipStateResult result = {.should_render = false, .clip_did_change = false}; + auto& subpass_state = GetCurrentSubpassState(); - switch (clip_coverage.type) { + switch (global_clip_coverage.type) { case Contents::ClipCoverage::Type::kNoChange: break; case Contents::ClipCoverage::Type::kAppend: { auto op = CurrentClipCoverage(); subpass_state.clip_coverage.push_back( - ClipCoverageLayer{.coverage = clip_coverage.coverage, + ClipCoverageLayer{.coverage = global_clip_coverage.coverage, .clip_depth = entity.GetClipDepth() + 1}); + result.clip_did_change = true; FML_DCHECK(subpass_state.clip_coverage.back().clip_depth == subpass_state.clip_coverage.front().clip_depth + @@ -71,14 +74,14 @@ bool EntityPassClipStack::AppendClipCoverage( if (!op.has_value()) { // Running this append op won't impact the clip buffer because the // whole screen is already being clipped, so skip it. - return false; + return result; } } break; case Contents::ClipCoverage::Type::kRestore: { if (subpass_state.clip_coverage.back().clip_depth <= entity.GetClipDepth()) { // Drop clip restores that will do nothing. - return false; + return result; } auto restoration_index = entity.GetClipDepth() - @@ -96,18 +99,19 @@ bool EntityPassClipStack::AppendClipCoverage( restore_coverage = restore_coverage->Shift(-global_pass_position); } subpass_state.clip_coverage.resize(restoration_index + 1); + result.clip_did_change = true; if constexpr (ContentContext::kEnableStencilThenCover) { // Skip all clip restores when stencil-then-cover is enabled. if (subpass_state.clip_coverage.back().coverage.has_value()) { - RecordEntity(entity, clip_coverage.type); + RecordEntity(entity, global_clip_coverage.type, Rect()); } - return false; + return result; } if (!subpass_state.clip_coverage.back().coverage.has_value()) { // Running this restore op won't make anything renderable, so skip it. - return false; + return result; } auto restore_contents = @@ -130,19 +134,23 @@ bool EntityPassClipStack::AppendClipCoverage( #endif entity.SetClipDepth(entity.GetClipDepth() - clip_depth_floor); - RecordEntity(entity, clip_coverage.type); + RecordEntity(entity, global_clip_coverage.type, + subpass_state.clip_coverage.back().coverage); - return true; + result.should_render = true; + return result; } void EntityPassClipStack::RecordEntity(const Entity& entity, - Contents::ClipCoverage::Type type) { + Contents::ClipCoverage::Type type, + std::optional clip_coverage) { auto& subpass_state = GetCurrentSubpassState(); switch (type) { case Contents::ClipCoverage::Type::kNoChange: return; case Contents::ClipCoverage::Type::kAppend: - subpass_state.rendered_clip_entities.push_back(entity.Clone()); + subpass_state.rendered_clip_entities.push_back( + {.entity = entity.Clone(), .clip_coverage = clip_coverage}); break; case Contents::ClipCoverage::Type::kRestore: if (!subpass_state.rendered_clip_entities.empty()) { @@ -157,7 +165,8 @@ EntityPassClipStack::GetCurrentSubpassState() { return subpass_state_.back(); } -const std::vector& EntityPassClipStack::GetReplayEntities() const { +const std::vector& +EntityPassClipStack::GetReplayEntities() const { return subpass_state_.back().rendered_clip_entities; } diff --git a/engine/src/flutter/impeller/entity/entity_pass_clip_stack.h b/engine/src/flutter/impeller/entity/entity_pass_clip_stack.h index bfd7f5ba83b..d5181d86b99 100644 --- a/engine/src/flutter/impeller/entity/entity_pass_clip_stack.h +++ b/engine/src/flutter/impeller/entity/entity_pass_clip_stack.h @@ -6,6 +6,8 @@ #define FLUTTER_IMPELLER_ENTITY_ENTITY_PASS_CLIP_STACK_H_ #include "impeller/entity/contents/contents.h" +#include "impeller/entity/entity.h" +#include "impeller/geometry/rect.h" namespace impeller { @@ -21,6 +23,20 @@ struct ClipCoverageLayer { /// stencil buffer is left in an identical state. class EntityPassClipStack { public: + struct ReplayResult { + Entity entity; + std::optional clip_coverage; + }; + + struct ClipStateResult { + /// Whether or not the Entity should be rendered. If false, the Entity may + /// be safely skipped. + bool should_render = false; + /// Whether or not the current clip coverage changed during the call to + /// `ApplyClipState`. + bool clip_did_change = false; + }; + /// Create a new [EntityPassClipStack] with an initialized coverage rect. explicit EntityPassClipStack(const Rect& initial_coverage_rect); @@ -34,24 +50,27 @@ class EntityPassClipStack { bool HasCoverage() const; - /// Returns true if entity should be rendered. - bool AppendClipCoverage(Contents::ClipCoverage clip_coverage, - Entity& entity, - size_t clip_depth_floor, - Point global_pass_position); + /// @brief Applies the current clip state to an Entity. If the given Entity + /// is a clip operation, then the clip state is updated accordingly. + ClipStateResult ApplyClipState(Contents::ClipCoverage global_clip_coverage, + Entity& entity, + size_t clip_depth_floor, + Point global_pass_position); // Visible for testing. - void RecordEntity(const Entity& entity, Contents::ClipCoverage::Type type); + void RecordEntity(const Entity& entity, + Contents::ClipCoverage::Type type, + std::optional clip_coverage); // Visible for testing. - const std::vector& GetReplayEntities() const; + const std::vector& GetReplayEntities() const; // Visible for testing. const std::vector GetClipCoverageLayers() const; private: struct SubpassState { - std::vector rendered_clip_entities; + std::vector rendered_clip_entities; std::vector clip_coverage; }; diff --git a/engine/src/flutter/impeller/entity/entity_pass_unittests.cc b/engine/src/flutter/impeller/entity/entity_pass_unittests.cc index ce2a66a82dc..137cbd2e4ad 100644 --- a/engine/src/flutter/impeller/entity/entity_pass_unittests.cc +++ b/engine/src/flutter/impeller/entity/entity_pass_unittests.cc @@ -17,16 +17,26 @@ TEST(EntityPassClipStackTest, CanPushAndPopEntities) { EXPECT_TRUE(recorder.GetReplayEntities().empty()); Entity entity; - recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kAppend); + recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kAppend, + Rect::MakeLTRB(0, 0, 100, 100)); EXPECT_EQ(recorder.GetReplayEntities().size(), 1u); - recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kAppend); + recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kAppend, + Rect::MakeLTRB(0, 0, 50, 50)); EXPECT_EQ(recorder.GetReplayEntities().size(), 2u); + ASSERT_TRUE(recorder.GetReplayEntities()[0].clip_coverage.has_value()); + ASSERT_TRUE(recorder.GetReplayEntities()[1].clip_coverage.has_value()); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_EQ(recorder.GetReplayEntities()[0].clip_coverage.value(), + Rect::MakeLTRB(0, 0, 100, 100)); + EXPECT_EQ(recorder.GetReplayEntities()[1].clip_coverage.value(), + Rect::MakeLTRB(0, 0, 50, 50)); + // NOLINTEND(bugprone-unchecked-optional-access) - recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kRestore); + recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kRestore, Rect()); EXPECT_EQ(recorder.GetReplayEntities().size(), 1u); - recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kRestore); + recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kRestore, Rect()); EXPECT_TRUE(recorder.GetReplayEntities().empty()); } @@ -37,7 +47,7 @@ TEST(EntityPassClipStackTest, CanPopEntitiesSafely) { EXPECT_TRUE(recorder.GetReplayEntities().empty()); Entity entity; - recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kRestore); + recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kRestore, Rect()); EXPECT_TRUE(recorder.GetReplayEntities().empty()); } @@ -48,7 +58,8 @@ TEST(EntityPassClipStackTest, CanAppendNoChange) { EXPECT_TRUE(recorder.GetReplayEntities().empty()); Entity entity; - recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kNoChange); + recorder.RecordEntity(entity, Contents::ClipCoverage::Type::kNoChange, + Rect()); EXPECT_TRUE(recorder.GetReplayEntities().empty()); } @@ -61,12 +72,14 @@ TEST(EntityPassClipStackTest, AppendCoverageNoChange) { EXPECT_EQ(recorder.GetClipCoverageLayers()[0].clip_depth, 0u); Entity entity; - recorder.AppendClipCoverage( + EntityPassClipStack::ClipStateResult result = recorder.ApplyClipState( Contents::ClipCoverage{ .type = Contents::ClipCoverage::Type::kNoChange, .coverage = std::nullopt, }, entity, 0, Point(0, 0)); + EXPECT_TRUE(result.should_render); + EXPECT_FALSE(result.clip_did_change); EXPECT_EQ(recorder.GetClipCoverageLayers()[0].coverage, Rect::MakeSize(Size::MakeWH(100, 100))); @@ -82,12 +95,14 @@ TEST(EntityPassClipStackTest, AppendAndRestoreClipCoverage) { // Push a clip. Entity entity; entity.SetClipDepth(0); - recorder.AppendClipCoverage( + EntityPassClipStack::ClipStateResult result = recorder.ApplyClipState( Contents::ClipCoverage{ .type = Contents::ClipCoverage::Type::kAppend, .coverage = Rect::MakeLTRB(50, 50, 55, 55), }, entity, 0, Point(0, 0)); + EXPECT_TRUE(result.should_render); + EXPECT_TRUE(result.clip_did_change); ASSERT_EQ(recorder.GetClipCoverageLayers().size(), 2u); EXPECT_EQ(recorder.GetClipCoverageLayers()[1].coverage, @@ -97,7 +112,7 @@ TEST(EntityPassClipStackTest, AppendAndRestoreClipCoverage) { // Restore the clip. entity.SetClipDepth(0); - recorder.AppendClipCoverage( + recorder.ApplyClipState( Contents::ClipCoverage{ .type = Contents::ClipCoverage::Type::kRestore, .coverage = Rect::MakeLTRB(50, 50, 55, 55), @@ -120,12 +135,14 @@ TEST(EntityPassClipStackTest, UnbalancedRestore) { // Restore the clip. Entity entity; entity.SetClipDepth(0); - recorder.AppendClipCoverage( + EntityPassClipStack::ClipStateResult result = recorder.ApplyClipState( Contents::ClipCoverage{ .type = Contents::ClipCoverage::Type::kRestore, .coverage = Rect::MakeLTRB(50, 50, 55, 55), }, entity, 0, Point(0, 0)); + EXPECT_FALSE(result.should_render); + EXPECT_FALSE(result.clip_did_change); ASSERT_EQ(recorder.GetClipCoverageLayers().size(), 1u); EXPECT_EQ(recorder.GetClipCoverageLayers()[0].coverage, @@ -143,12 +160,16 @@ TEST(EntityPassClipStackTest, ClipAndRestoreWithSubpasses) { // Push a clip. Entity entity; entity.SetClipDepth(0u); - recorder.AppendClipCoverage( - Contents::ClipCoverage{ - .type = Contents::ClipCoverage::Type::kAppend, - .coverage = Rect::MakeLTRB(50, 50, 55, 55), - }, - entity, 0, Point(0, 0)); + { + EntityPassClipStack::ClipStateResult result = recorder.ApplyClipState( + Contents::ClipCoverage{ + .type = Contents::ClipCoverage::Type::kAppend, + .coverage = Rect::MakeLTRB(50, 50, 55, 55), + }, + entity, 0, Point(0, 0)); + EXPECT_TRUE(result.should_render); + EXPECT_TRUE(result.clip_did_change); + } ASSERT_EQ(recorder.GetClipCoverageLayers().size(), 2u); EXPECT_EQ(recorder.GetClipCoverageLayers()[1].coverage, @@ -163,12 +184,16 @@ TEST(EntityPassClipStackTest, ClipAndRestoreWithSubpasses) { Rect::MakeLTRB(50, 50, 55, 55)); entity.SetClipDepth(1); - recorder.AppendClipCoverage( - Contents::ClipCoverage{ - .type = Contents::ClipCoverage::Type::kAppend, - .coverage = Rect::MakeLTRB(54, 54, 55, 55), - }, - entity, 0, Point(0, 0)); + { + EntityPassClipStack::ClipStateResult result = recorder.ApplyClipState( + Contents::ClipCoverage{ + .type = Contents::ClipCoverage::Type::kAppend, + .coverage = Rect::MakeLTRB(54, 54, 55, 55), + }, + entity, 0, Point(0, 0)); + EXPECT_TRUE(result.should_render); + EXPECT_TRUE(result.clip_did_change); + } EXPECT_EQ(recorder.GetClipCoverageLayers()[1].coverage, Rect::MakeLTRB(54, 54, 55, 55)); diff --git a/engine/src/flutter/testing/impeller_golden_tests_output.txt b/engine/src/flutter/testing/impeller_golden_tests_output.txt index 9c13bcb3843..ae8814380b1 100644 --- a/engine/src/flutter/testing/impeller_golden_tests_output.txt +++ b/engine/src/flutter/testing/impeller_golden_tests_output.txt @@ -161,6 +161,9 @@ impeller_Play_AiksTest_CanRenderBackdropBlurInteractive_Vulkan.png impeller_Play_AiksTest_CanRenderBackdropBlur_Metal.png impeller_Play_AiksTest_CanRenderBackdropBlur_OpenGLES.png impeller_Play_AiksTest_CanRenderBackdropBlur_Vulkan.png +impeller_Play_AiksTest_CanRenderClippedBackdropFilter_Metal.png +impeller_Play_AiksTest_CanRenderClippedBackdropFilter_OpenGLES.png +impeller_Play_AiksTest_CanRenderClippedBackdropFilter_Vulkan.png impeller_Play_AiksTest_CanRenderClippedBlur_Metal.png impeller_Play_AiksTest_CanRenderClippedBlur_OpenGLES.png impeller_Play_AiksTest_CanRenderClippedBlur_Vulkan.png