Skip to content

Commit

Permalink
[Impeller] round up subpass coverage when it is close to (and smaller…
Browse files Browse the repository at this point in the history
…) than root pass size. (flutter#49925)

Work towards flutter/flutter#136510

generally speaking, we try to size subpasses as small as we possibly can as an optimization for memory usage and fragment load. Though with specific blend modes and/or filters we are required us to flood to a larger size. However in some cases, this optimization is counter productive - as we can only recycle textures that are exactly the same size.

In scenario where a large subpass slightly changes sizes every frame (such as for the android zoom page transition), the tight sizing is maximally inefficient as we discard and recreate large textures each frame.

This PR adds a heuristic which (at least locally) improves this by rounding up subpass size that is within 10% of the root pass size.
  • Loading branch information
jonahwilliams authored Jan 25, 2024
1 parent 4424ea2 commit f3bf0bd
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 4 deletions.
48 changes: 48 additions & 0 deletions impeller/aiks/aiks_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3928,6 +3928,54 @@ TEST_P(AiksTest, GaussianBlurMipMapImageFilter) {
#endif
}

TEST_P(AiksTest, SaveLayersCloseToRootPassSizeAreScaledUp) {
Canvas canvas;
// Create a subpass with no bounds hint and an entity coverage of (95, 95).
canvas.SaveLayer({
.color = Color::Black().WithAlpha(0.5),
});
canvas.DrawRect(Rect::MakeLTRB(0, 0, 10, 10), {.color = Color::Red()});
canvas.DrawRect(Rect::MakeLTRB(0, 0, 95, 95), {.color = Color::Blue()});
canvas.Restore();

Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<RenderTargetCache> cache =
std::make_shared<RenderTargetCache>(GetContext()->GetResourceAllocator());
AiksContext aiks_context(GetContext(), nullptr, cache);
picture.ToImage(aiks_context, {100, 100});

for (auto it = cache->GetTextureDataBegin(); it != cache->GetTextureDataEnd();
++it) {
EXPECT_EQ(it->texture->GetTextureDescriptor().size, ISize(100, 100));
}
}

TEST_P(AiksTest, SaveLayersCloseToRootPassSizeAreNotScaledUpPastBoundsHint) {
Canvas canvas;
canvas.SaveLayer(
{
.color = Color::Black().WithAlpha(0.5),
},
Rect::MakeSize(ISize(95, 95)));
canvas.DrawRect(Rect::MakeLTRB(0, 0, 100, 100), {.color = Color::Red()});
canvas.DrawRect(Rect::MakeLTRB(50, 50, 150, 150), {.color = Color::Blue()});
canvas.Restore();

Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<RenderTargetCache> cache =
std::make_shared<RenderTargetCache>(GetContext()->GetResourceAllocator());
AiksContext aiks_context(GetContext(), nullptr, cache);
picture.ToImage(aiks_context, {100, 100});

// We expect a single 100x100 texture and the rest should be 95x95.
EXPECT_EQ(cache->GetTextureDataBegin()->texture->GetTextureDescriptor().size,
ISize(100, 100));
for (auto it = ++cache->GetTextureDataBegin();
it != cache->GetTextureDataEnd(); ++it) {
EXPECT_EQ(it->texture->GetTextureDescriptor().size, ISize(95, 95));
}
}

TEST_P(AiksTest, ImageColorSourceEffectTransform) {
// Compare with https://fiddle.skia.org/c/6cdc5aefb291fda3833b806ca347a885

Expand Down
55 changes: 51 additions & 4 deletions impeller/entity/entity_pass.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
#include "impeller/entity/entity.h"
#include "impeller/entity/inline_pass_context.h"
#include "impeller/geometry/color.h"
#include "impeller/geometry/matrix.h"
#include "impeller/geometry/rect.h"
#include "impeller/geometry/size.h"
#include "impeller/renderer/command_buffer.h"

#ifdef IMPELLER_DEBUG
Expand Down Expand Up @@ -67,6 +69,13 @@ std::optional<Rect> EntityPass::GetBoundsLimit() const {
return bounds_limit_;
}

std::optional<Rect> EntityPass::GetTransformedBoundsLimit() const {
if (bounds_limit_.has_value()) {
return bounds_limit_->TransformBounds(transform_);
}
return std::nullopt;
}

void EntityPass::AddEntity(Entity entity) {
if (entity.GetBlendMode() == BlendMode::kSourceOver &&
entity.GetContents()->IsOpaque()) {
Expand Down Expand Up @@ -190,12 +199,11 @@ std::optional<Rect> EntityPass::GetSubpassCoverage(
return std::nullopt;
}

if (!subpass.bounds_limit_.has_value()) {
auto user_bounds_coverage = subpass.GetTransformedBoundsLimit();
if (!user_bounds_coverage.has_value()) {
return entities_coverage;
}
auto user_bounds_coverage =
subpass.bounds_limit_->TransformBounds(subpass.transform_);
return entities_coverage->Intersection(user_bounds_coverage);
return entities_coverage->Intersection(user_bounds_coverage.value());
}

EntityPass* EntityPass::GetSuperpass() const {
Expand Down Expand Up @@ -485,6 +493,43 @@ bool EntityPass::Render(ContentContext& renderer,
clip_coverage_stack); // clip_coverage_stack
}

// When a subpass size is close to, but still smaller than the root pass
// size and smaller than the bounds hint, we may scale it up to the root
// pass size. This will improve performance by improving the efficiency of
// the render target cache, as only textures with exactly the same sizes +
// descriptors can be recycled.
static ISize MaybeRoundUpTextureSize(ISize subpass_size,
ISize root_pass_size,
std::optional<Rect> bounds_limit) {
// If the subpass is already bigger than the root pass size,
// return the existing subpass size.
if (subpass_size.width > root_pass_size.width ||
subpass_size.height > root_pass_size.height) {
return subpass_size;
}

// If there is a bounds limit and it is tigher than the root pass size,
// return the existing subpass size. This case could be removed if we
// conditionally inserted clips/scissor instead.
if (bounds_limit.has_value()) {
auto bounds_size = bounds_limit->GetSize();
if (bounds_size.width < root_pass_size.width ||
bounds_size.height < root_pass_size.height) {
return subpass_size;
}
}

// If the subpass size is within 10% of the root pass size, round up
// to the root pass size.
if (root_pass_size.width - subpass_size.width <=
(0.1 * root_pass_size.width) &&
root_pass_size.height - subpass_size.height <=
(0.1 * root_pass_size.height)) {
return root_pass_size;
}
return subpass_size;
}

EntityPass::EntityResult EntityPass::GetEntityForElement(
const EntityPass::Element& element,
ContentContext& renderer,
Expand Down Expand Up @@ -618,6 +663,8 @@ EntityPass::EntityResult EntityPass::GetEntityForElement(
return EntityPass::EntityResult::Skip();
}

subpass_size = MaybeRoundUpTextureSize(
subpass_size, root_pass_size, subpass->GetTransformedBoundsLimit());
auto subpass_target = CreateRenderTarget(
renderer, // renderer
subpass_size, // size
Expand Down
3 changes: 3 additions & 0 deletions impeller/entity/entity_pass.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ class EntityPass {
required_mip_count_ = mip_count;
}

/// @brief Return the local bounds transformed intro screen coordinate space.
std::optional<Rect> GetTransformedBoundsLimit() const;

//----------------------------------------------------------------------------
/// @brief Computes the coverage of a given subpass. This is used to
/// determine the texture size of a given subpass before it's rendered
Expand Down

0 comments on commit f3bf0bd

Please sign in to comment.