[Impeller] eagerly flip backdrop back to onscreen. (#55983)
Fixes https://github.com/flutter/flutter/issues/157112
We currently track the number of backdrop filters in a frame. When the end of the frame is reached, we do a blit from the offscreen texture to the onscreen texture. Howeever, this extra blit is unecessary once we know we've rendered the last backdrop - and we can instead start the last render pass with the onscreen. This cuts out a fairly expensive blit.
this can only be done on iOS physical devices, vulkan, and some GLES - as emulated blends also use the backdrop.
## Before
Shader time is lower but timeline claims about 2.8ms and about ~800ms of blit pass.

## After
Timeline is ~2.1 ms with no blit pass. Tracks with numbers above.

diff --git a/impeller/display_list/canvas.cc b/impeller/display_list/canvas.cc
index 77f6984..abfead7 100644
--- a/impeller/display_list/canvas.cc
+++ b/impeller/display_list/canvas.cc
@@ -87,116 +87,6 @@
entity.SetBlendMode(BlendMode::kSource);
}
-/// End the current render pass, saving the result as a texture, and then
-/// restart it with the backdrop cleared to the previous contents.
-///
-/// This method is used to set up the input for emulated advanced blends and
-/// backdrop filters.
-///
-/// Returns the previous render pass stored as a texture, or nullptr if there
-/// was a validation failure.
-///
-/// [should_remove_texture] defaults to false. If true, the render target
-/// texture is removed from the entity pass target. This allows the texture to
-/// be cached by the canvas dispatcher for usage in the backdrop filter reuse
-/// mechanism.
-static std::shared_ptr<Texture> FlipBackdrop(
- std::vector<LazyRenderingConfig>& render_passes,
- Point global_pass_position,
- EntityPassClipStack& clip_coverage_stack,
- ContentContext& renderer,
- bool should_remove_texture = false) {
- LazyRenderingConfig rendering_config = std::move(render_passes.back());
- render_passes.pop_back();
-
- // If the very first thing we render in this EntityPass is a subpass that
- // happens to have a backdrop filter or advanced blend, than that backdrop
- // filter/blend will sample from an uninitialized texture.
- //
- // By calling `pass_context.GetRenderPass` here, we force the texture to pass
- // through at least one RenderPass with the correct clear configuration before
- // any sampling occurs.
- //
- // In cases where there are no contents, we
- // could instead check the clear color and initialize a 1x2 CPU texture
- // instead of ending the pass.
- rendering_config.inline_pass_context->GetRenderPass();
- if (!rendering_config.inline_pass_context->EndPass()) {
- VALIDATION_LOG
- << "Failed to end the current render pass in order to read from "
- "the backdrop texture and apply an advanced blend or backdrop "
- "filter.";
- // Note: adding this render pass ensures there are no later crashes from
- // unbalanced save layers. Ideally, this method would return false and the
- // renderer could handle that by terminating dispatch.
- render_passes.push_back(LazyRenderingConfig(
- renderer, std::move(rendering_config.entity_pass_target),
- std::move(rendering_config.inline_pass_context)));
- return nullptr;
- }
-
- const std::shared_ptr<Texture>& input_texture =
- rendering_config.inline_pass_context->GetTexture();
-
- if (!input_texture) {
- VALIDATION_LOG << "Failed to fetch the color texture in order to "
- "apply an advanced blend or backdrop filter.";
-
- // Note: see above.
- render_passes.push_back(LazyRenderingConfig(
- renderer, std::move(rendering_config.entity_pass_target),
- std::move(rendering_config.inline_pass_context)));
- return nullptr;
- }
-
- render_passes.push_back(LazyRenderingConfig(
- renderer, std::move(rendering_config.entity_pass_target),
- std::move(rendering_config.inline_pass_context)));
- // If the current texture is being cached for a BDF we need to ensure we
- // don't recycle it during recording; remove it from the entity pass target.
- if (should_remove_texture) {
- render_passes.back().entity_pass_target->RemoveSecondary();
- }
- RenderPass& current_render_pass =
- *render_passes.back().inline_pass_context->GetRenderPass();
-
- // Eagerly restore the BDF contents.
-
- // 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 blit the non-MSAA resolve texture of the previous pass to MSAA
- // textures (let alone a transient one).
- Rect size_rect = Rect::MakeSize(input_texture->GetSize());
- auto msaa_backdrop_contents = TextureContents::MakeRect(size_rect);
- msaa_backdrop_contents->SetStencilEnabled(false);
- msaa_backdrop_contents->SetLabel("MSAA backdrop");
- msaa_backdrop_contents->SetSourceRect(size_rect);
- msaa_backdrop_contents->SetTexture(input_texture);
-
- Entity msaa_backdrop_entity;
- msaa_backdrop_entity.SetContents(std::move(msaa_backdrop_contents));
- msaa_backdrop_entity.SetBlendMode(BlendMode::kSource);
- msaa_backdrop_entity.SetClipDepth(std::numeric_limits<uint32_t>::max());
- if (!msaa_backdrop_entity.Render(renderer, current_render_pass)) {
- VALIDATION_LOG << "Failed to render MSAA backdrop entity.";
- return nullptr;
- }
-
- // 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(replay.clip_coverage, current_render_pass,
- global_pass_position);
- if (!replay.entity.Render(renderer, current_render_pass)) {
- VALIDATION_LOG << "Failed to render entity for clip restore.";
- }
- }
-
- return input_texture;
-}
-
/// @brief Create the subpass restore contents, appling any filters or opacity
/// from the provided paint object.
static std::shared_ptr<Contents> CreateContentsForSubpassTarget(
@@ -271,7 +161,7 @@
} // namespace
Canvas::Canvas(ContentContext& renderer,
- RenderTarget& render_target,
+ const RenderTarget& render_target,
bool requires_readback)
: renderer_(renderer),
render_target_(render_target),
@@ -283,7 +173,7 @@
}
Canvas::Canvas(ContentContext& renderer,
- RenderTarget& render_target,
+ const RenderTarget& render_target,
bool requires_readback,
Rect cull_rect)
: renderer_(renderer),
@@ -296,7 +186,7 @@
}
Canvas::Canvas(ContentContext& renderer,
- RenderTarget& render_target,
+ const RenderTarget& render_target,
bool requires_readback,
IRect cull_rect)
: renderer_(renderer),
@@ -1083,11 +973,15 @@
std::shared_ptr<Texture> input_texture;
- // If the backdrop ID is not the no-op id, and there is more than one usage
+ // If the backdrop ID is not nullopt and there is more than one usage
// of it in the current scene, cache the backdrop texture and remove it from
// the current entity pass flip.
bool will_cache_backdrop_texture = false;
BackdropData* backdrop_data = nullptr;
+ // If we've reached this point, there is at least one backdrop filter. But
+ // potentially more if there is a backdrop id. We may conditionally set this
+ // to a higher value in the if block below.
+ size_t backdrop_count = 1;
if (backdrop_id.has_value()) {
std::unordered_map<int64_t, BackdropData>::iterator backdrop_data_it =
backdrop_data_.find(backdrop_id.value());
@@ -1095,16 +989,24 @@
backdrop_data = &backdrop_data_it->second;
will_cache_backdrop_texture =
backdrop_data_it->second.backdrop_count > 1;
+ backdrop_count = backdrop_data_it->second.backdrop_count;
}
}
- if (!will_cache_backdrop_texture ||
- (will_cache_backdrop_texture && !backdrop_data->texture_slot)) {
- input_texture = FlipBackdrop(render_passes_, //
- GetGlobalPassPosition(), //
- clip_coverage_stack_, //
- renderer_, //
- will_cache_backdrop_texture //
+ if (!will_cache_backdrop_texture || !backdrop_data->texture_slot) {
+ backdrop_count_ -= backdrop_count;
+
+ // The onscreen texture can be flipped to if:
+ // 1. The device supports framebuffer fetch
+ // 2. There are no more backdrop filters
+ // 3. The current render pass is for the onscreen pass.
+ const bool should_use_onscreen =
+ renderer_.GetDeviceCapabilities().SupportsFramebufferFetch() &&
+ backdrop_count_ == 0 && render_passes_.size() == 1u;
+ input_texture = FlipBackdrop(
+ GetGlobalPassPosition(), //
+ /*should_remove_texture=*/will_cache_backdrop_texture, //
+ /*should_use_onscreen=*/should_use_onscreen //
);
if (!input_texture) {
// Validation failures are logged in FlipBackdrop.
@@ -1297,9 +1199,7 @@
// to the render target texture so far need to execute before it's bound
// for blending (otherwise the blend pass will end up executing before
// all the previous commands in the active pass).
- auto input_texture =
- FlipBackdrop(render_passes_, GetGlobalPassPosition(),
- clip_coverage_stack_, renderer_);
+ auto input_texture = FlipBackdrop(GetGlobalPassPosition());
if (!input_texture) {
return false;
}
@@ -1555,11 +1455,7 @@
// to the render target texture so far need to execute before it's bound
// for blending (otherwise the blend pass will end up executing before
// all the previous commands in the active pass).
- auto input_texture = FlipBackdrop(render_passes_, //
- GetGlobalPassPosition(), //
- clip_coverage_stack_, //
- renderer_ //
- );
+ auto input_texture = FlipBackdrop(GetGlobalPassPosition());
if (!input_texture) {
return;
}
@@ -1653,8 +1549,125 @@
}
void Canvas::SetBackdropData(
- std::unordered_map<int64_t, BackdropData> backdrop_data) {
+ std::unordered_map<int64_t, BackdropData> backdrop_data,
+ size_t backdrop_count) {
backdrop_data_ = std::move(backdrop_data);
+ backdrop_count_ = backdrop_count;
+}
+
+std::shared_ptr<Texture> Canvas::FlipBackdrop(Point global_pass_position,
+ bool should_remove_texture,
+ bool should_use_onscreen) {
+ LazyRenderingConfig rendering_config = std::move(render_passes_.back());
+ render_passes_.pop_back();
+
+ // If the very first thing we render in this EntityPass is a subpass that
+ // happens to have a backdrop filter or advanced blend, than that backdrop
+ // filter/blend will sample from an uninitialized texture.
+ //
+ // By calling `pass_context.GetRenderPass` here, we force the texture to pass
+ // through at least one RenderPass with the correct clear configuration before
+ // any sampling occurs.
+ //
+ // In cases where there are no contents, we
+ // could instead check the clear color and initialize a 1x2 CPU texture
+ // instead of ending the pass.
+ rendering_config.inline_pass_context->GetRenderPass();
+ if (!rendering_config.inline_pass_context->EndPass()) {
+ VALIDATION_LOG
+ << "Failed to end the current render pass in order to read from "
+ "the backdrop texture and apply an advanced blend or backdrop "
+ "filter.";
+ // Note: adding this render pass ensures there are no later crashes from
+ // unbalanced save layers. Ideally, this method would return false and the
+ // renderer could handle that by terminating dispatch.
+ render_passes_.push_back(LazyRenderingConfig(
+ renderer_, std::move(rendering_config.entity_pass_target),
+ std::move(rendering_config.inline_pass_context)));
+ return nullptr;
+ }
+
+ const std::shared_ptr<Texture>& input_texture =
+ rendering_config.inline_pass_context->GetTexture();
+
+ if (!input_texture) {
+ VALIDATION_LOG << "Failed to fetch the color texture in order to "
+ "apply an advanced blend or backdrop filter.";
+
+ // Note: see above.
+ render_passes_.push_back(LazyRenderingConfig(
+ renderer_, std::move(rendering_config.entity_pass_target),
+ std::move(rendering_config.inline_pass_context)));
+ return nullptr;
+ }
+
+ if (should_use_onscreen) {
+ ColorAttachment color0 =
+ render_target_.GetColorAttachments().find(0u)->second;
+ // When MSAA is being used, we end up overriding the entire backdrop by
+ // drawing the previous pass texture, and so we don't have to clear it and
+ // can use kDontCare.
+ color0.load_action = color0.resolve_texture != nullptr
+ ? LoadAction::kDontCare
+ : LoadAction::kLoad;
+ render_target_.SetColorAttachment(color0, 0);
+
+ auto entity_pass_target = std::make_unique<EntityPassTarget>(
+ render_target_, //
+ renderer_.GetDeviceCapabilities().SupportsReadFromResolve(), //
+ renderer_.GetDeviceCapabilities().SupportsImplicitResolvingMSAA() //
+ );
+ render_passes_.push_back(
+ LazyRenderingConfig(renderer_, std::move(entity_pass_target)));
+ requires_readback_ = false;
+ } else {
+ render_passes_.push_back(LazyRenderingConfig(
+ renderer_, std::move(rendering_config.entity_pass_target),
+ std::move(rendering_config.inline_pass_context)));
+ // If the current texture is being cached for a BDF we need to ensure we
+ // don't recycle it during recording; remove it from the entity pass target.
+ if (should_remove_texture) {
+ render_passes_.back().entity_pass_target->RemoveSecondary();
+ }
+ }
+ RenderPass& current_render_pass =
+ *render_passes_.back().inline_pass_context->GetRenderPass();
+
+ // Eagerly restore the BDF contents.
+
+ // 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 blit the non-MSAA resolve texture of the previous pass to MSAA
+ // textures (let alone a transient one).
+ Rect size_rect = Rect::MakeSize(input_texture->GetSize());
+ auto msaa_backdrop_contents = TextureContents::MakeRect(size_rect);
+ msaa_backdrop_contents->SetStencilEnabled(false);
+ msaa_backdrop_contents->SetLabel("MSAA backdrop");
+ msaa_backdrop_contents->SetSourceRect(size_rect);
+ msaa_backdrop_contents->SetTexture(input_texture);
+
+ Entity msaa_backdrop_entity;
+ msaa_backdrop_entity.SetContents(std::move(msaa_backdrop_contents));
+ msaa_backdrop_entity.SetBlendMode(BlendMode::kSource);
+ msaa_backdrop_entity.SetClipDepth(std::numeric_limits<uint32_t>::max());
+ if (!msaa_backdrop_entity.Render(renderer_, current_render_pass)) {
+ VALIDATION_LOG << "Failed to render MSAA backdrop entity.";
+ return nullptr;
+ }
+
+ // 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(replay.clip_coverage, current_render_pass,
+ global_pass_position);
+ if (!replay.entity.Render(renderer_, current_render_pass)) {
+ VALIDATION_LOG << "Failed to render entity for clip restore.";
+ }
+ }
+
+ return input_texture;
}
bool Canvas::BlitToOnscreen() {
diff --git a/impeller/display_list/canvas.h b/impeller/display_list/canvas.h
index 0d72771..0c139af 100644
--- a/impeller/display_list/canvas.h
+++ b/impeller/display_list/canvas.h
@@ -121,24 +121,25 @@
Entity::RenderingMode rendering_mode)>;
Canvas(ContentContext& renderer,
- RenderTarget& render_target,
+ const RenderTarget& render_target,
bool requires_readback);
explicit Canvas(ContentContext& renderer,
- RenderTarget& render_target,
+ const RenderTarget& render_target,
bool requires_readback,
Rect cull_rect);
explicit Canvas(ContentContext& renderer,
- RenderTarget& render_target,
+ const RenderTarget& render_target,
bool requires_readback,
IRect cull_rect);
~Canvas() = default;
/// @brief Update the backdrop data used to group together backdrop filters
- /// within the same layer.
- void SetBackdropData(std::unordered_map<int64_t, BackdropData> backdrop_data);
+ /// within the same layer
+ void SetBackdropData(std::unordered_map<int64_t, BackdropData> backdrop_data,
+ size_t backdrop_count);
/// @brief Return the culling bounds of the current render target, or nullopt
/// if there is no coverage.
@@ -238,18 +239,37 @@
Rect coverage;
};
+ // Visible for testing.
+ bool RequiresReadback() const { return requires_readback_; }
+
private:
ContentContext& renderer_;
- RenderTarget& render_target_;
- const bool requires_readback_;
+ RenderTarget render_target_;
+ bool requires_readback_;
EntityPassClipStack clip_coverage_stack_;
std::deque<CanvasStackEntry> transform_stack_;
std::optional<Rect> initial_cull_rect_;
std::vector<LazyRenderingConfig> render_passes_;
std::vector<SaveLayerState> save_layer_state_;
+
+ /// Backdrop layers identified by an optional backdrop id.
+ ///
+ /// This is not the same as the [backdrop_count_] below as not
+ /// all backdrop filters will have an identified backdrop id. The
+ /// backdrop_count_ is also mutated during rendering.
std::unordered_map<int64_t, BackdropData> backdrop_data_;
+ /// The remaining number of backdrop filters.
+ ///
+ /// This value is decremented while rendering. When it reaches 0, then
+ /// the FlipBackdrop can use the onscreen render target instead of
+ /// another offscreen.
+ ///
+ /// This optimization is disabled on devices that do not support framebuffer
+ /// fetch (iOS Simulator and certain OpenGLES devices).
+ size_t backdrop_count_ = 0u;
+
// All geometry objects created for regular draws can be stack allocated,
// but clip geometries must be cached for record/replay for backdrop filters
// and so must be kept alive longer.
@@ -271,6 +291,27 @@
void SetupRenderPass();
+ /// @brief Ends the current render pass, saving the result as a texture, and
+ /// thenrestart it with the backdrop cleared to the previous contents.
+ ///
+ /// The returned texture is used as the input for backdrop filters and
+ /// emulated advanced blends. Returns nullptr if there was a validation
+ /// failure.
+ ///
+ /// [should_remove_texture] defaults to false. If true, the render target
+ /// texture is removed from the entity pass target. This allows the texture to
+ /// be cached by the canvas dispatcher for usage in the backdrop filter reuse
+ /// mechanism.
+ ///
+ /// [should_use_onscreen] defaults to false. If true, the results are flipped
+ /// to the onscreen render target. This will set requires_readback_ to false.
+ /// This action is only safe to perform when there are no more backdrop
+ /// filters or advanced blends, or no more backdrop filters and the device
+ /// supports framebuffer fetch.
+ std::shared_ptr<Texture> FlipBackdrop(Point global_pass_position,
+ bool should_remove_texture = false,
+ bool should_use_onscreen = false);
+
bool BlitToOnscreen();
size_t GetClipHeight() const;
diff --git a/impeller/display_list/canvas_unittests.cc b/impeller/display_list/canvas_unittests.cc
index dae5e25..2a76265 100644
--- a/impeller/display_list/canvas_unittests.cc
+++ b/impeller/display_list/canvas_unittests.cc
@@ -2,25 +2,61 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <unordered_map>
+
+#include "display_list/dl_tile_mode.h"
+#include "display_list/effects/dl_image_filter.h"
+#include "display_list/geometry/dl_geometry_types.h"
#include "flutter/testing/testing.h"
+#include "gtest/gtest.h"
+#include "impeller/core/formats.h"
+#include "impeller/core/texture_descriptor.h"
#include "impeller/display_list/aiks_unittests.h"
#include "impeller/display_list/canvas.h"
#include "impeller/geometry/geometry_asserts.h"
+#include "impeller/renderer/render_target.h"
namespace impeller {
namespace testing {
std::unique_ptr<Canvas> CreateTestCanvas(
ContentContext& context,
- std::optional<Rect> cull_rect = std::nullopt) {
- RenderTarget render_target = context.GetRenderTargetCache()->CreateOffscreen(
- *context.GetContext(), {1, 1}, 1);
+ std::optional<Rect> cull_rect = std::nullopt,
+ bool requires_readback = false) {
+ TextureDescriptor onscreen_desc;
+ onscreen_desc.size = {100, 100};
+ onscreen_desc.format =
+ context.GetDeviceCapabilities().GetDefaultColorFormat();
+ onscreen_desc.usage = TextureUsage::kRenderTarget;
+ onscreen_desc.storage_mode = StorageMode::kDevicePrivate;
+ onscreen_desc.sample_count = SampleCount::kCount1;
+ std::shared_ptr<Texture> onscreen =
+ context.GetContext()->GetResourceAllocator()->CreateTexture(
+ onscreen_desc);
+
+ TextureDescriptor onscreen_msaa_desc = onscreen_desc;
+ onscreen_msaa_desc.sample_count = SampleCount::kCount4;
+ onscreen_msaa_desc.storage_mode = StorageMode::kDeviceTransient;
+ onscreen_msaa_desc.type = TextureType::kTexture2DMultisample;
+
+ std::shared_ptr<Texture> onscreen_msaa =
+ context.GetContext()->GetResourceAllocator()->CreateTexture(
+ onscreen_msaa_desc);
+
+ ColorAttachment color0;
+ color0.resolve_texture = onscreen;
+ color0.texture = onscreen_msaa;
+ color0.store_action = StoreAction::kMultisampleResolve;
+ color0.load_action = LoadAction::kClear;
+
+ RenderTarget render_target;
+ render_target.SetColorAttachment(color0, 0);
if (cull_rect.has_value()) {
- return std::make_unique<Canvas>(context, render_target, false,
+ return std::make_unique<Canvas>(context, render_target, requires_readback,
cull_rect.value());
}
- return std::make_unique<Canvas>(context, render_target, false);
+ return std::make_unique<Canvas>(context, render_target, requires_readback);
}
TEST_P(AiksTest, TransformMultipliesCorrectly) {
@@ -93,5 +129,149 @@
Matrix::MakeTranslation({100.0, 100.0, 0.0}));
}
+TEST_P(AiksTest, BackdropCountDownNormal) {
+ ContentContext context(GetContext(), nullptr);
+ if (!context.GetDeviceCapabilities().SupportsFramebufferFetch()) {
+ GTEST_SKIP() << "Test requires device with framebuffer fetch";
+ }
+ auto canvas = CreateTestCanvas(context, Rect::MakeLTRB(0, 0, 100, 100),
+ /*requires_readback=*/true);
+ // 3 backdrop filters
+ canvas->SetBackdropData({}, 3);
+
+ auto blur =
+ flutter::DlBlurImageFilter::Make(4, 4, flutter::DlTileMode::kClamp);
+ flutter::DlRect rect = flutter::DlRect::MakeLTRB(0, 0, 50, 50);
+
+ EXPECT_TRUE(canvas->RequiresReadback());
+ canvas->DrawRect(rect, {.color = Color::Azure()});
+ canvas->SaveLayer({}, rect, blur.get(),
+ ContentBoundsPromise::kContainsContents,
+ /*total_content_depth=*/1);
+ canvas->Restore();
+ EXPECT_TRUE(canvas->RequiresReadback());
+
+ canvas->SaveLayer({}, rect, blur.get(),
+ ContentBoundsPromise::kContainsContents,
+ /*total_content_depth=*/1);
+ canvas->Restore();
+ EXPECT_TRUE(canvas->RequiresReadback());
+
+ canvas->SaveLayer({}, rect, blur.get(),
+ ContentBoundsPromise::kContainsContents,
+ /*total_content_depth=*/1);
+ canvas->Restore();
+ EXPECT_FALSE(canvas->RequiresReadback());
+}
+
+TEST_P(AiksTest, BackdropCountDownBackdropId) {
+ ContentContext context(GetContext(), nullptr);
+ if (!context.GetDeviceCapabilities().SupportsFramebufferFetch()) {
+ GTEST_SKIP() << "Test requires device with framebuffer fetch";
+ }
+ auto canvas = CreateTestCanvas(context, Rect::MakeLTRB(0, 0, 100, 100),
+ /*requires_readback=*/true);
+ // 3 backdrop filters all with same id.
+ std::unordered_map<int64_t, BackdropData> data;
+ data[1] = BackdropData{.backdrop_count = 3};
+ canvas->SetBackdropData(data, 3);
+
+ auto blur =
+ flutter::DlBlurImageFilter::Make(4, 4, flutter::DlTileMode::kClamp);
+
+ EXPECT_TRUE(canvas->RequiresReadback());
+ canvas->DrawRect(flutter::DlRect::MakeLTRB(0, 0, 50, 50),
+ {.color = Color::Azure()});
+ canvas->SaveLayer({}, std::nullopt, blur.get(),
+ ContentBoundsPromise::kContainsContents,
+ /*total_content_depth=*/1, /*can_distribute_opacity=*/false,
+ /*backdrop_id=*/1);
+ canvas->Restore();
+ EXPECT_FALSE(canvas->RequiresReadback());
+
+ canvas->SaveLayer({}, std::nullopt, blur.get(),
+ ContentBoundsPromise::kContainsContents,
+ /*total_content_depth=*/1, /*can_distribute_opacity=*/false,
+ /*backdrop_id=*/1);
+ canvas->Restore();
+ EXPECT_FALSE(canvas->RequiresReadback());
+
+ canvas->SaveLayer({}, std::nullopt, blur.get(),
+ ContentBoundsPromise::kContainsContents,
+ /*total_content_depth=*/1, /*can_distribute_opacity=*/false,
+ /*backdrop_id=*/1);
+ canvas->Restore();
+ EXPECT_FALSE(canvas->RequiresReadback());
+}
+
+TEST_P(AiksTest, BackdropCountDownBackdropIdMixed) {
+ ContentContext context(GetContext(), nullptr);
+ if (!context.GetDeviceCapabilities().SupportsFramebufferFetch()) {
+ GTEST_SKIP() << "Test requires device with framebuffer fetch";
+ }
+ auto canvas = CreateTestCanvas(context, Rect::MakeLTRB(0, 0, 100, 100),
+ /*requires_readback=*/true);
+ // 3 backdrop filters, 2 with same id.
+ std::unordered_map<int64_t, BackdropData> data;
+ data[1] = BackdropData{.backdrop_count = 2};
+ canvas->SetBackdropData(data, 3);
+
+ auto blur =
+ flutter::DlBlurImageFilter::Make(4, 4, flutter::DlTileMode::kClamp);
+
+ EXPECT_TRUE(canvas->RequiresReadback());
+ canvas->DrawRect(flutter::DlRect::MakeLTRB(0, 0, 50, 50),
+ {.color = Color::Azure()});
+ canvas->SaveLayer({}, std::nullopt, blur.get(),
+ ContentBoundsPromise::kContainsContents, 1, false);
+ canvas->Restore();
+ EXPECT_TRUE(canvas->RequiresReadback());
+
+ canvas->SaveLayer({}, std::nullopt, blur.get(),
+ ContentBoundsPromise::kContainsContents, 1, false, 1);
+ canvas->Restore();
+ EXPECT_FALSE(canvas->RequiresReadback());
+
+ canvas->SaveLayer({}, std::nullopt, blur.get(),
+ ContentBoundsPromise::kContainsContents, 1, false, 1);
+ canvas->Restore();
+ EXPECT_FALSE(canvas->RequiresReadback());
+}
+
+// We only know the total number of backdrop filters, not the number of backdrop
+// filters in the root pass. If we reach a count of 0 while in a nested
+// saveLayer, we should not restore to the onscreen.
+TEST_P(AiksTest, BackdropCountDownWithNestedSaveLayers) {
+ ContentContext context(GetContext(), nullptr);
+ if (!context.GetDeviceCapabilities().SupportsFramebufferFetch()) {
+ GTEST_SKIP() << "Test requires device with framebuffer fetch";
+ }
+ auto canvas = CreateTestCanvas(context, Rect::MakeLTRB(0, 0, 100, 100),
+ /*requires_readback=*/true);
+
+ canvas->SetBackdropData({}, 2);
+
+ auto blur =
+ flutter::DlBlurImageFilter::Make(4, 4, flutter::DlTileMode::kClamp);
+
+ EXPECT_TRUE(canvas->RequiresReadback());
+ canvas->DrawRect(flutter::DlRect::MakeLTRB(0, 0, 50, 50),
+ {.color = Color::Azure()});
+ canvas->SaveLayer({}, std::nullopt, blur.get(),
+ ContentBoundsPromise::kContainsContents,
+ /*total_content_depth=*/3);
+
+ // This filter is nested in the first saveLayer. We cannot restore to onscreen
+ // here.
+ canvas->SaveLayer({}, std::nullopt, blur.get(),
+ ContentBoundsPromise::kContainsContents,
+ /*total_content_depth=*/1);
+ canvas->Restore();
+ EXPECT_TRUE(canvas->RequiresReadback());
+
+ canvas->Restore();
+ EXPECT_TRUE(canvas->RequiresReadback());
+}
+
} // namespace testing
} // namespace impeller
diff --git a/impeller/display_list/dl_dispatcher.cc b/impeller/display_list/dl_dispatcher.cc
index 804f344..989be7a 100644
--- a/impeller/display_list/dl_dispatcher.cc
+++ b/impeller/display_list/dl_dispatcher.cc
@@ -969,8 +969,9 @@
}
void CanvasDlDispatcher::SetBackdropData(
- std::unordered_map<int64_t, BackdropData> backdrop) {
- GetCanvas().SetBackdropData(std::move(backdrop));
+ std::unordered_map<int64_t, BackdropData> backdrop,
+ size_t backdrop_count) {
+ GetCanvas().SetBackdropData(std::move(backdrop), backdrop_count);
}
//// Text Frame Dispatcher
@@ -997,6 +998,7 @@
std::optional<int64_t> backdrop_id) {
save();
+ backdrop_count_ += (backdrop == nullptr ? 0 : 1);
if (backdrop != nullptr && backdrop_id.has_value()) {
std::shared_ptr<flutter::DlImageFilter> shared_backdrop =
backdrop->shared();
@@ -1210,11 +1212,11 @@
}
}
-std::unordered_map<int64_t, BackdropData>
+std::pair<std::unordered_map<int64_t, BackdropData>, size_t>
TextFrameDispatcher::TakeBackdropData() {
std::unordered_map<int64_t, BackdropData> temp;
std::swap(temp, backdrop_data_);
- return temp;
+ return std::make_pair(temp, backdrop_count_);
}
std::shared_ptr<Texture> DisplayListToTexture(
@@ -1264,7 +1266,8 @@
display_list->max_root_blend_mode(), //
impeller::IRect::MakeSize(size) //
);
- impeller_dispatcher.SetBackdropData(collector.TakeBackdropData());
+ const auto& [data, count] = collector.TakeBackdropData();
+ impeller_dispatcher.SetBackdropData(data, count);
display_list->Dispatch(impeller_dispatcher, sk_cull_rect);
impeller_dispatcher.FinishRecording();
@@ -1293,7 +1296,8 @@
display_list->max_root_blend_mode(), //
IRect::RoundOut(ip_cull_rect) //
);
- impeller_dispatcher.SetBackdropData(collector.TakeBackdropData());
+ const auto& [data, count] = collector.TakeBackdropData();
+ impeller_dispatcher.SetBackdropData(data, count);
display_list->Dispatch(impeller_dispatcher, cull_rect);
impeller_dispatcher.FinishRecording();
if (reset_host_buffer) {
diff --git a/impeller/display_list/dl_dispatcher.h b/impeller/display_list/dl_dispatcher.h
index b8ee05f..4a61290 100644
--- a/impeller/display_list/dl_dispatcher.h
+++ b/impeller/display_list/dl_dispatcher.h
@@ -260,7 +260,8 @@
~CanvasDlDispatcher() = default;
- void SetBackdropData(std::unordered_map<int64_t, BackdropData> backdrop);
+ void SetBackdropData(std::unordered_map<int64_t, BackdropData> backdrop,
+ size_t backdrop_count);
// |flutter::DlOpReceiver|
void save() override {
@@ -364,7 +365,7 @@
// |flutter::DlOpReceiver|
void setImageFilter(const flutter::DlImageFilter* filter) override;
- std::unordered_map<int64_t, BackdropData> TakeBackdropData();
+ std::pair<std::unordered_map<int64_t, BackdropData>, size_t> TakeBackdropData();
private:
const Rect GetCurrentLocalCullingBounds() const;
@@ -376,6 +377,7 @@
// note: cull rects are always in the global coordinate space.
std::vector<Rect> cull_rect_state_;
bool has_image_filter_ = false;
+ size_t backdrop_count_ = 0;
Paint paint_;
};
diff --git a/impeller/entity/contents/content_context.cc b/impeller/entity/contents/content_context.cc
index 70d1df7..e08670c 100644
--- a/impeller/entity/contents/content_context.cc
+++ b/impeller/entity/contents/content_context.cc
@@ -257,7 +257,7 @@
{
TextureDescriptor desc;
- desc.storage_mode = StorageMode::kHostVisible;
+ desc.storage_mode = StorageMode::kDevicePrivate;
desc.format = PixelFormat::kR8G8B8A8UNormInt;
desc.size = ISize{1, 1};
empty_texture_ = GetContext()->GetResourceAllocator()->CreateTexture(desc);