[Impeller]: new blur - adds mips for backdrop filters (#49607)

If the new blur flag is on and a blur is used as a backdrop filter, the
rendered texture will now have mipmaps which will make the downscaling
step of the blur have more signal (and thus be less shimmery).

issue: https://github.com/flutter/flutter/issues/140193
fixes: https://github.com/flutter/flutter/issues/140949

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index fb771b8..df2bfd9 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -5553,6 +5553,8 @@
 ORIGIN: ../../../flutter/impeller/renderer/stroke.comp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/surface.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/surface.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/impeller/renderer/texture_mipmap.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/impeller/renderer/texture_mipmap.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/threadgroup_sizing_test.comp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/vertex_buffer_builder.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/vertex_buffer_builder.h + ../../../flutter/LICENSE
@@ -8381,6 +8383,8 @@
 FILE: ../../../flutter/impeller/renderer/stroke.comp
 FILE: ../../../flutter/impeller/renderer/surface.cc
 FILE: ../../../flutter/impeller/renderer/surface.h
+FILE: ../../../flutter/impeller/renderer/texture_mipmap.cc
+FILE: ../../../flutter/impeller/renderer/texture_mipmap.h
 FILE: ../../../flutter/impeller/renderer/threadgroup_sizing_test.comp
 FILE: ../../../flutter/impeller/renderer/vertex_buffer_builder.cc
 FILE: ../../../flutter/impeller/renderer/vertex_buffer_builder.h
diff --git a/impeller/aiks/aiks_context.cc b/impeller/aiks/aiks_context.cc
index d001c18..3be53f9 100644
--- a/impeller/aiks/aiks_context.cc
+++ b/impeller/aiks/aiks_context.cc
@@ -12,14 +12,18 @@
 
 AiksContext::AiksContext(
     std::shared_ptr<Context> context,
-    std::shared_ptr<TypographerContext> typographer_context)
+    std::shared_ptr<TypographerContext> typographer_context,
+    std::optional<std::shared_ptr<RenderTargetAllocator>>
+        render_target_allocator)
     : context_(std::move(context)) {
   if (!context_ || !context_->IsValid()) {
     return;
   }
 
   content_context_ = std::make_unique<ContentContext>(
-      context_, std::move(typographer_context));
+      context_, std::move(typographer_context),
+      render_target_allocator.has_value() ? render_target_allocator.value()
+                                          : nullptr);
   if (!content_context_->IsValid()) {
     return;
   }
diff --git a/impeller/aiks/aiks_context.h b/impeller/aiks/aiks_context.h
index ad8d642..5b35b63 100644
--- a/impeller/aiks/aiks_context.h
+++ b/impeller/aiks/aiks_context.h
@@ -28,8 +28,12 @@
   ///                             `nullptr` is supplied, then attempting to draw
   ///                             text with Aiks will result in validation
   ///                             errors.
+  /// @param render_target_allocator Injects a render target allocator or
+  ///                                allocates its own if none is supplied.
   AiksContext(std::shared_ptr<Context> context,
-              std::shared_ptr<TypographerContext> typographer_context);
+              std::shared_ptr<TypographerContext> typographer_context,
+              std::optional<std::shared_ptr<RenderTargetAllocator>>
+                  render_target_allocator = std::nullopt);
 
   ~AiksContext();
 
diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc
index e1f75aa..c2b0739 100644
--- a/impeller/aiks/aiks_unittests.cc
+++ b/impeller/aiks/aiks_unittests.cc
@@ -4,6 +4,7 @@
 
 #include "flutter/impeller/aiks/aiks_unittests.h"
 
+#include <algorithm>
 #include <array>
 #include <cmath>
 #include <cstdlib>
@@ -26,6 +27,7 @@
 #include "impeller/entity/contents/radial_gradient_contents.h"
 #include "impeller/entity/contents/solid_color_contents.h"
 #include "impeller/entity/contents/sweep_gradient_contents.h"
+#include "impeller/entity/render_target_cache.h"
 #include "impeller/geometry/color.h"
 #include "impeller/geometry/constants.h"
 #include "impeller/geometry/geometry_asserts.h"
@@ -3716,5 +3718,81 @@
   // will be filled with NaNs and may produce a magenta texture on macOS or iOS.
   ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
 }
+
+TEST_P(AiksTest, GuassianBlurUpdatesMipmapContents) {
+  // This makes sure if mip maps are recycled across invocations of blurs the
+  // contents get updated each frame correctly. If they aren't updated the color
+  // inside the blur and outside the blur will be different.
+  //
+  // If there is some change to render target caching this could display a false
+  // positive in the future.  Also, if the LOD that is rendered is 1 it could
+  // present a false positive.
+  int32_t count = 0;
+  auto callback = [&](AiksContext& renderer) -> std::optional<Picture> {
+    Canvas canvas;
+    if (count++ == 0) {
+      canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()});
+    } else {
+      canvas.DrawCircle({100, 100}, 50, {.color = Color::Chartreuse()});
+    }
+    canvas.ClipRRect(Rect::MakeLTRB(75, 50, 375, 275), {20, 20});
+    canvas.SaveLayer({.blend_mode = BlendMode::kSource}, std::nullopt,
+                     ImageFilter::MakeBlur(Sigma(30.0), Sigma(30.0),
+                                           FilterContents::BlurStyle::kNormal,
+                                           Entity::TileMode::kClamp));
+    canvas.Restore();
+    return canvas.EndRecordingAsPicture();
+  };
+
+  ASSERT_TRUE(OpenPlaygroundHere(callback));
+}
+
+TEST_P(AiksTest, GaussianBlurSetsMipCountOnPass) {
+  Canvas canvas;
+  canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()});
+  canvas.SaveLayer({}, std::nullopt,
+                   ImageFilter::MakeBlur(Sigma(3), Sigma(3),
+                                         FilterContents::BlurStyle::kNormal,
+                                         Entity::TileMode::kClamp));
+  canvas.Restore();
+
+  Picture picture = canvas.EndRecordingAsPicture();
+
+  int32_t max_mip_count = 0;
+  picture.pass->IterateAllElements([&](EntityPass::Element& element) -> bool {
+    if (auto subpass = std::get_if<std::unique_ptr<EntityPass>>(&element)) {
+      max_mip_count =
+          std::max(max_mip_count, subpass->get()->GetRequiredMipCount());
+    }
+    return true;
+  });
+
+  EXPECT_EQ(1, max_mip_count);
+}
+
+TEST_P(AiksTest, GaussianBlurAllocatesCorrectMipCountRenderTarget) {
+  Canvas canvas;
+  canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()});
+  canvas.SaveLayer({}, std::nullopt,
+                   ImageFilter::MakeBlur(Sigma(3), Sigma(3),
+                                         FilterContents::BlurStyle::kNormal,
+                                         Entity::TileMode::kClamp));
+  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});
+
+  size_t max_mip_count = 0;
+  for (auto it = cache->GetTextureDataBegin(); it != cache->GetTextureDataEnd();
+       ++it) {
+    max_mip_count =
+        std::max(it->texture->GetTextureDescriptor().mip_count, max_mip_count);
+  }
+  EXPECT_EQ(max_mip_count, 1lu);
+}
+
 }  // namespace testing
 }  // namespace impeller
diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc
index c5b9ae9..f66da9a 100644
--- a/impeller/aiks/canvas.cc
+++ b/impeller/aiks/canvas.cc
@@ -133,6 +133,37 @@
   Save(false);
 }
 
+namespace {
+class MipCountVisitor : public ImageFilterVisitor {
+ public:
+  virtual void Visit(const BlurImageFilter& filter) {
+    required_mip_count_ = FilterContents::kBlurFilterRequiredMipCount;
+  }
+  virtual void Visit(const LocalMatrixImageFilter& filter) {
+    required_mip_count_ = 1;
+  }
+  virtual void Visit(const DilateImageFilter& filter) {
+    required_mip_count_ = 1;
+  }
+  virtual void Visit(const ErodeImageFilter& filter) {
+    required_mip_count_ = 1;
+  }
+  virtual void Visit(const MatrixImageFilter& filter) {
+    required_mip_count_ = 1;
+  }
+  virtual void Visit(const ComposeImageFilter& filter) {
+    required_mip_count_ = 1;
+  }
+  virtual void Visit(const ColorImageFilter& filter) {
+    required_mip_count_ = 1;
+  }
+  int32_t GetRequiredMipCount() const { return required_mip_count_; }
+
+ private:
+  int32_t required_mip_count_ = -1;
+};
+}  // namespace
+
 void Canvas::Save(bool create_subpass,
                   BlendMode blend_mode,
                   const std::shared_ptr<ImageFilter>& backdrop_filter) {
@@ -156,6 +187,9 @@
             return filter;
           };
       subpass->SetBackdropFilter(backdrop_filter_proc);
+      MipCountVisitor mip_count_visitor;
+      backdrop_filter->Visit(mip_count_visitor);
+      subpass->SetRequiredMipCount(mip_count_visitor.GetRequiredMipCount());
     }
     subpass->SetBlendMode(blend_mode);
     current_pass_ = GetCurrentPass().AddSubpass(std::move(subpass));
diff --git a/impeller/aiks/picture.cc b/impeller/aiks/picture.cc
index ea07dfd..8fc31dc 100644
--- a/impeller/aiks/picture.cc
+++ b/impeller/aiks/picture.cc
@@ -64,6 +64,7 @@
         *impeller_context,        // context
         render_target_allocator,  // allocator
         size,                     // size
+        /*mip_count=*/1,
         "Picture Snapshot MSAA",  // label
         RenderTarget::
             kDefaultColorAttachmentConfigMSAA,  // color_attachment_config
@@ -71,9 +72,10 @@
     );
   } else {
     target = RenderTarget::CreateOffscreen(
-        *impeller_context,                            // context
-        render_target_allocator,                      // allocator
-        size,                                         // size
+        *impeller_context,        // context
+        render_target_allocator,  // allocator
+        size,                     // size
+        /*mip_count=*/1,
         "Picture Snapshot",                           // label
         RenderTarget::kDefaultColorAttachmentConfig,  // color_attachment_config
         std::nullopt  // stencil_attachment_config
diff --git a/impeller/core/texture.h b/impeller/core/texture.h
index b28dbb2..af14a9e 100644
--- a/impeller/core/texture.h
+++ b/impeller/core/texture.h
@@ -45,6 +45,9 @@
 
   virtual Scalar GetYCoordScale() const;
 
+  /// Returns true if mipmaps have never been generated.
+  /// The contents of the mipmap may be out of date if the root texture has been
+  /// modified and the mipmaps hasn't been regenerated.
   bool NeedsMipmapGeneration() const;
 
  protected:
diff --git a/impeller/entity/contents/checkerboard_contents_unittests.cc b/impeller/entity/contents/checkerboard_contents_unittests.cc
index 4632862..63a2cbd 100644
--- a/impeller/entity/contents/checkerboard_contents_unittests.cc
+++ b/impeller/entity/contents/checkerboard_contents_unittests.cc
@@ -35,7 +35,8 @@
   auto buffer = content_context->GetContext()->CreateCommandBuffer();
   auto render_target = RenderTarget::CreateOffscreenMSAA(
       *content_context->GetContext(),
-      *GetContentContext()->GetRenderTargetCache(), {100, 100});
+      *GetContentContext()->GetRenderTargetCache(), {100, 100},
+      /*mip_count=*/1);
   auto render_pass = buffer->CreateRenderPass(render_target);
   Entity entity;
 
diff --git a/impeller/entity/contents/content_context.cc b/impeller/entity/contents/content_context.cc
index 634ae0b..7680481 100644
--- a/impeller/entity/contents/content_context.cc
+++ b/impeller/entity/contents/content_context.cc
@@ -418,14 +418,14 @@
   RenderTarget subpass_target;
   if (context->GetCapabilities()->SupportsOffscreenMSAA() && msaa_enabled) {
     subpass_target = RenderTarget::CreateOffscreenMSAA(
-        *context, *GetRenderTargetCache(), texture_size,
+        *context, *GetRenderTargetCache(), texture_size, /*mip_count=*/1,
         SPrintF("%s Offscreen", label.c_str()),
         RenderTarget::kDefaultColorAttachmentConfigMSAA,
         std::nullopt  // stencil_attachment_config
     );
   } else {
     subpass_target = RenderTarget::CreateOffscreen(
-        *context, *GetRenderTargetCache(), texture_size,
+        *context, *GetRenderTargetCache(), texture_size, /*mip_count=*/1,
         SPrintF("%s Offscreen", label.c_str()),
         RenderTarget::kDefaultColorAttachmentConfig,  //
         std::nullopt  // stencil_attachment_config
diff --git a/impeller/entity/contents/filters/filter_contents.cc b/impeller/entity/contents/filters/filter_contents.cc
index 5f2c310..235a531 100644
--- a/impeller/entity/contents/filters/filter_contents.cc
+++ b/impeller/entity/contents/filters/filter_contents.cc
@@ -50,6 +50,12 @@
   return blur;
 }
 
+#ifdef IMPELLER_ENABLE_NEW_GAUSSIAN_FILTER
+const int32_t FilterContents::kBlurFilterRequiredMipCount = 4;
+#else
+const int32_t FilterContents::kBlurFilterRequiredMipCount = 1;
+#endif
+
 std::shared_ptr<FilterContents> FilterContents::MakeGaussianBlur(
     const FilterInput::Ref& input,
     Sigma sigma_x,
diff --git a/impeller/entity/contents/filters/filter_contents.h b/impeller/entity/contents/filters/filter_contents.h
index 3e210d8..076593f 100644
--- a/impeller/entity/contents/filters/filter_contents.h
+++ b/impeller/entity/contents/filters/filter_contents.h
@@ -20,6 +20,8 @@
 
 class FilterContents : public Contents {
  public:
+  static const int32_t kBlurFilterRequiredMipCount;
+
   enum class BlurStyle {
     /// Blurred inside and outside.
     kNormal,
diff --git a/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc b/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc
index 22d49a3..13610bd 100644
--- a/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc
+++ b/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc
@@ -11,6 +11,7 @@
 #include "impeller/entity/texture_fill.vert.h"
 #include "impeller/renderer/command.h"
 #include "impeller/renderer/render_pass.h"
+#include "impeller/renderer/texture_mipmap.h"
 #include "impeller/renderer/vertex_buffer_builder.h"
 
 namespace impeller {
@@ -186,7 +187,6 @@
                                      rect.GetSize());
   return result.Scale(1.0f / Vector2(reference.GetSize()));
 }
-
 }  // namespace
 
 GaussianBlurFilterContents::GaussianBlurFilterContents(
@@ -278,6 +278,12 @@
                                 entity.GetClipDepth());  // No blur to render.
   }
 
+  // In order to avoid shimmering in downsampling step, we should have mips.
+  if (input_snapshot->texture->GetMipCount() <= 1) {
+    FML_DLOG(ERROR) << "Applying gaussian blur without mipmap.";
+  }
+  FML_DCHECK(!input_snapshot->texture->NeedsMipmapGeneration());
+
   Scalar desired_scalar =
       std::min(CalculateScale(scaled_sigma.x), CalculateScale(scaled_sigma.y));
   // TODO(jonahwilliams): If desired_scalar is 1.0 and we fully acquired the
diff --git a/impeller/entity/contents/scene_contents.cc b/impeller/entity/contents/scene_contents.cc
index 54eb9f5..f5da628 100644
--- a/impeller/entity/contents/scene_contents.cc
+++ b/impeller/entity/contents/scene_contents.cc
@@ -50,7 +50,8 @@
         *renderer.GetContext(),             // context
         *renderer.GetRenderTargetCache(),   // allocator
         ISize(coverage.value().GetSize()),  // size
-        "SceneContents",                    // label
+        /*mip_count=*/1,
+        "SceneContents",  // label
         RenderTarget::AttachmentConfigMSAA{
             .storage_mode = StorageMode::kDeviceTransient,
             .resolve_storage_mode = StorageMode::kDevicePrivate,
@@ -68,7 +69,8 @@
         *renderer.GetContext(),             // context
         *renderer.GetRenderTargetCache(),   // allocator
         ISize(coverage.value().GetSize()),  // size
-        "SceneContents",                    // label
+        /*mip_count=*/1,
+        "SceneContents",  // label
         RenderTarget::AttachmentConfig{
             .storage_mode = StorageMode::kDevicePrivate,
             .load_action = LoadAction::kClear,
diff --git a/impeller/entity/contents/tiled_texture_contents_unittests.cc b/impeller/entity/contents/tiled_texture_contents_unittests.cc
index 2060f23..4715eaa 100644
--- a/impeller/entity/contents/tiled_texture_contents_unittests.cc
+++ b/impeller/entity/contents/tiled_texture_contents_unittests.cc
@@ -33,7 +33,8 @@
   auto buffer = content_context->GetContext()->CreateCommandBuffer();
   auto render_target = RenderTarget::CreateOffscreenMSAA(
       *content_context->GetContext(),
-      *GetContentContext()->GetRenderTargetCache(), {100, 100});
+      *GetContentContext()->GetRenderTargetCache(), {100, 100},
+      /*mip_count=*/1);
   auto render_pass = buffer->CreateRenderPass(render_target);
 
   ASSERT_TRUE(contents.Render(*GetContentContext(), {}, *render_pass));
@@ -68,7 +69,8 @@
   auto buffer = content_context->GetContext()->CreateCommandBuffer();
   auto render_target = RenderTarget::CreateOffscreenMSAA(
       *content_context->GetContext(),
-      *GetContentContext()->GetRenderTargetCache(), {100, 100});
+      *GetContentContext()->GetRenderTargetCache(), {100, 100},
+      /*mip_count=*/1);
   auto render_pass = buffer->CreateRenderPass(render_target);
 
   ASSERT_TRUE(contents.Render(*GetContentContext(), {}, *render_pass));
diff --git a/impeller/entity/contents/vertices_contents_unittests.cc b/impeller/entity/contents/vertices_contents_unittests.cc
index 173b13e..17fac72 100644
--- a/impeller/entity/contents/vertices_contents_unittests.cc
+++ b/impeller/entity/contents/vertices_contents_unittests.cc
@@ -61,7 +61,8 @@
   auto buffer = content_context->GetContext()->CreateCommandBuffer();
   auto render_target = RenderTarget::CreateOffscreenMSAA(
       *content_context->GetContext(),
-      *GetContentContext()->GetRenderTargetCache(), {100, 100});
+      *GetContentContext()->GetRenderTargetCache(), {100, 100},
+      /*mip_count=*/1);
   auto render_pass = buffer->CreateRenderPass(render_target);
   Entity entity;
 
diff --git a/impeller/entity/entity_pass.cc b/impeller/entity/entity_pass.cc
index 67e343d..576c3a9 100644
--- a/impeller/entity/entity_pass.cc
+++ b/impeller/entity/entity_pass.cc
@@ -247,6 +247,7 @@
 
 static EntityPassTarget CreateRenderTarget(ContentContext& renderer,
                                            ISize size,
+                                           int mip_count,
                                            const Color& clear_color) {
   auto context = renderer.GetContext();
 
@@ -258,24 +259,27 @@
   RenderTarget target;
   if (context->GetCapabilities()->SupportsOffscreenMSAA()) {
     target = RenderTarget::CreateOffscreenMSAA(
-        *context,                          // context
-        *renderer.GetRenderTargetCache(),  // allocator
-        size,                              // size
-        "EntityPass",                      // label
+        /*context=*/*context,
+        /*allocator=*/*renderer.GetRenderTargetCache(),
+        /*size=*/size,
+        /*mip_count=*/mip_count,
+        /*label=*/"EntityPass",
+        /*color_attachment_config=*/
         RenderTarget::AttachmentConfigMSAA{
             .storage_mode = StorageMode::kDeviceTransient,
             .resolve_storage_mode = StorageMode::kDevicePrivate,
             .load_action = LoadAction::kDontCare,
             .store_action = StoreAction::kMultisampleResolve,
-            .clear_color = clear_color},  // color_attachment_config
-        kDefaultStencilConfig             // stencil_attachment_config
-    );
+            .clear_color = clear_color},
+        /*stencil_attachment_config=*/
+        kDefaultStencilConfig);
   } else {
     target = RenderTarget::CreateOffscreen(
         *context,                          // context
         *renderer.GetRenderTargetCache(),  // allocator
         size,                              // size
-        "EntityPass",                      // label
+        /*mip_count=*/mip_count,
+        "EntityPass",  // label
         RenderTarget::AttachmentConfig{
             .storage_mode = StorageMode::kDevicePrivate,
             .load_action = LoadAction::kDontCare,
@@ -321,13 +325,23 @@
                   Rect::MakeSize(root_render_target.GetRenderTargetSize()),
                   {.readonly = true});
 
-  IterateAllEntities([lazy_glyph_atlas =
-                          renderer.GetLazyGlyphAtlas()](const Entity& entity) {
-    if (const auto& contents = entity.GetContents()) {
-      contents->PopulateGlyphAtlas(lazy_glyph_atlas, entity.DeriveTextScale());
-    }
-    return true;
-  });
+  int32_t required_mip_count = 1;
+  IterateAllElements(
+      [&required_mip_count, lazy_glyph_atlas = renderer.GetLazyGlyphAtlas()](
+          const Element& element) {
+        if (auto entity = std::get_if<Entity>(&element)) {
+          if (const auto& contents = entity->GetContents()) {
+            contents->PopulateGlyphAtlas(lazy_glyph_atlas,
+                                         entity->DeriveTextScale());
+          }
+        }
+        if (auto subpass = std::get_if<std::unique_ptr<EntityPass>>(&element)) {
+          const EntityPass* entity_pass = subpass->get();
+          required_mip_count =
+              std::max(required_mip_count, entity_pass->GetRequiredMipCount());
+        }
+        return true;
+      });
 
   ClipCoverageStack clip_coverage_stack = {ClipCoverageLayer{
       .coverage = Rect::MakeSize(root_render_target.GetRenderTargetSize()),
@@ -338,8 +352,8 @@
   // and then blit the results onto the onscreen texture. If using this branch,
   // there's no need to set up a stencil attachment on the root render target.
   if (reads_from_onscreen_backdrop) {
-    auto offscreen_target = CreateRenderTarget(
-        renderer, root_render_target.GetRenderTargetSize(),
+    EntityPassTarget offscreen_target = CreateRenderTarget(
+        renderer, root_render_target.GetRenderTargetSize(), required_mip_count,
         GetClearColorOrDefault(render_target.GetRenderTargetSize()));
 
     if (!OnRender(renderer,  // renderer
@@ -599,8 +613,9 @@
     }
 
     auto subpass_target = CreateRenderTarget(
-        renderer,                                        // renderer
-        subpass_size,                                    // size
+        renderer,      // renderer
+        subpass_size,  // size
+        /*mip_count=*/1,
         subpass->GetClearColorOrDefault(subpass_size));  // clear_color
 
     if (!subpass_target.IsValid()) {
@@ -1015,6 +1030,25 @@
   }
 }
 
+void EntityPass::IterateAllElements(
+    const std::function<bool(const Element&)>& iterator) const {
+  /// TODO(gaaclarke): Remove duplication here between const and non-const
+  /// versions.
+  if (!iterator) {
+    return;
+  }
+
+  for (auto& element : elements_) {
+    if (!iterator(element)) {
+      return;
+    }
+    if (auto subpass = std::get_if<std::unique_ptr<EntityPass>>(&element)) {
+      const EntityPass* entity_pass = subpass->get();
+      entity_pass->IterateAllElements(iterator);
+    }
+  }
+}
+
 void EntityPass::IterateAllEntities(
     const std::function<bool(Entity&)>& iterator) {
   if (!iterator) {
diff --git a/impeller/entity/entity_pass.h b/impeller/entity/entity_pass.h
index 39f5449..7de68d0 100644
--- a/impeller/entity/entity_pass.h
+++ b/impeller/entity/entity_pass.h
@@ -101,6 +101,9 @@
   ///         it's included in the stream before its children.
   void IterateAllElements(const std::function<bool(Element&)>& iterator);
 
+  void IterateAllElements(
+      const std::function<bool(const Element&)>& iterator) const;
+
   //----------------------------------------------------------------------------
   /// @brief  Iterate all entities in this pass, recursively including entities
   ///         of child passes. The iteration order is depth-first.
@@ -148,6 +151,12 @@
 
   void SetEnableOffscreenCheckerboard(bool enabled);
 
+  int32_t GetRequiredMipCount() const { return required_mip_count_; }
+
+  void SetRequiredMipCount(int32_t 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
@@ -297,6 +306,7 @@
   std::optional<Rect> bounds_limit_;
   std::unique_ptr<EntityPassClipRecorder> clip_replay_ =
       std::make_unique<EntityPassClipRecorder>();
+  int32_t required_mip_count_ = 1;
 
   /// These values are incremented whenever something is added to the pass that
   /// requires reading from the backdrop texture. Currently, this can happen in
diff --git a/impeller/entity/entity_pass_target_unittests.cc b/impeller/entity/entity_pass_target_unittests.cc
index 390f792..108da36 100644
--- a/impeller/entity/entity_pass_target_unittests.cc
+++ b/impeller/entity/entity_pass_target_unittests.cc
@@ -26,7 +26,8 @@
   auto buffer = content_context->GetContext()->CreateCommandBuffer();
   auto render_target = RenderTarget::CreateOffscreenMSAA(
       *content_context->GetContext(),
-      *GetContentContext()->GetRenderTargetCache(), {100, 100});
+      *GetContentContext()->GetRenderTargetCache(), {100, 100},
+      /*mip_count=*/1);
 
   auto entity_pass_target = EntityPassTarget(render_target, false, false);
 
diff --git a/impeller/entity/entity_unittests.cc b/impeller/entity/entity_unittests.cc
index 202cd77..37b926a 100644
--- a/impeller/entity/entity_unittests.cc
+++ b/impeller/entity/entity_unittests.cc
@@ -48,6 +48,7 @@
 #include "impeller/renderer/command.h"
 #include "impeller/renderer/pipeline_descriptor.h"
 #include "impeller/renderer/render_pass.h"
+#include "impeller/renderer/testing/mocks.h"
 #include "impeller/renderer/vertex_buffer_builder.h"
 #include "impeller/typographer/backends/skia/text_frame_skia.h"
 #include "impeller/typographer/backends/skia/typographer_context_skia.h"
@@ -2510,8 +2511,9 @@
       .store_action = StoreAction::kDontCare,
       .clear_color = Color::BlackTransparent()};
   auto rt = RenderTarget::CreateOffscreen(
-      *GetContext(), *test_allocator, ISize::MakeWH(1000, 1000), "Offscreen",
-      RenderTarget::kDefaultColorAttachmentConfig, stencil_config);
+      *GetContext(), *test_allocator, ISize::MakeWH(1000, 1000),
+      /*mip_count=*/1, "Offscreen", RenderTarget::kDefaultColorAttachmentConfig,
+      stencil_config);
   auto content_context = ContentContext(
       GetContext(), TypographerContextSkia::Make(), test_allocator);
   pass->AddEntity(std::move(entity));
diff --git a/impeller/entity/inline_pass_context.cc b/impeller/entity/inline_pass_context.cc
index b8295c2..1a4589c 100644
--- a/impeller/entity/inline_pass_context.cc
+++ b/impeller/entity/inline_pass_context.cc
@@ -6,11 +6,13 @@
 
 #include <utility>
 
+#include "flutter/fml/status.h"
 #include "impeller/base/allocation.h"
 #include "impeller/base/validation.h"
 #include "impeller/core/formats.h"
 #include "impeller/entity/entity_pass_target.h"
 #include "impeller/renderer/command_buffer.h"
+#include "impeller/renderer/texture_mipmap.h"
 
 namespace impeller {
 
@@ -64,6 +66,15 @@
     }
   }
 
+  std::shared_ptr<Texture> target_texture =
+      GetPassTarget().GetRenderTarget().GetRenderTargetTexture();
+  if (target_texture->GetMipCount() > 1) {
+    fml::Status mip_status = AddMipmapGeneration(context_, target_texture);
+    if (!mip_status.ok()) {
+      return false;
+    }
+  }
+
   pass_ = nullptr;
   command_buffer_ = nullptr;
 
diff --git a/impeller/entity/render_target_cache.h b/impeller/entity/render_target_cache.h
index b59bd43..382781b 100644
--- a/impeller/entity/render_target_cache.h
+++ b/impeller/entity/render_target_cache.h
@@ -43,6 +43,17 @@
   RenderTargetCache(const RenderTargetCache&) = delete;
 
   RenderTargetCache& operator=(const RenderTargetCache&) = delete;
+
+ public:
+  /// Visible for testing.
+  std::vector<TextureData>::const_iterator GetTextureDataBegin() const {
+    return texture_data_.begin();
+  }
+
+  /// Visible for testing.
+  std::vector<TextureData>::const_iterator GetTextureDataEnd() const {
+    return texture_data_.end();
+  }
 };
 
 }  // namespace impeller
diff --git a/impeller/golden_tests/golden_playground_test_mac.cc b/impeller/golden_tests/golden_playground_test_mac.cc
index 81cec2c..ef7bff9 100644
--- a/impeller/golden_tests/golden_playground_test_mac.cc
+++ b/impeller/golden_tests/golden_playground_test_mac.cc
@@ -55,6 +55,8 @@
     "impeller_Play_AiksTest_TextRotated_Vulkan",
     // Runtime stage based tests get confused with a Metal context.
     "impeller_Play_AiksTest_CanRenderClippedRuntimeEffects_Vulkan",
+    "impeller_Play_AiksTest_CaptureContext_Metal",
+    "impeller_Play_AiksTest_CaptureContext_Vulkan",
 };
 
 namespace {
@@ -153,7 +155,20 @@
 bool GoldenPlaygroundTest::OpenPlaygroundHere(
     AiksPlaygroundCallback
         callback) {  // NOLINT(performance-unnecessary-value-param)
-  return false;
+  AiksContext renderer(GetContext(), typographer_context_);
+
+  std::optional<Picture> picture;
+  std::unique_ptr<testing::MetalScreenshot> screenshot;
+  for (int i = 0; i < 2; ++i) {
+    picture = callback(renderer);
+    if (!picture.has_value()) {
+      return false;
+    }
+    screenshot = pimpl_->screenshotter->MakeScreenshot(
+        renderer, picture.value(), pimpl_->window_size);
+  }
+
+  return SaveScreenshot(std::move(screenshot));
 }
 
 std::shared_ptr<Texture> GoldenPlaygroundTest::CreateTextureForFixture(
diff --git a/impeller/renderer/BUILD.gn b/impeller/renderer/BUILD.gn
index 6b17817..3e0c7b5 100644
--- a/impeller/renderer/BUILD.gn
+++ b/impeller/renderer/BUILD.gn
@@ -92,6 +92,8 @@
     "snapshot.h",
     "surface.cc",
     "surface.h",
+    "texture_mipmap.cc",
+    "texture_mipmap.h",
     "vertex_buffer_builder.cc",
     "vertex_buffer_builder.h",
     "vertex_descriptor.cc",
diff --git a/impeller/renderer/render_target.cc b/impeller/renderer/render_target.cc
index 3a9f1e0..d4a1e3c 100644
--- a/impeller/renderer/render_target.cc
+++ b/impeller/renderer/render_target.cc
@@ -224,6 +224,7 @@
     const Context& context,
     RenderTargetAllocator& allocator,
     ISize size,
+    int mip_count,
     const std::string& label,
     AttachmentConfig color_attachment_config,
     std::optional<AttachmentConfig> stencil_attachment_config) {
@@ -237,6 +238,7 @@
   color_tex0.storage_mode = color_attachment_config.storage_mode;
   color_tex0.format = pixel_format;
   color_tex0.size = size;
+  color_tex0.mip_count = mip_count;
   color_tex0.usage = static_cast<uint64_t>(TextureUsage::kRenderTarget) |
                      static_cast<uint64_t>(TextureUsage::kShaderRead);
 
@@ -266,6 +268,7 @@
     const Context& context,
     RenderTargetAllocator& allocator,
     ISize size,
+    int mip_count,
     const std::string& label,
     AttachmentConfigMSAA color_attachment_config,
     std::optional<AttachmentConfig> stencil_attachment_config) {
@@ -310,6 +313,7 @@
   color0_resolve_tex_desc.usage =
       static_cast<uint64_t>(TextureUsage::kRenderTarget) |
       static_cast<uint64_t>(TextureUsage::kShaderRead);
+  color0_resolve_tex_desc.mip_count = mip_count;
 
   auto color0_resolve_tex = allocator.CreateTexture(color0_resolve_tex_desc);
   if (!color0_resolve_tex) {
diff --git a/impeller/renderer/render_target.h b/impeller/renderer/render_target.h
index dbd115a..997ebac 100644
--- a/impeller/renderer/render_target.h
+++ b/impeller/renderer/render_target.h
@@ -86,6 +86,7 @@
       const Context& context,
       RenderTargetAllocator& allocator,
       ISize size,
+      int mip_count,
       const std::string& label = "Offscreen",
       AttachmentConfig color_attachment_config = kDefaultColorAttachmentConfig,
       std::optional<AttachmentConfig> stencil_attachment_config =
@@ -95,6 +96,7 @@
       const Context& context,
       RenderTargetAllocator& allocator,
       ISize size,
+      int mip_count,
       const std::string& label = "Offscreen MSAA",
       AttachmentConfigMSAA color_attachment_config =
           kDefaultColorAttachmentConfigMSAA,
diff --git a/impeller/renderer/renderer_unittests.cc b/impeller/renderer/renderer_unittests.cc
index e36f634..0881fd6 100644
--- a/impeller/renderer/renderer_unittests.cc
+++ b/impeller/renderer/renderer_unittests.cc
@@ -1261,8 +1261,8 @@
   auto render_target_cache = std::make_shared<RenderTargetAllocator>(
       GetContext()->GetResourceAllocator());
 
-  auto render_target =
-      RenderTarget::CreateOffscreen(*context, *render_target_cache, {100, 100});
+  auto render_target = RenderTarget::CreateOffscreen(
+      *context, *render_target_cache, {100, 100}, /*mip_count=*/1);
   auto render_pass = cmd_buffer->CreateRenderPass(render_target);
 
   render_pass->ReserveCommands(100u);
@@ -1276,8 +1276,8 @@
   auto render_target_cache = std::make_shared<RenderTargetAllocator>(
       GetContext()->GetResourceAllocator());
 
-  auto render_target =
-      RenderTarget::CreateOffscreen(*context, *render_target_cache, {100, 100});
+  auto render_target = RenderTarget::CreateOffscreen(
+      *context, *render_target_cache, {100, 100}, /*mip_count=*/1);
   auto render_pass = cmd_buffer->CreateRenderPass(render_target);
 
   EXPECT_EQ(render_pass->GetSampleCount(), render_target.GetSampleCount());
diff --git a/impeller/renderer/texture_mipmap.cc b/impeller/renderer/texture_mipmap.cc
new file mode 100644
index 0000000..0a633c6
--- /dev/null
+++ b/impeller/renderer/texture_mipmap.cc
@@ -0,0 +1,31 @@
+// 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/renderer/texture_mipmap.h"
+#include "impeller/renderer/blit_pass.h"
+#include "impeller/renderer/command_buffer.h"
+
+namespace impeller {
+
+fml::Status AddMipmapGeneration(const std::shared_ptr<Context>& context,
+                                const std::shared_ptr<Texture>& texture) {
+  std::shared_ptr<CommandBuffer> mip_cmd_buffer =
+      context->CreateCommandBuffer();
+  std::shared_ptr<BlitPass> blit_pass = mip_cmd_buffer->CreateBlitPass();
+  bool success = blit_pass->GenerateMipmap(texture);
+  if (!success) {
+    return fml::Status(fml::StatusCode::kUnknown, "");
+  }
+  success = blit_pass->EncodeCommands(context->GetResourceAllocator());
+  if (!success) {
+    return fml::Status(fml::StatusCode::kUnknown, "");
+  }
+  success = mip_cmd_buffer->SubmitCommands(/*callback=*/nullptr);
+  if (!success) {
+    return fml::Status(fml::StatusCode::kUnknown, "");
+  }
+  return {};
+}
+
+}  // namespace impeller
diff --git a/impeller/renderer/texture_mipmap.h b/impeller/renderer/texture_mipmap.h
new file mode 100644
index 0000000..f2f7d8c
--- /dev/null
+++ b/impeller/renderer/texture_mipmap.h
@@ -0,0 +1,21 @@
+// 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_TEXTURE_MIPMAP_H_
+#define FLUTTER_IMPELLER_TEXTURE_MIPMAP_H_
+
+#include "flutter/fml/status.h"
+#include "impeller/core/texture.h"
+#include "impeller/renderer/context.h"
+
+namespace impeller {
+
+/// Adds a blit command to the render pass.
+[[nodiscard]] fml::Status AddMipmapGeneration(
+    const std::shared_ptr<Context>& context,
+    const std::shared_ptr<Texture>& texture);
+
+}  // namespace impeller
+
+#endif  // FLUTTER_IMPELLER_TEXTURE_MIPMAP_H_