[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.

![image](https://github.com/user-attachments/assets/557d12ed-b169-4806-a2d1-88db9f24d8a1)

## After

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

![image](https://github.com/user-attachments/assets/ad547131-af0a-4247-91a2-deeb3ffda8e9)
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);