[Impeller] separate algorithm for computing render target size. (flutter/engine#54604)

Separate out the math for computing a saveLayer's allocated texture size based on input bounds, paint, bdf, coverage limit, et cetera.
This commit is contained in:
Jonah Williams 2024-08-30 17:42:56 -07:00 committed by GitHub
parent 4994d9c4d6
commit 060d89a46f
17 changed files with 439 additions and 363 deletions

View File

@ -173,6 +173,7 @@
../../../flutter/impeller/entity/entity_unittests.cc ../../../flutter/impeller/entity/entity_unittests.cc
../../../flutter/impeller/entity/geometry/geometry_unittests.cc ../../../flutter/impeller/entity/geometry/geometry_unittests.cc
../../../flutter/impeller/entity/render_target_cache_unittests.cc ../../../flutter/impeller/entity/render_target_cache_unittests.cc
../../../flutter/impeller/entity/save_layer_utils_unittests.cc
../../../flutter/impeller/fixtures ../../../flutter/impeller/fixtures
../../../flutter/impeller/geometry/README.md ../../../flutter/impeller/geometry/README.md
../../../flutter/impeller/geometry/geometry_unittests.cc ../../../flutter/impeller/geometry/geometry_unittests.cc

View File

@ -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/inline_pass_context.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/render_target_cache.cc + ../../../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/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.frag + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/shaders/blending/advanced_blend.vert + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/shaders/blending/advanced_blend.vert + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/shaders/blending/blend_select.glsl + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/shaders/blending/blend_select.glsl + ../../../flutter/LICENSE
@ -45761,6 +45763,8 @@ FILE: ../../../flutter/impeller/entity/inline_pass_context.cc
FILE: ../../../flutter/impeller/entity/inline_pass_context.h FILE: ../../../flutter/impeller/entity/inline_pass_context.h
FILE: ../../../flutter/impeller/entity/render_target_cache.cc FILE: ../../../flutter/impeller/entity/render_target_cache.cc
FILE: ../../../flutter/impeller/entity/render_target_cache.h 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.frag
FILE: ../../../flutter/impeller/entity/shaders/blending/advanced_blend.vert FILE: ../../../flutter/impeller/entity/shaders/blending/advanced_blend.vert
FILE: ../../../flutter/impeller/entity/shaders/blending/blend_select.glsl FILE: ../../../flutter/impeller/entity/shaders/blending/blend_select.glsl

View File

@ -127,12 +127,13 @@ TEST_P(AiksTest, ColorWheel) {
draw_color_wheel(canvas); draw_color_wheel(canvas);
auto color_wheel_picture = canvas.EndRecordingAsPicture(); auto color_wheel_picture = canvas.EndRecordingAsPicture();
auto snapshot = color_wheel_picture.Snapshot(renderer); auto image = color_wheel_picture.ToImage(
if (!snapshot.has_value() || !snapshot->texture) { renderer, ISize{GetWindowSize().width, GetWindowSize().height});
if (!image) {
return std::nullopt; return std::nullopt;
} }
color_wheel_image = snapshot->texture; color_wheel_image = image;
color_wheel_transform = snapshot->transform; color_wheel_transform = Matrix();
} }
Canvas canvas; Canvas canvas;

View File

@ -814,8 +814,7 @@ void Canvas::SaveLayer(const Paint& paint,
const std::shared_ptr<ImageFilter>& backdrop_filter, const std::shared_ptr<ImageFilter>& backdrop_filter,
ContentBoundsPromise bounds_promise, ContentBoundsPromise bounds_promise,
uint32_t total_content_depth, uint32_t total_content_depth,
bool can_distribute_opacity, bool can_distribute_opacity) {
bool bounds_from_caller) {
if (can_distribute_opacity && !backdrop_filter && if (can_distribute_opacity && !backdrop_filter &&
Paint::CanApplyOpacityPeephole(paint) && Paint::CanApplyOpacityPeephole(paint) &&
bounds_promise != ContentBoundsPromise::kMayClipContents) { bounds_promise != ContentBoundsPromise::kMayClipContents) {
@ -837,7 +836,7 @@ void Canvas::SaveLayer(const Paint& paint,
auto& new_layer_pass = GetCurrentPass(); auto& new_layer_pass = GetCurrentPass();
if (bounds) { if (bounds) {
new_layer_pass.SetBoundsLimit(bounds, bounds_promise); new_layer_pass.SetBoundsLimit(bounds);
} }
// When applying a save layer, absorb any pending distributed opacity. // When applying a save layer, absorb any pending distributed opacity.

View File

@ -76,8 +76,7 @@ class Canvas {
const std::shared_ptr<ImageFilter>& backdrop_filter = nullptr, const std::shared_ptr<ImageFilter>& backdrop_filter = nullptr,
ContentBoundsPromise bounds_promise = ContentBoundsPromise::kUnknown, ContentBoundsPromise bounds_promise = ContentBoundsPromise::kUnknown,
uint32_t total_content_depth = kMaxDepth, uint32_t total_content_depth = kMaxDepth,
bool can_distribute_opacity = false, bool can_distribute_opacity = false);
bool bounds_from_caller = false);
virtual bool Restore(); virtual bool Restore();

View File

@ -12,10 +12,12 @@
#include "impeller/core/allocator.h" #include "impeller/core/allocator.h"
#include "impeller/core/formats.h" #include "impeller/core/formats.h"
#include "impeller/entity/contents/clip_contents.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/framebuffer_blend_contents.h"
#include "impeller/entity/contents/text_contents.h" #include "impeller/entity/contents/text_contents.h"
#include "impeller/entity/entity.h" #include "impeller/entity/entity.h"
#include "impeller/entity/entity_pass_clip_stack.h" #include "impeller/entity/entity_pass_clip_stack.h"
#include "impeller/entity/save_layer_utils.h"
#include "impeller/geometry/color.h" #include "impeller/geometry/color.h"
#include "impeller/renderer/render_target.h" #include "impeller/renderer/render_target.h"
@ -314,15 +316,9 @@ void ExperimentalCanvas::SaveLayer(
const std::shared_ptr<ImageFilter>& backdrop_filter, const std::shared_ptr<ImageFilter>& backdrop_filter,
ContentBoundsPromise bounds_promise, ContentBoundsPromise bounds_promise,
uint32_t total_content_depth, uint32_t total_content_depth,
bool can_distribute_opacity, bool can_distribute_opacity) {
bool bounds_from_caller) {
TRACE_EVENT0("flutter", "Canvas::saveLayer"); TRACE_EVENT0("flutter", "Canvas::saveLayer");
if (bounds.has_value() && bounds->IsEmpty()) {
Save(total_content_depth);
return;
}
if (!clip_coverage_stack_.HasCoverage()) { if (!clip_coverage_stack_.HasCoverage()) {
// The current clip is empty. This means the pass texture won't be // The current clip is empty. This means the pass texture won't be
// visible, so skip it. // visible, so skip it.
@ -346,13 +342,6 @@ void ExperimentalCanvas::SaveLayer(
->GetSize())) ->GetSize()))
.Intersection(current_clip_coverage); .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()) { if (!maybe_coverage_limit.has_value() || maybe_coverage_limit->IsEmpty()) {
Save(total_content_depth); Save(total_content_depth);
return; return;
@ -367,12 +356,33 @@ void ExperimentalCanvas::SaveLayer(
return; return;
} }
std::shared_ptr<FilterContents> filter_contents;
if (paint.image_filter) {
filter_contents = paint.image_filter->GetFilterContents();
}
std::optional<Rect> 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. // Backdrop filter state, ignored if there is no BDF.
std::shared_ptr<FilterContents> backdrop_filter_contents; std::shared_ptr<FilterContents> backdrop_filter_contents;
Point local_position = {0, 0}; Point local_position = {0, 0};
if (backdrop_filter) { if (backdrop_filter) {
local_position = local_position = subpass_coverage.GetOrigin() - GetGlobalPassPosition();
current_clip_coverage.GetOrigin() - GetGlobalPassPosition();
EntityPass::BackdropFilterProc backdrop_filter_proc = EntityPass::BackdropFilterProc backdrop_filter_proc =
[backdrop_filter = backdrop_filter->Clone()]( [backdrop_filter = backdrop_filter->Clone()](
const FilterInput::Ref& input, const Matrix& effect_transform, const FilterInput::Ref& input, const Matrix& effect_transform,
@ -409,21 +419,6 @@ void ExperimentalCanvas::SaveLayer(
paint_copy.color.alpha *= transform_stack_.back().distributed_opacity; paint_copy.color.alpha *= transform_stack_.back().distributed_opacity;
transform_stack_.back().distributed_opacity = 1.0; 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( render_passes_.push_back(LazyRenderingConfig(
renderer_, // renderer_, //
CreateRenderTarget(renderer_, // CreateRenderTarget(renderer_, //

View File

@ -71,8 +71,7 @@ class ExperimentalCanvas : public Canvas {
const std::shared_ptr<ImageFilter>& backdrop_filter, const std::shared_ptr<ImageFilter>& backdrop_filter,
ContentBoundsPromise bounds_promise, ContentBoundsPromise bounds_promise,
uint32_t total_content_depth, uint32_t total_content_depth,
bool can_distribute_opacity, bool can_distribute_opacity) override;
bool bounds_from_caller) override;
bool Restore() override; bool Restore() override;

View File

@ -8,26 +8,10 @@
#include <optional> #include <optional>
#include "impeller/base/validation.h" #include "impeller/base/validation.h"
#include "impeller/entity/entity.h"
#include "impeller/renderer/render_target.h" #include "impeller/renderer/render_target.h"
#include "impeller/renderer/snapshot.h"
namespace impeller { namespace impeller {
std::optional<Snapshot> 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<Texture> Picture::ToImage(AiksContext& context, std::shared_ptr<Texture> Picture::ToImage(AiksContext& context,
ISize size) const { ISize size) const {
if (size.IsEmpty()) { if (size.IsEmpty()) {

View File

@ -16,8 +16,6 @@ namespace impeller {
struct Picture { struct Picture {
std::unique_ptr<EntityPass> pass; std::unique_ptr<EntityPass> pass;
std::optional<Snapshot> Snapshot(AiksContext& context);
std::shared_ptr<Texture> ToImage(AiksContext& context, ISize size) const; std::shared_ptr<Texture> ToImage(AiksContext& context, ISize size) const;
private: private:

View File

@ -723,22 +723,18 @@ void DlDispatcherBase::saveLayer(const SkRect& bounds,
? ContentBoundsPromise::kMayClipContents ? ContentBoundsPromise::kMayClipContents
: ContentBoundsPromise::kContainsContents; : ContentBoundsPromise::kContainsContents;
std::optional<Rect> impeller_bounds; std::optional<Rect> 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()) { if (!options.content_is_unbounded() || options.bounds_from_caller()) {
impeller_bounds = skia_conversions::ToRect(bounds); impeller_bounds = skia_conversions::ToRect(bounds);
} }
// Empty bounds on a save layer that contains a BDF or destructive blend GetCanvas().SaveLayer(
// should be treated as unbounded. All other empty bounds can be skipped. paint, impeller_bounds, ToImageFilter(backdrop), promise,
if (impeller_bounds.has_value() && impeller_bounds->IsEmpty() && total_content_depth,
(backdrop != nullptr || // Unbounded content can still have user specified bounds that require a
Entity::IsBlendModeDestructive(paint.blend_mode))) { // saveLayer to be created to perform the clip.
impeller_bounds = std::nullopt; options.can_distribute_opacity() && !options.content_is_unbounded());
}
GetCanvas().SaveLayer(paint, impeller_bounds, ToImageFilter(backdrop),
promise, total_content_depth,
options.can_distribute_opacity(),
options.bounds_from_caller());
} }
// |flutter::DlOpReceiver| // |flutter::DlOpReceiver|

View File

@ -206,6 +206,8 @@ impeller_component("entity") {
"inline_pass_context.h", "inline_pass_context.h",
"render_target_cache.cc", "render_target_cache.cc",
"render_target_cache.h", "render_target_cache.h",
"save_layer_utils.cc",
"save_layer_utils.h",
] ]
public_deps = [ public_deps = [
@ -260,6 +262,7 @@ impeller_component("entity_unittests") {
"entity_unittests.cc", "entity_unittests.cc",
"geometry/geometry_unittests.cc", "geometry/geometry_unittests.cc",
"render_target_cache_unittests.cc", "render_target_cache_unittests.cc",
"save_layer_utils_unittests.cc",
] ]
deps = [ deps = [

View File

@ -24,6 +24,7 @@
#include "impeller/entity/entity.h" #include "impeller/entity/entity.h"
#include "impeller/entity/entity_pass_clip_stack.h" #include "impeller/entity/entity_pass_clip_stack.h"
#include "impeller/entity/inline_pass_context.h" #include "impeller/entity/inline_pass_context.h"
#include "impeller/entity/save_layer_utils.h"
#include "impeller/geometry/color.h" #include "impeller/geometry/color.h"
#include "impeller/geometry/rect.h" #include "impeller/geometry/rect.h"
#include "impeller/geometry/size.h" #include "impeller/geometry/size.h"
@ -60,48 +61,14 @@ void EntityPass::SetDelegate(std::shared_ptr<EntityPassDelegate> delegate) {
delegate_ = std::move(delegate); delegate_ = std::move(delegate);
} }
void EntityPass::SetBoundsLimit(std::optional<Rect> bounds_limit, void EntityPass::SetBoundsLimit(std::optional<Rect> bounds_limit) {
ContentBoundsPromise bounds_promise) {
bounds_limit_ = bounds_limit; bounds_limit_ = bounds_limit;
bounds_promise_ = bounds_limit.has_value() ? bounds_promise
: ContentBoundsPromise::kUnknown;
} }
std::optional<Rect> EntityPass::GetBoundsLimit() const { std::optional<Rect> EntityPass::GetBoundsLimit() const {
return bounds_limit_; 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) { void EntityPass::AddEntity(Entity entity) {
if (entity.GetBlendMode() == BlendMode::kSourceOver && if (entity.GetBlendMode() == BlendMode::kSourceOver &&
entity.GetContents()->IsOpaque(entity.GetTransform())) { entity.GetContents()->IsOpaque(entity.GetTransform())) {
@ -160,115 +127,6 @@ size_t EntityPass::GetSubpassesDepth() const {
return max_subpass_depth + 1u; return max_subpass_depth + 1u;
} }
std::optional<Rect> EntityPass::GetElementsCoverage(
std::optional<Rect> coverage_limit) const {
std::optional<Rect> accumulated_coverage;
for (const auto& element : elements_) {
std::optional<Rect> element_coverage;
if (auto entity = std::get_if<Entity>(&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<std::unique_ptr<EntityPass>>(&element)) {
auto& subpass = *subpass_ptr->get();
std::optional<Rect> 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<FilterContents> 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<FilterContents> 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<Rect> EntityPass::GetSubpassCoverage(
const EntityPass& subpass,
std::optional<Rect> coverage_limit) const {
if (subpass.bounds_limit_.has_value() && subpass.GetBoundsLimitIsSnug()) {
return subpass.bounds_limit_->TransformBounds(subpass.transform_);
}
std::shared_ptr<FilterContents> 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 { EntityPass* EntityPass::GetSuperpass() const {
return superpass_; return superpass_;
} }
@ -641,10 +499,18 @@ EntityPass::EntityResult EntityPass::GetEntityForElement(
return EntityPass::EntityResult::Skip(); return EntityPass::EntityResult::Skip();
} }
auto subpass_coverage = std::shared_ptr<FilterContents> image_filter =
(subpass->flood_clip_ || subpass_backdrop_filter_contents) subpass->delegate_->WithImageFilter(Rect(), subpass->transform_);
? coverage_limit
: GetSubpassCoverage(*subpass, coverage_limit); 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()) { if (!subpass_coverage.has_value()) {
return EntityPass::EntityResult::Skip(); return EntityPass::EntityResult::Skip();
} }

View File

@ -67,33 +67,13 @@ class EntityPass {
void SetDelegate(std::shared_ptr<EntityPassDelegate> delgate); void SetDelegate(std::shared_ptr<EntityPassDelegate> delgate);
/// @brief Set the bounds limit, which is provided by the user when creating /// @brief Set the computed content bounds, or std::nullopt if the contents
/// a SaveLayer. This is a hint that allows the user to communicate /// are unbounded.
/// that it's OK to not render content outside of the bounds. void SetBoundsLimit(std::optional<Rect> content_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<Rect> bounds_limit,
ContentBoundsPromise bounds_promise = ContentBoundsPromise::kUnknown);
/// @brief Get the bounds limit, which is provided by the user when creating /// @brief Get the bounds limit.
/// a SaveLayer.
std::optional<Rect> GetBoundsLimit() const; std::optional<Rect> GetBoundsLimit() const;
/// @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;
size_t GetSubpassesDepth() const; size_t GetSubpassesDepth() const;
/// @brief Add an entity to the current entity pass. /// @brief Add an entity to the current entity pass.
@ -181,30 +161,6 @@ class EntityPass {
required_mip_count_ = mip_count; 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<Rect> GetSubpassCoverage(
const EntityPass& subpass,
std::optional<Rect> coverage_limit) const;
std::optional<Rect> GetElementsCoverage(
std::optional<Rect> coverage_limit) const;
private: private:
struct EntityResult { struct EntityResult {
enum Status { enum Status {
@ -333,7 +289,6 @@ class EntityPass {
BlendMode blend_mode_ = BlendMode::kSourceOver; BlendMode blend_mode_ = BlendMode::kSourceOver;
bool flood_clip_ = false; bool flood_clip_ = false;
std::optional<Rect> bounds_limit_; std::optional<Rect> bounds_limit_;
ContentBoundsPromise bounds_promise_ = ContentBoundsPromise::kUnknown;
int32_t required_mip_count_ = 1; int32_t required_mip_count_ = 1;
/// These values indicate whether something has been added to the EntityPass /// These values indicate whether something has been added to the EntityPass

View File

@ -119,73 +119,10 @@ auto CreatePassWithRectPath(
PathBuilder{}.AddRect(rect).TakePath(), Color::Red())); PathBuilder{}.AddRect(rect).TakePath(), Color::Red()));
subpass->AddEntity(std::move(entity)); subpass->AddEntity(std::move(entity));
subpass->SetDelegate(std::make_unique<TestPassDelegate>(collapse)); subpass->SetDelegate(std::make_unique<TestPassDelegate>(collapse));
subpass->SetBoundsLimit(bounds_hint, bounds_promise); subpass->SetBoundsLimit(bounds_hint);
return subpass; 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) { TEST_P(EntityTest, EntityPassCanMergeSubpassIntoParent) {
// Both a red and a blue box should appear if the pass merging has worked // Both a red and a blue box should appear if the pass merging has worked
// correctly. // correctly.
@ -208,36 +145,6 @@ TEST_P(EntityTest, EntityPassCanMergeSubpassIntoParent) {
ASSERT_TRUE(OpenPlaygroundHere(pass)); 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) { TEST_P(EntityTest, FilterCoverageRespectsCropRect) {
auto image = CreateTextureForFixture("boston.jpg"); auto image = CreateTextureForFixture("boston.jpg");
auto filter = ColorFilterContents::MakeBlend(BlendMode::kSoftLight, auto filter = ColorFilterContents::MakeBlend(BlendMode::kSoftLight,

View File

@ -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<Rect> ComputeSaveLayerCoverage(
const Rect& content_coverage,
const Matrix& effect_transform,
const Rect& coverage_limit,
const std::shared_ptr<FilterContents>& 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<Rect> 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

View File

@ -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 <memory>
#include <optional>
#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<Rect> ComputeSaveLayerCoverage(
const Rect& content_coverage,
const Matrix& effect_transform,
const Rect& coverage_limit,
const std::shared_ptr<FilterContents>& image_filter,
bool flood_output_coverage = false,
bool flood_input_coverage = false);
} // namespace impeller
#endif // FLUTTER_IMPELLER_ENTITY_SAVE_LAYER_UTILS_H_

View File

@ -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)