diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index bb71dc35ac2d8..97491ca78e895 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -173,6 +173,7 @@ ../../../flutter/impeller/entity/entity_unittests.cc ../../../flutter/impeller/entity/geometry/geometry_unittests.cc ../../../flutter/impeller/entity/render_target_cache_unittests.cc +../../../flutter/impeller/entity/save_layer_utils_unittests.cc ../../../flutter/impeller/fixtures ../../../flutter/impeller/geometry/README.md ../../../flutter/impeller/geometry/geometry_unittests.cc diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 653a2f5cc8549..564b5b71156fb 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -42871,6 +42871,8 @@ ORIGIN: ../../../flutter/impeller/entity/inline_pass_context.cc + ../../../flutt ORIGIN: ../../../flutter/impeller/entity/inline_pass_context.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/render_target_cache.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/render_target_cache.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/entity/save_layer_utils.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/entity/save_layer_utils.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/shaders/blending/advanced_blend.frag + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/shaders/blending/advanced_blend.vert + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/shaders/blending/blend_select.glsl + ../../../flutter/LICENSE @@ -45759,6 +45761,8 @@ FILE: ../../../flutter/impeller/entity/inline_pass_context.cc FILE: ../../../flutter/impeller/entity/inline_pass_context.h FILE: ../../../flutter/impeller/entity/render_target_cache.cc FILE: ../../../flutter/impeller/entity/render_target_cache.h +FILE: ../../../flutter/impeller/entity/save_layer_utils.cc +FILE: ../../../flutter/impeller/entity/save_layer_utils.h FILE: ../../../flutter/impeller/entity/shaders/blending/advanced_blend.frag FILE: ../../../flutter/impeller/entity/shaders/blending/advanced_blend.vert FILE: ../../../flutter/impeller/entity/shaders/blending/blend_select.glsl diff --git a/impeller/aiks/aiks_blend_unittests.cc b/impeller/aiks/aiks_blend_unittests.cc index ccd5bf3ee73ba..a1fc120aa4196 100644 --- a/impeller/aiks/aiks_blend_unittests.cc +++ b/impeller/aiks/aiks_blend_unittests.cc @@ -127,12 +127,13 @@ TEST_P(AiksTest, ColorWheel) { draw_color_wheel(canvas); auto color_wheel_picture = canvas.EndRecordingAsPicture(); - auto snapshot = color_wheel_picture.Snapshot(renderer); - if (!snapshot.has_value() || !snapshot->texture) { + auto image = color_wheel_picture.ToImage( + renderer, ISize{GetWindowSize().width, GetWindowSize().height}); + if (!image) { return std::nullopt; } - color_wheel_image = snapshot->texture; - color_wheel_transform = snapshot->transform; + color_wheel_image = image; + color_wheel_transform = Matrix(); } Canvas canvas; diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc index b0540e1bb8485..0c8151150744c 100644 --- a/impeller/aiks/canvas.cc +++ b/impeller/aiks/canvas.cc @@ -814,8 +814,7 @@ void Canvas::SaveLayer(const Paint& paint, const std::shared_ptr& backdrop_filter, ContentBoundsPromise bounds_promise, uint32_t total_content_depth, - bool can_distribute_opacity, - bool bounds_from_caller) { + bool can_distribute_opacity) { if (can_distribute_opacity && !backdrop_filter && Paint::CanApplyOpacityPeephole(paint) && bounds_promise != ContentBoundsPromise::kMayClipContents) { @@ -837,7 +836,7 @@ void Canvas::SaveLayer(const Paint& paint, auto& new_layer_pass = GetCurrentPass(); if (bounds) { - new_layer_pass.SetBoundsLimit(bounds, bounds_promise); + new_layer_pass.SetBoundsLimit(bounds); } // When applying a save layer, absorb any pending distributed opacity. diff --git a/impeller/aiks/canvas.h b/impeller/aiks/canvas.h index 74a031f09e6d8..dbc3a1952dd73 100644 --- a/impeller/aiks/canvas.h +++ b/impeller/aiks/canvas.h @@ -76,8 +76,7 @@ class Canvas { const std::shared_ptr& backdrop_filter = nullptr, ContentBoundsPromise bounds_promise = ContentBoundsPromise::kUnknown, uint32_t total_content_depth = kMaxDepth, - bool can_distribute_opacity = false, - bool bounds_from_caller = false); + bool can_distribute_opacity = false); virtual bool Restore(); diff --git a/impeller/aiks/experimental_canvas.cc b/impeller/aiks/experimental_canvas.cc index 799a20acf78ff..0ea88adc32511 100644 --- a/impeller/aiks/experimental_canvas.cc +++ b/impeller/aiks/experimental_canvas.cc @@ -12,10 +12,12 @@ #include "impeller/core/allocator.h" #include "impeller/core/formats.h" #include "impeller/entity/contents/clip_contents.h" +#include "impeller/entity/contents/filters/filter_contents.h" #include "impeller/entity/contents/framebuffer_blend_contents.h" #include "impeller/entity/contents/text_contents.h" #include "impeller/entity/entity.h" #include "impeller/entity/entity_pass_clip_stack.h" +#include "impeller/entity/save_layer_utils.h" #include "impeller/geometry/color.h" #include "impeller/renderer/render_target.h" @@ -314,15 +316,9 @@ void ExperimentalCanvas::SaveLayer( const std::shared_ptr& backdrop_filter, ContentBoundsPromise bounds_promise, uint32_t total_content_depth, - bool can_distribute_opacity, - bool bounds_from_caller) { + bool can_distribute_opacity) { TRACE_EVENT0("flutter", "Canvas::saveLayer"); - if (bounds.has_value() && bounds->IsEmpty()) { - Save(total_content_depth); - return; - } - if (!clip_coverage_stack_.HasCoverage()) { // The current clip is empty. This means the pass texture won't be // visible, so skip it. @@ -346,13 +342,6 @@ void ExperimentalCanvas::SaveLayer( ->GetSize())) .Intersection(current_clip_coverage); - if (!maybe_coverage_limit.has_value()) { - Save(total_content_depth); - return; - } - maybe_coverage_limit = maybe_coverage_limit->Intersection( - Rect::MakeSize(render_target_.GetRenderTargetSize())); - if (!maybe_coverage_limit.has_value() || maybe_coverage_limit->IsEmpty()) { Save(total_content_depth); return; @@ -367,12 +356,33 @@ void ExperimentalCanvas::SaveLayer( return; } + std::shared_ptr filter_contents; + if (paint.image_filter) { + filter_contents = paint.image_filter->GetFilterContents(); + } + + std::optional maybe_subpass_coverage = ComputeSaveLayerCoverage( + bounds.value_or(Rect::MakeMaximum()), + transform_stack_.back().transform, // + coverage_limit, // + filter_contents, // + /*flood_output_coverage=*/ + Entity::IsBlendModeDestructive(paint.blend_mode), // + /*flood_input_coverage=*/!!backdrop_filter // + ); + + if (!maybe_subpass_coverage.has_value() || + maybe_subpass_coverage->IsEmpty()) { + Save(total_content_depth); + return; + } + auto subpass_coverage = maybe_subpass_coverage.value(); + // Backdrop filter state, ignored if there is no BDF. std::shared_ptr backdrop_filter_contents; Point local_position = {0, 0}; if (backdrop_filter) { - local_position = - current_clip_coverage.GetOrigin() - GetGlobalPassPosition(); + local_position = subpass_coverage.GetOrigin() - GetGlobalPassPosition(); EntityPass::BackdropFilterProc backdrop_filter_proc = [backdrop_filter = backdrop_filter->Clone()]( const FilterInput::Ref& input, const Matrix& effect_transform, @@ -409,21 +419,6 @@ void ExperimentalCanvas::SaveLayer( paint_copy.color.alpha *= transform_stack_.back().distributed_opacity; transform_stack_.back().distributed_opacity = 1.0; - // Backdrop Filter must expand bounds to at least the clip stack, otherwise - // the coverage of the parent render pass. - Rect subpass_coverage; - if (backdrop_filter_contents || - Entity::IsBlendModeDestructive(paint.blend_mode) || !bounds.has_value()) { - subpass_coverage = coverage_limit; - // TODO(jonahwilliams): if we have tight bounds we should be able to reduce - // this size here. if (bounds.has_value() && bounds_from_caller) { - // subpass_coverage = - // coverage_limit.Intersection(bounds.value()).value_or(bounds.value()); - // } - } else { - subpass_coverage = bounds->TransformBounds(GetCurrentTransform()); - } - render_passes_.push_back(LazyRenderingConfig( renderer_, // CreateRenderTarget(renderer_, // diff --git a/impeller/aiks/experimental_canvas.h b/impeller/aiks/experimental_canvas.h index 8ec5e18b7c1e2..debb4baf088fc 100644 --- a/impeller/aiks/experimental_canvas.h +++ b/impeller/aiks/experimental_canvas.h @@ -71,8 +71,7 @@ class ExperimentalCanvas : public Canvas { const std::shared_ptr& backdrop_filter, ContentBoundsPromise bounds_promise, uint32_t total_content_depth, - bool can_distribute_opacity, - bool bounds_from_caller) override; + bool can_distribute_opacity) override; bool Restore() override; diff --git a/impeller/aiks/picture.cc b/impeller/aiks/picture.cc index fc8ad2ed3639d..ba4793a0ad3eb 100644 --- a/impeller/aiks/picture.cc +++ b/impeller/aiks/picture.cc @@ -8,26 +8,10 @@ #include #include "impeller/base/validation.h" -#include "impeller/entity/entity.h" #include "impeller/renderer/render_target.h" -#include "impeller/renderer/snapshot.h" namespace impeller { -std::optional Picture::Snapshot(AiksContext& context) { - auto coverage = pass->GetElementsCoverage(std::nullopt); - if (!coverage.has_value() || coverage->IsEmpty()) { - return std::nullopt; - } - - const auto translate = Matrix::MakeTranslation(-coverage->GetOrigin()); - auto texture = - RenderToTexture(context, ISize(coverage->GetSize()), translate); - return impeller::Snapshot{ - .texture = std::move(texture), - .transform = Matrix::MakeTranslation(coverage->GetOrigin())}; -} - std::shared_ptr Picture::ToImage(AiksContext& context, ISize size) const { if (size.IsEmpty()) { diff --git a/impeller/aiks/picture.h b/impeller/aiks/picture.h index f461c7ff3f15c..2d783cffeed05 100644 --- a/impeller/aiks/picture.h +++ b/impeller/aiks/picture.h @@ -16,8 +16,6 @@ namespace impeller { struct Picture { std::unique_ptr pass; - std::optional Snapshot(AiksContext& context); - std::shared_ptr ToImage(AiksContext& context, ISize size) const; private: diff --git a/impeller/display_list/dl_dispatcher.cc b/impeller/display_list/dl_dispatcher.cc index 2c63029cd3480..18be21f0a4999 100644 --- a/impeller/display_list/dl_dispatcher.cc +++ b/impeller/display_list/dl_dispatcher.cc @@ -723,22 +723,18 @@ void DlDispatcherBase::saveLayer(const SkRect& bounds, ? ContentBoundsPromise::kMayClipContents : ContentBoundsPromise::kContainsContents; std::optional impeller_bounds; + // If the content is unbounded but has developer specified bounds, we take + // the original bounds so that we clip the content as expected. if (!options.content_is_unbounded() || options.bounds_from_caller()) { impeller_bounds = skia_conversions::ToRect(bounds); } - // Empty bounds on a save layer that contains a BDF or destructive blend - // should be treated as unbounded. All other empty bounds can be skipped. - if (impeller_bounds.has_value() && impeller_bounds->IsEmpty() && - (backdrop != nullptr || - Entity::IsBlendModeDestructive(paint.blend_mode))) { - impeller_bounds = std::nullopt; - } - - GetCanvas().SaveLayer(paint, impeller_bounds, ToImageFilter(backdrop), - promise, total_content_depth, - options.can_distribute_opacity(), - options.bounds_from_caller()); + GetCanvas().SaveLayer( + paint, impeller_bounds, ToImageFilter(backdrop), promise, + total_content_depth, + // Unbounded content can still have user specified bounds that require a + // saveLayer to be created to perform the clip. + options.can_distribute_opacity() && !options.content_is_unbounded()); } // |flutter::DlOpReceiver| diff --git a/impeller/entity/BUILD.gn b/impeller/entity/BUILD.gn index 0dfef176863b2..9bfc71f8ccbbf 100644 --- a/impeller/entity/BUILD.gn +++ b/impeller/entity/BUILD.gn @@ -206,6 +206,8 @@ impeller_component("entity") { "inline_pass_context.h", "render_target_cache.cc", "render_target_cache.h", + "save_layer_utils.cc", + "save_layer_utils.h", ] public_deps = [ @@ -260,6 +262,7 @@ impeller_component("entity_unittests") { "entity_unittests.cc", "geometry/geometry_unittests.cc", "render_target_cache_unittests.cc", + "save_layer_utils_unittests.cc", ] deps = [ diff --git a/impeller/entity/entity_pass.cc b/impeller/entity/entity_pass.cc index b238a2c3296e5..01c8d4b11dd25 100644 --- a/impeller/entity/entity_pass.cc +++ b/impeller/entity/entity_pass.cc @@ -24,6 +24,7 @@ #include "impeller/entity/entity.h" #include "impeller/entity/entity_pass_clip_stack.h" #include "impeller/entity/inline_pass_context.h" +#include "impeller/entity/save_layer_utils.h" #include "impeller/geometry/color.h" #include "impeller/geometry/rect.h" #include "impeller/geometry/size.h" @@ -60,48 +61,14 @@ void EntityPass::SetDelegate(std::shared_ptr delegate) { delegate_ = std::move(delegate); } -void EntityPass::SetBoundsLimit(std::optional bounds_limit, - ContentBoundsPromise bounds_promise) { +void EntityPass::SetBoundsLimit(std::optional bounds_limit) { bounds_limit_ = bounds_limit; - bounds_promise_ = bounds_limit.has_value() ? bounds_promise - : ContentBoundsPromise::kUnknown; } std::optional EntityPass::GetBoundsLimit() const { return bounds_limit_; } -bool EntityPass::GetBoundsLimitMightClipContent() const { - switch (bounds_promise_) { - case ContentBoundsPromise::kUnknown: - // If the promise is unknown due to not having a bounds limit, - // then no clipping will occur. But if we have a bounds limit - // and it is unkown, then we can make no promises about whether - // it causes clipping of the entity pass contents and we - // conservatively return true. - return bounds_limit_.has_value(); - case ContentBoundsPromise::kContainsContents: - FML_DCHECK(bounds_limit_.has_value()); - return false; - case ContentBoundsPromise::kMayClipContents: - FML_DCHECK(bounds_limit_.has_value()); - return true; - } - FML_UNREACHABLE(); -} - -bool EntityPass::GetBoundsLimitIsSnug() const { - switch (bounds_promise_) { - case ContentBoundsPromise::kUnknown: - return false; - case ContentBoundsPromise::kContainsContents: - case ContentBoundsPromise::kMayClipContents: - FML_DCHECK(bounds_limit_.has_value()); - return true; - } - FML_UNREACHABLE(); -} - void EntityPass::AddEntity(Entity entity) { if (entity.GetBlendMode() == BlendMode::kSourceOver && entity.GetContents()->IsOpaque(entity.GetTransform())) { @@ -160,115 +127,6 @@ size_t EntityPass::GetSubpassesDepth() const { return max_subpass_depth + 1u; } -std::optional EntityPass::GetElementsCoverage( - std::optional coverage_limit) const { - std::optional accumulated_coverage; - for (const auto& element : elements_) { - std::optional element_coverage; - - if (auto entity = std::get_if(&element)) { - element_coverage = entity->GetCoverage(); - - // When the coverage limit is std::nullopt, that means there is no limit, - // as opposed to empty coverage. - if (element_coverage.has_value() && coverage_limit.has_value()) { - const auto* filter = entity->GetContents()->AsFilter(); - if (!filter || filter->IsTranslationOnly()) { - element_coverage = - element_coverage->Intersection(coverage_limit.value()); - } - } - } else if (auto subpass_ptr = - std::get_if>(&element)) { - auto& subpass = *subpass_ptr->get(); - - std::optional unfiltered_coverage = - GetSubpassCoverage(subpass, std::nullopt); - - // If the current pass elements have any coverage so far and there's a - // backdrop filter, then incorporate the backdrop filter in the - // pre-filtered coverage of the subpass. - if (accumulated_coverage.has_value() && subpass.backdrop_filter_proc_) { - std::shared_ptr backdrop_filter = - subpass.backdrop_filter_proc_( - FilterInput::Make(accumulated_coverage.value()), - subpass.transform_, - Entity::RenderingMode::kSubpassAppendSnapshotTransform); - if (backdrop_filter) { - auto backdrop_coverage = backdrop_filter->GetCoverage({}); - unfiltered_coverage = - Rect::Union(unfiltered_coverage, backdrop_coverage); - } else { - VALIDATION_LOG << "The EntityPass backdrop filter proc didn't return " - "a valid filter."; - } - } - - if (!unfiltered_coverage.has_value()) { - continue; - } - - // Additionally, subpass textures may be passed through filters, which may - // modify the coverage. - // - // Note that we currently only assume that ImageFilters (such as blurs and - // matrix transforms) may modify coverage, although it's technically - // possible ColorFilters to affect coverage as well. For example: A - // ColorMatrixFilter could output a completely transparent result, and - // we could potentially detect this case as zero coverage in the future. - std::shared_ptr image_filter = - subpass.delegate_->WithImageFilter(*unfiltered_coverage, - subpass.transform_); - if (image_filter) { - Entity subpass_entity; - subpass_entity.SetTransform(subpass.transform_); - element_coverage = image_filter->GetCoverage(subpass_entity); - } else { - element_coverage = unfiltered_coverage; - } - - element_coverage = Rect::Intersection(element_coverage, coverage_limit); - } else { - FML_UNREACHABLE(); - } - - accumulated_coverage = Rect::Union(accumulated_coverage, element_coverage); - } - return accumulated_coverage; -} - -std::optional EntityPass::GetSubpassCoverage( - const EntityPass& subpass, - std::optional coverage_limit) const { - if (subpass.bounds_limit_.has_value() && subpass.GetBoundsLimitIsSnug()) { - return subpass.bounds_limit_->TransformBounds(subpass.transform_); - } - - std::shared_ptr image_filter = - subpass.delegate_->WithImageFilter(Rect(), subpass.transform_); - - // If the subpass has an image filter, then its coverage space may deviate - // from the parent pass and make intersecting with the pass coverage limit - // unsafe. - if (image_filter && coverage_limit.has_value()) { - coverage_limit = image_filter->GetSourceCoverage(subpass.transform_, - coverage_limit.value()); - } - - auto entities_coverage = subpass.GetElementsCoverage(coverage_limit); - // The entities don't cover anything. There is nothing to do. - if (!entities_coverage.has_value()) { - return std::nullopt; - } - - if (!subpass.bounds_limit_.has_value()) { - return entities_coverage; - } - auto user_bounds_coverage = - subpass.bounds_limit_->TransformBounds(subpass.transform_); - return entities_coverage->Intersection(user_bounds_coverage); -} - EntityPass* EntityPass::GetSuperpass() const { return superpass_; } @@ -641,10 +499,18 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( return EntityPass::EntityResult::Skip(); } - auto subpass_coverage = - (subpass->flood_clip_ || subpass_backdrop_filter_contents) - ? coverage_limit - : GetSubpassCoverage(*subpass, coverage_limit); + std::shared_ptr image_filter = + subpass->delegate_->WithImageFilter(Rect(), subpass->transform_); + + auto subpass_coverage = ComputeSaveLayerCoverage( + subpass->bounds_limit_.value_or(Rect::MakeMaximum()), // + subpass->transform_, // + coverage_limit.value(), // + image_filter, // + /*flood_output_coverage=*/subpass->flood_clip_, // + /*flood_input_coverage=*/!!subpass_backdrop_filter_contents // + ); + if (!subpass_coverage.has_value()) { return EntityPass::EntityResult::Skip(); } diff --git a/impeller/entity/entity_pass.h b/impeller/entity/entity_pass.h index 38e5d28d44231..8df8f39e21a30 100644 --- a/impeller/entity/entity_pass.h +++ b/impeller/entity/entity_pass.h @@ -67,32 +67,12 @@ class EntityPass { void SetDelegate(std::shared_ptr delgate); - /// @brief Set the bounds limit, which is provided by the user when creating - /// a SaveLayer. This is a hint that allows the user to communicate - /// that it's OK to not render content outside of the bounds. - /// - /// For consistency with Skia, we effectively treat this like a - /// rectangle clip by forcing the subpass texture size to never exceed - /// it. - /// - /// The entity pass will assume that these bounds cause a clipping - /// effect on the layer unless this call is followed up with a - /// call to |SetBoundsClipsContent()| specifying otherwise. - void SetBoundsLimit( - std::optional bounds_limit, - ContentBoundsPromise bounds_promise = ContentBoundsPromise::kUnknown); - - /// @brief Get the bounds limit, which is provided by the user when creating - /// a SaveLayer. - std::optional GetBoundsLimit() const; + /// @brief Set the computed content bounds, or std::nullopt if the contents + /// are unbounded. + void SetBoundsLimit(std::optional content_bounds); - /// @brief Indicates if the bounds limit set using |SetBoundsLimit()| - /// might clip the contents of the pass. - bool GetBoundsLimitMightClipContent() const; - - /// @brief Indicates if the bounds limit set using |SetBoundsLimit()| - /// is a reasonably tight estimate of the bounds of the contents. - bool GetBoundsLimitIsSnug() const; + /// @brief Get the bounds limit. + std::optional GetBoundsLimit() const; size_t GetSubpassesDepth() const; @@ -181,30 +161,6 @@ class EntityPass { required_mip_count_ = mip_count; } - //---------------------------------------------------------------------------- - /// @brief Computes the coverage of a given subpass. This is used to - /// determine the texture size of a given subpass before it's rendered - /// to and passed through the subpass ImageFilter, if any. - /// - /// @param[in] subpass The EntityPass for which to compute - /// pre-filteredcoverage. - /// @param[in] coverage_limit Confines coverage to a specified area. This - /// hint is used to trim coverage to the root - /// framebuffer area. `std::nullopt` means there - /// is no limit. - /// - /// @return The screen space pixel area that the subpass contents will render - /// into, prior to being transformed by the subpass ImageFilter, if - /// any. `std::nullopt` means rendering the subpass will have no - /// effect on the color attachment. - /// - std::optional GetSubpassCoverage( - const EntityPass& subpass, - std::optional coverage_limit) const; - - std::optional GetElementsCoverage( - std::optional coverage_limit) const; - private: struct EntityResult { enum Status { @@ -333,7 +289,6 @@ class EntityPass { BlendMode blend_mode_ = BlendMode::kSourceOver; bool flood_clip_ = false; std::optional bounds_limit_; - ContentBoundsPromise bounds_promise_ = ContentBoundsPromise::kUnknown; int32_t required_mip_count_ = 1; /// These values indicate whether something has been added to the EntityPass diff --git a/impeller/entity/entity_unittests.cc b/impeller/entity/entity_unittests.cc index 563c19af04c04..34d5b35ae88f4 100644 --- a/impeller/entity/entity_unittests.cc +++ b/impeller/entity/entity_unittests.cc @@ -119,73 +119,10 @@ auto CreatePassWithRectPath( PathBuilder{}.AddRect(rect).TakePath(), Color::Red())); subpass->AddEntity(std::move(entity)); subpass->SetDelegate(std::make_unique(collapse)); - subpass->SetBoundsLimit(bounds_hint, bounds_promise); + subpass->SetBoundsLimit(bounds_hint); return subpass; } -TEST_P(EntityTest, EntityPassRespectsUntrustedSubpassBoundsLimit) { - EntityPass pass; - - auto subpass0 = CreatePassWithRectPath(Rect::MakeLTRB(0, 0, 100, 100), - Rect::MakeLTRB(50, 50, 150, 150)); - auto subpass1 = CreatePassWithRectPath(Rect::MakeLTRB(500, 500, 1000, 1000), - Rect::MakeLTRB(800, 800, 900, 900)); - - auto subpass0_coverage = - pass.GetSubpassCoverage(*subpass0.get(), std::nullopt); - ASSERT_TRUE(subpass0_coverage.has_value()); - ASSERT_RECT_NEAR(subpass0_coverage.value(), Rect::MakeLTRB(50, 50, 100, 100)); - - auto subpass1_coverage = - pass.GetSubpassCoverage(*subpass1.get(), std::nullopt); - ASSERT_TRUE(subpass1_coverage.has_value()); - ASSERT_RECT_NEAR(subpass1_coverage.value(), - Rect::MakeLTRB(800, 800, 900, 900)); - - pass.AddSubpass(std::move(subpass0)); - pass.AddSubpass(std::move(subpass1)); - - auto coverage = pass.GetElementsCoverage(std::nullopt); - ASSERT_TRUE(coverage.has_value()); - ASSERT_RECT_NEAR(coverage.value(), Rect::MakeLTRB(50, 50, 900, 900)); -} - -TEST_P(EntityTest, EntityPassTrustsSnugSubpassBoundsLimit) { - EntityPass pass; - - auto subpass0 = // - CreatePassWithRectPath(Rect::MakeLTRB(10, 10, 90, 90), - Rect::MakeLTRB(5, 5, 95, 95), - ContentBoundsPromise::kContainsContents); - auto subpass1 = // - CreatePassWithRectPath(Rect::MakeLTRB(500, 500, 1000, 1000), - Rect::MakeLTRB(495, 495, 1005, 1005), - ContentBoundsPromise::kContainsContents); - - auto subpass0_coverage = - pass.GetSubpassCoverage(*subpass0.get(), std::nullopt); - EXPECT_TRUE(subpass0_coverage.has_value()); - // Result should be the overridden bounds - // (we lied about them being snug, but the property is respected) - EXPECT_RECT_NEAR(subpass0_coverage.value(), Rect::MakeLTRB(5, 5, 95, 95)); - - auto subpass1_coverage = - pass.GetSubpassCoverage(*subpass1.get(), std::nullopt); - EXPECT_TRUE(subpass1_coverage.has_value()); - // Result should be the overridden bounds - // (we lied about them being snug, but the property is respected) - EXPECT_RECT_NEAR(subpass1_coverage.value(), - Rect::MakeLTRB(495, 495, 1005, 1005)); - - pass.AddSubpass(std::move(subpass0)); - pass.AddSubpass(std::move(subpass1)); - - auto coverage = pass.GetElementsCoverage(std::nullopt); - EXPECT_TRUE(coverage.has_value()); - // This result should be the union of the overridden bounds - EXPECT_RECT_NEAR(coverage.value(), Rect::MakeLTRB(5, 5, 1005, 1005)); -} - TEST_P(EntityTest, EntityPassCanMergeSubpassIntoParent) { // Both a red and a blue box should appear if the pass merging has worked // correctly. @@ -208,36 +145,6 @@ TEST_P(EntityTest, EntityPassCanMergeSubpassIntoParent) { ASSERT_TRUE(OpenPlaygroundHere(pass)); } -TEST_P(EntityTest, EntityPassCoverageRespectsCoverageLimit) { - // Rect is drawn entirely in negative area. - auto pass = CreatePassWithRectPath(Rect::MakeLTRB(-200, -200, -100, -100), - std::nullopt); - - // Without coverage limit. - { - auto pass_coverage = pass->GetElementsCoverage(std::nullopt); - ASSERT_TRUE(pass_coverage.has_value()); - ASSERT_RECT_NEAR(pass_coverage.value(), - Rect::MakeLTRB(-200, -200, -100, -100)); - } - - // With limit that doesn't overlap. - { - auto pass_coverage = - pass->GetElementsCoverage(Rect::MakeLTRB(0, 0, 100, 100)); - ASSERT_FALSE(pass_coverage.has_value()); - } - - // With limit that partially overlaps. - { - auto pass_coverage = - pass->GetElementsCoverage(Rect::MakeLTRB(-150, -150, 0, 0)); - ASSERT_TRUE(pass_coverage.has_value()); - ASSERT_RECT_NEAR(pass_coverage.value(), - Rect::MakeLTRB(-150, -150, -100, -100)); - } -} - TEST_P(EntityTest, FilterCoverageRespectsCropRect) { auto image = CreateTextureForFixture("boston.jpg"); auto filter = ColorFilterContents::MakeBlend(BlendMode::kSoftLight, diff --git a/impeller/entity/save_layer_utils.cc b/impeller/entity/save_layer_utils.cc new file mode 100644 index 0000000000000..68abbf7a9c2a9 --- /dev/null +++ b/impeller/entity/save_layer_utils.cc @@ -0,0 +1,90 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "impeller/entity/save_layer_utils.h" + +namespace impeller { + +std::optional ComputeSaveLayerCoverage( + const Rect& content_coverage, + const Matrix& effect_transform, + const Rect& coverage_limit, + const std::shared_ptr& image_filter, + bool flood_output_coverage, + bool flood_input_coverage) { + Rect coverage = content_coverage; + // There are three conditions that should cause input coverage to flood, the + // first is the presence of a backdrop filter on the saveLayer. The second is + // the presence of a color filter that effects transparent black on the + // saveLayer. The last is the presence of unbounded content within the + // saveLayer (such as a drawPaint, bdf, et cetera). Note that currently + // only the presence of a backdrop filter impacts this flag, while color + // filters are not yet handled + // (https://github.com/flutter/flutter/issues/154035) and unbounded coverage + // is handled in the display list dispatcher. + // + // Backdrop filters apply before the saveLayer is restored. The presence of + // a backdrop filter causes the content coverage of the saveLayer to be + // unbounded. + // + // If there is a color filter that needs to flood its output. The color filter + // is applied before any image filters, so this floods input coverage and not + // the output coverage. Technically, we only need to flood the output of the + // color filter and could allocate a render target sized just to the content, + // but we don't currenty have the means to do so. Flooding the coverage is a + // non-optimal but technically correct way to render this. + // + // If the saveLayer contains unbounded content, then at this point the + // dl_dispatcher will have set content coverage to Rect::MakeMaximum(). + if (flood_input_coverage) { + coverage = Rect::MakeMaximum(); + } + + // The content coverage must be scaled by any image filters present on the + // saveLayer paint. For example, if a saveLayer has a coverage limit of + // 100x100, but it has a Matrix image filter that scales by one half, the + // actual coverage limit is 200x200. + if (image_filter) { + // Transform the input coverage into the global coordinate space before + // computing the bounds limit intersection. This is the "worst case" + // coverage value before we intersect with the content coverage below. + std::optional source_coverage_limit = + image_filter->GetSourceCoverage(effect_transform, coverage_limit); + if (!source_coverage_limit.has_value()) { + // No intersection with parent coverage limit. + return std::nullopt; + } + // The image filter may change the coverage limit required to flood + // the parent layer. Returning the source coverage limit so that we + // can guarantee the render target is larger enough. + // + // See note below on flood_output_coverage. + if (flood_output_coverage || coverage.IsMaximum()) { + return source_coverage_limit; + } + + return coverage.TransformBounds(effect_transform) + .Intersection(source_coverage_limit.value()); + } + + // If the input coverage is maximum, just return the coverage limit that + // is already in the global coordinate space. + // + // If flood_output_coverage is true, then the restore is applied with a + // destructive blend mode that requires flooding to the coverage limit. + // Technically we could only allocated a render target as big as the input + // coverage and then use a decal sampling mode to perform the flood. Returning + // the coverage limit is a correct but non optimal means of ensuring correct + // rendering. + if (flood_output_coverage || coverage.IsMaximum()) { + return coverage_limit; + } + + // Transform the input coverage into the global coordinate space before + // computing the bounds limit intersection. + return coverage.TransformBounds(effect_transform) + .Intersection(coverage_limit); +} + +} // namespace impeller diff --git a/impeller/entity/save_layer_utils.h b/impeller/entity/save_layer_utils.h new file mode 100644 index 0000000000000..417fdad9444da --- /dev/null +++ b/impeller/entity/save_layer_utils.h @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_IMPELLER_ENTITY_SAVE_LAYER_UTILS_H_ +#define FLUTTER_IMPELLER_ENTITY_SAVE_LAYER_UTILS_H_ + +#include +#include + +#include "impeller/entity/contents/filters/filter_contents.h" +#include "impeller/geometry/rect.h" + +namespace impeller { + +/// @brief Compute the coverage of a subpass in the global coordinate space. +/// +/// @param content_coverage the computed coverage of the contents of the save +/// layer. This value may be empty if the save layer has +/// no contents, or Rect::Maximum if the contents are +/// unbounded (like a destructive blend). +/// +/// @param effect_transform The CTM of the subpass. +/// @param coverage_limit The current clip coverage. This is used to bound the +/// subpass size. +/// @param image_filter A subpass image filter, or nullptr. +/// @param flood_output_coverage Whether the coverage should be flooded to clip +/// coverage regardless of input coverage. This should be set to true when the +/// restore Paint has a destructive blend mode. +/// @param flood_input_coverage Whther the content coverage should be flooded. +/// This should be set to true if the paint has a backdrop filter or if there is +/// a transparent black effecting color filter. +/// +/// The coverage computation expects `content_coverage` to be in the child +/// coordinate space. `effect_transform` is used to transform this back into the +/// global coordinate space. A return value of std::nullopt indicates that the +/// coverage is empty or otherwise does not intersect with the parent coverage +/// limit and should be discarded. +std::optional ComputeSaveLayerCoverage( + const Rect& content_coverage, + const Matrix& effect_transform, + const Rect& coverage_limit, + const std::shared_ptr& image_filter, + bool flood_output_coverage = false, + bool flood_input_coverage = false); + +} // namespace impeller + +#endif // FLUTTER_IMPELLER_ENTITY_SAVE_LAYER_UTILS_H_ diff --git a/impeller/entity/save_layer_utils_unittests.cc b/impeller/entity/save_layer_utils_unittests.cc new file mode 100644 index 0000000000000..aa5dfd92cae25 --- /dev/null +++ b/impeller/entity/save_layer_utils_unittests.cc @@ -0,0 +1,230 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/testing/testing.h" +#include "impeller/entity/contents/filters/filter_contents.h" +#include "impeller/entity/save_layer_utils.h" + +// TODO(zanderso): https://github.com/flutter/flutter/issues/127701 +// NOLINTBEGIN(bugprone-unchecked-optional-access) + +namespace impeller { +namespace testing { + +using SaveLayerUtilsTest = ::testing::Test; + +TEST(SaveLayerUtilsTest, SimplePaintComputedCoverage) { + // Basic Case, simple paint, computed coverage + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 10, 10), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/nullptr // + ); + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 10, 10)); +} + +TEST(SaveLayerUtilsTest, BackdropFiterComputedCoverage) { + // Backdrop Filter, computed coverage + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 10, 10), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/nullptr, + /*flood_output_coverage=*/false, // + /*flood_input_coverage=*/true // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 2400, 1800)); +} + +TEST(SaveLayerUtilsTest, ImageFiterComputedCoverage) { + // Image Filter, computed coverage + auto image_filter = FilterContents::MakeMatrixFilter( + FilterInput::Make(Rect()), Matrix::MakeScale({2, 2, 1}), {}); + + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 10, 10), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/image_filter // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 10, 10)); +} + +TEST(SaveLayerUtilsTest, + ImageFiterSmallScaleComputedCoverageLargerThanBoundsLimit) { + // Image Filter scaling large, computed coverage is larger than bounds limit. + auto image_filter = FilterContents::MakeMatrixFilter( + FilterInput::Make(Rect()), Matrix::MakeScale({2, 2, 1}), {}); + + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 10, 10), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 5, 5), // + /*image_filter=*/image_filter // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 2.5, 2.5)); +} + +TEST(SaveLayerUtilsTest, + ImageFiterLargeScaleComputedCoverageLargerThanBoundsLimit) { + // Image Filter scaling small, computed coverage is larger than bounds limit. + auto image_filter = FilterContents::MakeMatrixFilter( + FilterInput::Make(Rect()), Matrix::MakeScale({0.5, 0.5, 1}), {}); + + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 10, 10), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 5, 5), // + /*image_filter=*/image_filter // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 10, 10)); +} + +TEST(SaveLayerUtilsTest, DisjointCoverage) { + // No intersection in coverage + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(200, 200, 210, 210), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 100, 100), // + /*image_filter=*/nullptr // + ); + + EXPECT_FALSE(coverage.has_value()); +} + +TEST(SaveLayerUtilsTest, DisjointCoverageTransformedByImageFilter) { + // Coverage disjoint from parent coverage but transformed into parent space + // with image filter. + auto image_filter = FilterContents::MakeMatrixFilter( + FilterInput::Make(Rect()), Matrix::MakeTranslation({-200, -200, 0}), {}); + + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(200, 200, 210, 210), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 100, 100), // + /*image_filter=*/image_filter // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(200, 200, 210, 210)); +} + +TEST(SaveLayerUtilsTest, DisjointCoveragTransformedByCTM) { + // Coverage disjoint from parent coverage. + Matrix ctm = Matrix::MakeTranslation({-200, -200, 0}); + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(200, 200, 210, 210), // + /*effect_transform=*/ctm, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 100, 100), // + /*image_filter=*/nullptr // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 10, 10)); +} + +TEST(SaveLayerUtilsTest, BasicEmptyCoverage) { + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 0, 0), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/nullptr // + ); + + ASSERT_FALSE(coverage.has_value()); +} + +TEST(SaveLayerUtilsTest, ImageFilterEmptyCoverage) { + // Empty coverage with Image Filter + auto image_filter = FilterContents::MakeMatrixFilter( + FilterInput::Make(Rect()), Matrix::MakeTranslation({-200, -200, 0}), {}); + + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 0, 0), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/image_filter // + ); + + ASSERT_FALSE(coverage.has_value()); +} + +TEST(SaveLayerUtilsTest, BackdropFilterEmptyCoverage) { + // Empty coverage with backdrop filter. + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 0, 0), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/nullptr, // + /*flood_output_coverage=*/true // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 2400, 1800)); +} + +TEST(SaveLayerUtilsTest, FloodInputCoverage) { + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 0, 0), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/nullptr, // + /*flood_output_coverage=*/false, // + /*flood_input_coverage=*/true // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 2400, 1800)); +} + +TEST(SaveLayerUtilsTest, FloodInputCoverageWithImageFilter) { + auto image_filter = FilterContents::MakeMatrixFilter( + FilterInput::Make(Rect()), Matrix::MakeScale({0.5, 0.5, 1}), {}); + + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 0, 0), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/image_filter, // + /*flood_output_coverage=*/false, // + /*flood_input_coverage=*/true // + ); + + ASSERT_TRUE(coverage.has_value()); + EXPECT_EQ(coverage.value(), Rect::MakeLTRB(0, 0, 4800, 3600)); +} + +TEST(SaveLayerUtilsTest, + FloodInputCoverageWithImageFilterWithNoCoverageProducesNoCoverage) { + // Even if we flood the input coverage due to a bdf, we can still cull out the + // layer if the image filter results in no coverage. + auto image_filter = FilterContents::MakeMatrixFilter( + FilterInput::Make(Rect()), Matrix::MakeScale({1, 1, 0}), {}); + + auto coverage = ComputeSaveLayerCoverage( + /*content_coverage=*/Rect::MakeLTRB(0, 0, 0, 0), // + /*effect_transform=*/{}, // + /*coverage_limit=*/Rect::MakeLTRB(0, 0, 2400, 1800), // + /*image_filter=*/image_filter, // + /*flood_output_coverage=*/false, // + /*flood_input_coverage=*/true // + ); + + ASSERT_FALSE(coverage.has_value()); +} + +} // namespace testing +} // namespace impeller + +// NOLINTEND(bugprone-unchecked-optional-access)