[Impeller] optimize drawImageRect with blend and matrix color filter. (#165998)

Fixes https://github.com/flutter/flutter/issues/165959

Flame uses a lot of individual calls to drawImageRect w/ blend color
filters to color the sprites. This is much slower with Impeller today,
because we convert this into:

```
saveLayer(color filter)
drawImageRect
restore
```

Instead, we can detect this case and convert to a call to drawAtlas.
drawAtlas will use the porter-duff shader which removes the saveLayer
entirely. We could do this optimization while recording, but for now its
fine to do in dispatch.

We do have a snapshot optimization that catches "normal" image usage,
but it doesn't apply in the flame case as the source rects are sampling
from a larger texture and we don't have a way to pass those through the
snapshot system.

## Before

<img
src="https://github.com/user-attachments/assets/faada042-843f-4f15-af20-9baf862f66fd"
width="200"/>


## After

<img
src="https://github.com/user-attachments/assets/c83e061c-190f-4613-a725-88e16b776c04"
width="200"/>

---------

Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com>
diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_atlas_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_atlas_unittests.cc
index 3206db5..6d4d4fe 100644
--- a/engine/src/flutter/impeller/display_list/aiks_dl_atlas_unittests.cc
+++ b/engine/src/flutter/impeller/display_list/aiks_dl_atlas_unittests.cc
@@ -3,6 +3,10 @@
 // found in the LICENSE file.
 
 #include "display_list/dl_sampling_options.h"
+#include "display_list/dl_types.h"
+#include "display_list/effects/dl_color_filter.h"
+#include "display_list/effects/image_filters/dl_matrix_image_filter.h"
+#include "display_list/geometry/dl_geometry_types.h"
 #include "flutter/impeller/display_list/aiks_unittests.h"
 
 #include "flutter/display_list/dl_blend_mode.h"
@@ -248,5 +252,70 @@
   EXPECT_TRUE(geom.ShouldSkip());
 }
 
+TEST_P(AiksTest, DrawImageRectWithBlendColorFilter) {
+  sk_sp<DlImageImpeller> texture =
+      DlImageImpeller::Make(CreateTextureForFixture("bay_bridge.jpg"));
+
+  DisplayListBuilder builder;
+  DlPaint paint = DlPaint().setColorFilter(DlColorFilter::MakeBlend(
+      DlColor::kRed().withAlphaF(0.4), DlBlendMode::kSrcOver));
+
+  DlMatrix filter_matrix = DlMatrix();
+  auto filter = flutter::DlMatrixImageFilter(filter_matrix,
+                                             flutter::DlImageSampling::kLinear);
+  DlPaint paint_with_filter = paint;
+  paint_with_filter.setImageFilter(&filter);
+
+  // Compare porter-duff blend modes.
+  builder.DrawPaint(DlPaint().setColor(DlColor::kWhite()));
+  // Uses image filter to disable atlas conversion.
+  builder.DrawImageRect(texture, DlRect::MakeSize(texture->GetSize()),
+                        DlRect::MakeLTRB(0, 0, 500, 500), {},
+                        &paint_with_filter);
+
+  // Uses atlas conversion.
+  builder.Translate(600, 0);
+  builder.DrawImageRect(texture, DlRect::MakeSize(texture->GetSize()),
+                        DlRect::MakeLTRB(0, 0, 500, 500), {}, &paint);
+
+  ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
+}
+
+TEST_P(AiksTest, DrawImageRectWithMatrixColorFilter) {
+  sk_sp<DlImageImpeller> texture =
+      DlImageImpeller::Make(CreateTextureForFixture("bay_bridge.jpg"));
+
+  DisplayListBuilder builder;
+  static const constexpr ColorMatrix kColorInversion = {
+      .array = {
+          -1.0, 0,    0,    1.0, 0,  //
+          0,    -1.0, 0,    1.0, 0,  //
+          0,    0,    -1.0, 1.0, 0,  //
+          1.0,  1.0,  1.0,  1.0, 0   //
+      }};
+  DlPaint paint = DlPaint().setColorFilter(
+      DlColorFilter::MakeMatrix(kColorInversion.array));
+
+  DlMatrix filter_matrix = DlMatrix();
+  auto filter = flutter::DlMatrixImageFilter(filter_matrix,
+                                             flutter::DlImageSampling::kLinear);
+  DlPaint paint_with_filter = paint;
+  paint_with_filter.setImageFilter(&filter);
+
+  // Compare inverting color matrix filter.
+  builder.DrawPaint(DlPaint().setColor(DlColor::kWhite()));
+  // Uses image filter to disable atlas conversion.
+  builder.DrawImageRect(texture, DlRect::MakeSize(texture->GetSize()),
+                        DlRect::MakeLTRB(0, 0, 500, 500), {},
+                        &paint_with_filter);
+
+  // Uses atlas conversion.
+  builder.Translate(600, 0);
+  builder.DrawImageRect(texture, DlRect::MakeSize(texture->GetSize()),
+                        DlRect::MakeLTRB(0, 0, 500, 500), {}, &paint);
+
+  ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
+}
+
 }  // namespace testing
 }  // namespace impeller
diff --git a/engine/src/flutter/impeller/display_list/canvas.cc b/engine/src/flutter/impeller/display_list/canvas.cc
index 17436c1..442c54d 100644
--- a/engine/src/flutter/impeller/display_list/canvas.cc
+++ b/engine/src/flutter/impeller/display_list/canvas.cc
@@ -9,13 +9,18 @@
 #include <unordered_map>
 #include <utility>
 
+#include "display_list/effects/color_filters/dl_blend_color_filter.h"
+#include "display_list/effects/color_filters/dl_matrix_color_filter.h"
+#include "display_list/effects/dl_color_filter.h"
 #include "display_list/effects/dl_color_source.h"
 #include "display_list/effects/dl_image_filter.h"
+#include "display_list/image/dl_image.h"
 #include "flutter/fml/logging.h"
 #include "flutter/fml/trace_event.h"
 #include "impeller/base/validation.h"
 #include "impeller/core/formats.h"
 #include "impeller/display_list/color_filter.h"
+#include "impeller/display_list/dl_atlas_geometry.h"
 #include "impeller/display_list/image_filter.h"
 #include "impeller/display_list/skia_conversions.h"
 #include "impeller/entity/contents/atlas_contents.h"
@@ -44,12 +49,19 @@
 #include "impeller/geometry/color.h"
 #include "impeller/geometry/constants.h"
 #include "impeller/geometry/path_builder.h"
+#include "impeller/geometry/rstransform.h"
 #include "impeller/renderer/command_buffer.h"
 
 namespace impeller {
 
 namespace {
 
+bool IsPipelineBlendOrMatrixFilter(const flutter::DlColorFilter* filter) {
+  return filter->type() == flutter::DlColorFilterType::kMatrix ||
+         (filter->type() == flutter::DlColorFilterType::kBlend &&
+          filter->asBlend()->mode() <= Entity::kLastPipelineBlendMode);
+}
+
 static bool UseColorSourceContents(
     const std::shared_ptr<VerticesGeometry>& vertices,
     const Paint& paint) {
@@ -156,8 +168,10 @@
   }
 
   return std::make_unique<EntityPassTarget>(
-      target, renderer.GetDeviceCapabilities().SupportsReadFromResolve(),
-      renderer.GetDeviceCapabilities().SupportsImplicitResolvingMSAA());
+      target,                                                           //
+      renderer.GetDeviceCapabilities().SupportsReadFromResolve(),       //
+      renderer.GetDeviceCapabilities().SupportsImplicitResolvingMSAA()  //
+  );
 }
 
 }  // namespace
@@ -315,6 +329,78 @@
   AddRenderEntityWithFiltersToCurrentPass(entity, &geom, paint);
 }
 
+// Optimization: if the texture has a color filter that is a simple
+// porter-duff blend or matrix filter, then instead of performing a save layer
+// we should swap out the shader for the porter duff blend shader and avoid a
+// saveLayer. This can only be done for imageRects without a strict source
+// rect, as the porter duff shader does not support this feature. This
+// optimization is important for Flame.
+bool Canvas::AttemptColorFilterOptimization(
+    const std::shared_ptr<Texture>& image,
+    Rect source,
+    Rect dest,
+    const Paint& paint,
+    const SamplerDescriptor& sampler,
+    SourceRectConstraint src_rect_constraint) {
+  if (src_rect_constraint == SourceRectConstraint::kStrict ||  //
+      !paint.color_filter ||                                   //
+      paint.image_filter != nullptr ||                         //
+      paint.invert_colors ||                                   //
+      paint.mask_blur_descriptor.has_value() ||                //
+      !IsPipelineBlendOrMatrixFilter(paint.color_filter)) {
+    return false;
+  }
+
+  if (paint.color_filter->type() == flutter::DlColorFilterType::kBlend) {
+    const flutter::DlBlendColorFilter* blend_filter =
+        paint.color_filter->asBlend();
+    DrawImageRectAtlasGeometry geometry = DrawImageRectAtlasGeometry(
+        /*texture=*/image,
+        /*source=*/source,
+        /*destination=*/dest,
+        /*color=*/skia_conversions::ToColor(blend_filter->color()),
+        /*blend_mode=*/blend_filter->mode(),
+        /*desc=*/sampler);
+
+    auto atlas_contents = std::make_shared<AtlasContents>();
+    atlas_contents->SetGeometry(&geometry);
+    atlas_contents->SetAlpha(paint.color.alpha);
+
+    Entity entity;
+    entity.SetTransform(GetCurrentTransform());
+    entity.SetBlendMode(paint.blend_mode);
+    entity.SetContents(atlas_contents);
+
+    AddRenderEntityToCurrentPass(entity);
+  } else {
+    const flutter::DlMatrixColorFilter* matrix_filter =
+        paint.color_filter->asMatrix();
+
+    DrawImageRectAtlasGeometry geometry = DrawImageRectAtlasGeometry(
+        /*texture=*/image,
+        /*source=*/source,
+        /*destination=*/dest,
+        /*color=*/Color::Khaki(),            // ignored
+        /*blend_mode=*/BlendMode::kSrcOver,  // ignored
+        /*desc=*/sampler);
+
+    auto atlas_contents = std::make_shared<ColorFilterAtlasContents>();
+    atlas_contents->SetGeometry(&geometry);
+    atlas_contents->SetAlpha(paint.color.alpha);
+    impeller::ColorMatrix color_matrix;
+    matrix_filter->get_matrix(color_matrix.array);
+    atlas_contents->SetMatrix(color_matrix);
+
+    Entity entity;
+    entity.SetTransform(GetCurrentTransform());
+    entity.SetBlendMode(paint.blend_mode);
+    entity.SetContents(atlas_contents);
+
+    AddRenderEntityToCurrentPass(entity);
+  }
+  return true;
+}
+
 bool Canvas::AttemptDrawBlurredRRect(const Rect& rect,
                                      Size corner_radii,
                                      const Paint& paint) {
@@ -703,8 +789,8 @@
     return;
   }
 
-  const auto source = Rect::MakeSize(image->GetSize());
-  const auto dest = source.Shift(offset);
+  const Rect source = Rect::MakeSize(image->GetSize());
+  const Rect dest = source.Shift(offset);
 
   DrawImageRect(image, source, dest, paint, sampler);
 }
@@ -719,8 +805,7 @@
     return;
   }
 
-  auto size = image->GetSize();
-
+  ISize size = image->GetSize();
   if (size.IsEmpty()) {
     return;
   }
@@ -730,6 +815,12 @@
   if (!clipped_source) {
     return;
   }
+
+  if (AttemptColorFilterOptimization(image, source, dest, paint, sampler,
+                                     src_rect_constraint)) {
+    return;
+  }
+
   if (*clipped_source != source) {
     Scalar sx = dest.GetWidth() / source.GetWidth();
     Scalar sy = dest.GetHeight() / source.GetHeight();
diff --git a/engine/src/flutter/impeller/display_list/canvas.h b/engine/src/flutter/impeller/display_list/canvas.h
index 1962d48..746167b 100644
--- a/engine/src/flutter/impeller/display_list/canvas.h
+++ b/engine/src/flutter/impeller/display_list/canvas.h
@@ -357,6 +357,17 @@
                                Size corner_radii,
                                const Paint& paint);
 
+  /// For simple DrawImageRect calls, optimize any draws with a color filter
+  /// into the corresponding atlas draw.
+  ///
+  /// Returns whether not the optimization was applied.
+  bool AttemptColorFilterOptimization(const std::shared_ptr<Texture>& image,
+                                      Rect source,
+                                      Rect dest,
+                                      const Paint& paint,
+                                      const SamplerDescriptor& sampler,
+                                      SourceRectConstraint src_rect_constraint);
+
   RenderPass& GetCurrentRenderPass() const;
 
   Canvas(const Canvas&) = delete;
diff --git a/engine/src/flutter/impeller/display_list/dl_atlas_geometry.cc b/engine/src/flutter/impeller/display_list/dl_atlas_geometry.cc
index 2188770..bcd473a 100644
--- a/engine/src/flutter/impeller/display_list/dl_atlas_geometry.cc
+++ b/engine/src/flutter/impeller/display_list/dl_atlas_geometry.cc
@@ -54,7 +54,7 @@
   return bounding_box;
 }
 
-std::shared_ptr<Texture> DlAtlasGeometry::GetAtlas() const {
+const std::shared_ptr<Texture>& DlAtlasGeometry::GetAtlas() const {
   return atlas_;
 }
 
@@ -69,16 +69,15 @@
 VertexBuffer DlAtlasGeometry::CreateSimpleVertexBuffer(
     HostBuffer& host_buffer) const {
   using VS = TextureFillVertexShader;
-
   constexpr size_t indices[6] = {0, 1, 2, 1, 2, 3};
 
-  auto buffer_view = host_buffer.Emplace(
+  BufferView buffer_view = host_buffer.Emplace(
       sizeof(VS::PerVertexData) * count_ * 6, alignof(VS::PerVertexData),
       [&](uint8_t* raw_data) {
         VS::PerVertexData* data =
             reinterpret_cast<VS::PerVertexData*>(raw_data);
         int offset = 0;
-        auto texture_size = atlas_->GetSize();
+        ISize texture_size = atlas_->GetSize();
         for (auto i = 0u; i < count_; i++) {
           flutter::DlRect sample_rect = tex_[i];
           auto points = sample_rect.GetPoints();
@@ -102,16 +101,15 @@
 VertexBuffer DlAtlasGeometry::CreateBlendVertexBuffer(
     HostBuffer& host_buffer) const {
   using VS = PorterDuffBlendVertexShader;
-
   constexpr size_t indices[6] = {0, 1, 2, 1, 2, 3};
 
-  auto buffer_view = host_buffer.Emplace(
+  BufferView buffer_view = host_buffer.Emplace(
       sizeof(VS::PerVertexData) * count_ * 6, alignof(VS::PerVertexData),
       [&](uint8_t* raw_data) {
         VS::PerVertexData* data =
             reinterpret_cast<VS::PerVertexData*>(raw_data);
         int offset = 0;
-        auto texture_size = atlas_->GetSize();
+        ISize texture_size = atlas_->GetSize();
         for (auto i = 0u; i < count_; i++) {
           flutter::DlRect sample_rect = tex_[i];
           auto points = sample_rect.GetPoints();
diff --git a/engine/src/flutter/impeller/display_list/dl_atlas_geometry.h b/engine/src/flutter/impeller/display_list/dl_atlas_geometry.h
index 3c29451..ed20379 100644
--- a/engine/src/flutter/impeller/display_list/dl_atlas_geometry.h
+++ b/engine/src/flutter/impeller/display_list/dl_atlas_geometry.h
@@ -38,7 +38,7 @@
 
   Rect ComputeBoundingBox() const override;
 
-  std::shared_ptr<Texture> GetAtlas() const override;
+  const std::shared_ptr<Texture>& GetAtlas() const override;
 
   const SamplerDescriptor& GetSamplerDescriptor() const override;
 
diff --git a/engine/src/flutter/impeller/entity/contents/atlas_contents.cc b/engine/src/flutter/impeller/entity/contents/atlas_contents.cc
index 4f1d1d4..6bd795a 100644
--- a/engine/src/flutter/impeller/entity/contents/atlas_contents.cc
+++ b/engine/src/flutter/impeller/entity/contents/atlas_contents.cc
@@ -8,6 +8,7 @@
 #include "impeller/entity/contents/atlas_contents.h"
 #include "impeller/entity/contents/content_context.h"
 #include "impeller/entity/contents/filters/blend_filter_contents.h"
+#include "impeller/entity/contents/pipelines.h"
 #include "impeller/entity/entity.h"
 #include "impeller/entity/texture_fill.frag.h"
 #include "impeller/entity/texture_fill.vert.h"
@@ -16,6 +17,114 @@
 
 namespace impeller {
 
+DrawImageRectAtlasGeometry::DrawImageRectAtlasGeometry(
+    std::shared_ptr<Texture> texture,
+    const Rect& source,
+    const Rect& destination,
+    const Color& color,
+    BlendMode blend_mode,
+    const SamplerDescriptor& desc)
+    : texture_(std::move(texture)),
+      source_(source),
+      destination_(destination),
+      color_(color),
+      blend_mode_(blend_mode),
+      desc_(desc) {}
+
+DrawImageRectAtlasGeometry::~DrawImageRectAtlasGeometry() = default;
+
+bool DrawImageRectAtlasGeometry::ShouldUseBlend() const {
+  return true;
+}
+
+bool DrawImageRectAtlasGeometry::ShouldSkip() const {
+  return false;
+}
+
+VertexBuffer DrawImageRectAtlasGeometry::CreateSimpleVertexBuffer(
+    HostBuffer& host_buffer) const {
+  using VS = TextureFillVertexShader;
+  constexpr size_t indices[6] = {0, 1, 2, 1, 2, 3};
+
+  BufferView buffer_view = host_buffer.Emplace(
+      sizeof(VS::PerVertexData) * 6, alignof(VS::PerVertexData),
+      [&](uint8_t* raw_data) {
+        VS::PerVertexData* data =
+            reinterpret_cast<VS::PerVertexData*>(raw_data);
+        int offset = 0;
+        std::array<TPoint<float>, 4> destination_points =
+            destination_.GetPoints();
+        std::array<TPoint<float>, 4> texture_coords =
+            Rect::MakeSize(texture_->GetSize()).Project(source_).GetPoints();
+        for (size_t j = 0; j < 6; j++) {
+          data[offset].position = destination_points[indices[j]];
+          data[offset].texture_coords = texture_coords[indices[j]];
+          offset++;
+        }
+      });
+
+  return VertexBuffer{
+      .vertex_buffer = buffer_view,
+      .index_buffer = {},
+      .vertex_count = 6,
+      .index_type = IndexType::kNone,
+  };
+}
+
+VertexBuffer DrawImageRectAtlasGeometry::CreateBlendVertexBuffer(
+    HostBuffer& host_buffer) const {
+  using VS = PorterDuffBlendVertexShader;
+  constexpr size_t indices[6] = {0, 1, 2, 1, 2, 3};
+
+  BufferView buffer_view = host_buffer.Emplace(
+      sizeof(VS::PerVertexData) * 6, alignof(VS::PerVertexData),
+      [&](uint8_t* raw_data) {
+        VS::PerVertexData* data =
+            reinterpret_cast<VS::PerVertexData*>(raw_data);
+        int offset = 0;
+        std::array<TPoint<float>, 4> texture_coords =
+            Rect::MakeSize(texture_->GetSize()).Project(source_).GetPoints();
+        std::array<TPoint<float>, 4> destination_points =
+            destination_.GetPoints();
+        for (size_t j = 0; j < 6; j++) {
+          data[offset].vertices = destination_points[indices[j]];
+          data[offset].texture_coords = texture_coords[indices[j]];
+          data[offset].color = color_.Premultiply();
+          offset++;
+        }
+      });
+
+  return VertexBuffer{
+      .vertex_buffer = buffer_view,
+      .index_buffer = {},
+      .vertex_count = 6,
+      .index_type = IndexType::kNone,
+  };
+}
+
+Rect DrawImageRectAtlasGeometry::ComputeBoundingBox() const {
+  return destination_;
+}
+
+const std::shared_ptr<Texture>& DrawImageRectAtlasGeometry::GetAtlas() const {
+  return texture_;
+}
+
+const SamplerDescriptor& DrawImageRectAtlasGeometry::GetSamplerDescriptor()
+    const {
+  return desc_;
+}
+
+BlendMode DrawImageRectAtlasGeometry::GetBlendMode() const {
+  return blend_mode_;
+}
+
+bool DrawImageRectAtlasGeometry::ShouldInvertBlendMode() const {
+  return false;
+}
+
+////
+
 AtlasContents::AtlasContents() = default;
 
 AtlasContents::~AtlasContents() = default;
@@ -42,11 +151,8 @@
     return true;
   }
 
-  auto dst_sampler_descriptor = geometry_->GetSamplerDescriptor();
-  if (renderer.GetDeviceCapabilities().SupportsDecalSamplerAddressMode()) {
-    dst_sampler_descriptor.width_address_mode = SamplerAddressMode::kDecal;
-    dst_sampler_descriptor.height_address_mode = SamplerAddressMode::kDecal;
-  }
+  const SamplerDescriptor& dst_sampler_descriptor =
+      geometry_->GetSamplerDescriptor();
   raw_ptr<const Sampler> dst_sampler =
       renderer.GetContext()->GetSamplerLibrary()->GetSampler(
           dst_sampler_descriptor);
@@ -56,8 +162,6 @@
     using VS = TextureFillVertexShader;
     using FS = TextureFillFragmentShader;
 
-    auto dst_sampler_descriptor = geometry_->GetSamplerDescriptor();
-
     raw_ptr<const Sampler> dst_sampler =
         renderer.GetContext()->GetSamplerLibrary()->GetSampler(
             dst_sampler_descriptor);
@@ -97,10 +201,12 @@
     pass.SetCommandLabel("DrawAtlas Blend");
 #endif  // IMPELLER_DEBUG
     pass.SetVertexBuffer(geometry_->CreateBlendVertexBuffer(host_buffer));
-    auto inverted_blend_mode =
-        InvertPorterDuffBlend(blend_mode).value_or(BlendMode::kSrc);
-    pass.SetPipeline(renderer.GetPorterDuffPipeline(inverted_blend_mode,
-                                                    OptionsFromPass(pass)));
+    BlendMode inverted_blend_mode =
+        geometry_->ShouldInvertBlendMode()
+            ? (InvertPorterDuffBlend(blend_mode).value_or(BlendMode::kSrc))
+            : blend_mode;
+    pass.SetPipeline(renderer.GetPorterDuffPipeline(
+        inverted_blend_mode, OptionsFromPassAndEntity(pass, entity)));
 
     FS::FragInfo frag_info;
     VS::FrameInfo frame_info;
@@ -134,7 +240,8 @@
 #endif  // IMPELLER_DEBUG
   pass.SetVertexBuffer(geometry_->CreateBlendVertexBuffer(host_buffer));
 
-  renderer.GetDrawVerticesUberPipeline(blend_mode, OptionsFromPass(pass));
+  renderer.GetDrawVerticesUberPipeline(blend_mode,
+                                       OptionsFromPassAndEntity(pass, entity));
   FS::BindTextureSampler(pass, geometry_->GetAtlas(), dst_sampler);
 
   VUS::FrameInfo frame_info;
@@ -157,4 +264,83 @@
   return pass.Draw().ok();
 }
 
+///////////////
+
+ColorFilterAtlasContents::ColorFilterAtlasContents() = default;
+
+ColorFilterAtlasContents::~ColorFilterAtlasContents() = default;
+
+std::optional<Rect> ColorFilterAtlasContents::GetCoverage(
+    const Entity& entity) const {
+  if (!geometry_) {
+    return std::nullopt;
+  }
+  return geometry_->ComputeBoundingBox().TransformBounds(entity.GetTransform());
+}
+
+void ColorFilterAtlasContents::SetGeometry(AtlasGeometry* geometry) {
+  geometry_ = geometry;
+}
+
+void ColorFilterAtlasContents::SetAlpha(Scalar alpha) {
+  alpha_ = alpha;
+}
+
+void ColorFilterAtlasContents::SetMatrix(ColorMatrix matrix) {
+  matrix_ = matrix;
+}
+
+bool ColorFilterAtlasContents::Render(const ContentContext& renderer,
+                                      const Entity& entity,
+                                      RenderPass& pass) const {
+  if (geometry_->ShouldSkip() || alpha_ <= 0.0) {
+    return true;
+  }
+
+  const SamplerDescriptor& dst_sampler_descriptor =
+      geometry_->GetSamplerDescriptor();
+
+  raw_ptr<const Sampler> dst_sampler =
+      renderer.GetContext()->GetSamplerLibrary()->GetSampler(
+          dst_sampler_descriptor);
+
+  auto& host_buffer = renderer.GetTransientsBuffer();
+
+  using VS = ColorMatrixColorFilterPipeline::VertexShader;
+  using FS = ColorMatrixColorFilterPipeline::FragmentShader;
+
+#ifdef IMPELLER_DEBUG
+  pass.SetCommandLabel("Atlas ColorFilter");
+#endif  // IMPELLER_DEBUG
+  pass.SetVertexBuffer(geometry_->CreateSimpleVertexBuffer(host_buffer));
+  pass.SetPipeline(
+      renderer.GetColorMatrixColorFilterPipeline(OptionsFromPass(pass)));
+
+  FS::FragInfo frag_info;
+  VS::FrameInfo frame_info;
+
+  FS::BindInputTexture(pass, geometry_->GetAtlas(), dst_sampler);
+  frame_info.texture_sampler_y_coord_scale =
+      geometry_->GetAtlas()->GetYCoordScale();
+
+  frag_info.input_alpha = 1;
+  frag_info.output_alpha = alpha_;
+  const float* matrix = matrix_.array;
+  frag_info.color_v = Vector4(matrix[4], matrix[9], matrix[14], matrix[19]);
+  frag_info.color_m = Matrix(matrix[0], matrix[5], matrix[10], matrix[15],  //
+                             matrix[1], matrix[6], matrix[11], matrix[16],  //
+                             matrix[2], matrix[7], matrix[12], matrix[17],  //
+                             matrix[3], matrix[8], matrix[13], matrix[18]   //
+  );
+
+  FS::BindFragInfo(pass, host_buffer.EmplaceUniform(frag_info));
+
+  frame_info.mvp = entity.GetShaderTransform(pass);
+
+  auto uniform_view = host_buffer.EmplaceUniform(frame_info);
+  VS::BindFrameInfo(pass, uniform_view);
+
+  return pass.Draw().ok();
+}
+
 }  // namespace impeller
diff --git a/engine/src/flutter/impeller/entity/contents/atlas_contents.h b/engine/src/flutter/impeller/entity/contents/atlas_contents.h
index a0c0046..f34c85f 100644
--- a/engine/src/flutter/impeller/entity/contents/atlas_contents.h
+++ b/engine/src/flutter/impeller/entity/contents/atlas_contents.h
@@ -30,11 +30,52 @@
 
   virtual Rect ComputeBoundingBox() const = 0;
 
-  virtual std::shared_ptr<Texture> GetAtlas() const = 0;
+  virtual const std::shared_ptr<Texture>& GetAtlas() const = 0;
 
   virtual const SamplerDescriptor& GetSamplerDescriptor() const = 0;
 
   virtual BlendMode GetBlendMode() const = 0;
+
+  virtual bool ShouldInvertBlendMode() const { return true; }
+};
+
+/// @brief An atlas geometry that adapts for drawImageRect.
+class DrawImageRectAtlasGeometry : public AtlasGeometry {
+ public:
+  DrawImageRectAtlasGeometry(std::shared_ptr<Texture> texture,
+                             const Rect& source,
+                             const Rect& destination,
+                             const Color& color,
+                             BlendMode blend_mode,
+                             const SamplerDescriptor& desc);
+
+  ~DrawImageRectAtlasGeometry();
+
+  bool ShouldUseBlend() const override;
+
+  bool ShouldSkip() const override;
+
+  VertexBuffer CreateSimpleVertexBuffer(HostBuffer& host_buffer) const override;
+
+  VertexBuffer CreateBlendVertexBuffer(HostBuffer& host_buffer) const override;
+
+  Rect ComputeBoundingBox() const override;
+
+  const std::shared_ptr<Texture>& GetAtlas() const override;
+
+  const SamplerDescriptor& GetSamplerDescriptor() const override;
+
+  BlendMode GetBlendMode() const override;
+
+  bool ShouldInvertBlendMode() const override;
+
+ private:
+  const std::shared_ptr<Texture> texture_;
+  const Rect source_;
+  const Rect destination_;
+  const Color color_;
+  const BlendMode blend_mode_;
+  const SamplerDescriptor desc_;
 };
 
 class AtlasContents final : public Contents {
@@ -64,6 +105,41 @@
   AtlasContents& operator=(const AtlasContents&) = delete;
 };
 
+/// A specialized atlas class for applying a color matrix filter to a
+/// drawImageRect call.
+class ColorFilterAtlasContents final : public Contents {
+ public:
+  explicit ColorFilterAtlasContents();
+
+  ~ColorFilterAtlasContents() override;
+
+  void SetGeometry(AtlasGeometry* geometry);
+
+  void SetAlpha(Scalar alpha);
+
+  void SetMatrix(ColorMatrix matrix);
+
+  // |Contents|
+  std::optional<Rect> GetCoverage(const Entity& entity) const override;
+
+  // |Contents|
+  bool Render(const ContentContext& renderer,
+              const Entity& entity,
+              RenderPass& pass) const override;
+
+ private:
+  // These contents are created temporarily on the stack and never stored.
+  // The referenced geometry is also stack allocated and will be de-allocated
+  // after the contents are.
+  AtlasGeometry* geometry_ = nullptr;
+  ColorMatrix matrix_;
+  Scalar alpha_ = 1.0;
+
+  ColorFilterAtlasContents(const ColorFilterAtlasContents&) = delete;
+
+  ColorFilterAtlasContents& operator=(const ColorFilterAtlasContents&) = delete;
+};
+
 }  // namespace impeller
 
 #endif  // FLUTTER_IMPELLER_ENTITY_CONTENTS_ATLAS_CONTENTS_H_
diff --git a/engine/src/flutter/impeller/entity/contents/filters/color_matrix_filter_contents.cc b/engine/src/flutter/impeller/entity/contents/filters/color_matrix_filter_contents.cc
index 0bcf9d8..337512f 100644
--- a/engine/src/flutter/impeller/entity/contents/filters/color_matrix_filter_contents.cc
+++ b/engine/src/flutter/impeller/entity/contents/filters/color_matrix_filter_contents.cc
@@ -64,10 +64,10 @@
     auto size = input_snapshot->texture->GetSize();
 
     std::array<VS::PerVertexData, 4> vertices = {
-        VS::PerVertexData{Point(0, 0)},
-        VS::PerVertexData{Point(1, 0)},
-        VS::PerVertexData{Point(0, 1)},
-        VS::PerVertexData{Point(1, 1)},
+        VS::PerVertexData{Point(0, 0), Point(0, 0)},
+        VS::PerVertexData{Point(1, 0), Point(1, 0)},
+        VS::PerVertexData{Point(0, 1), Point(0, 1)},
+        VS::PerVertexData{Point(1, 1), Point(1, 1)},
     };
     auto& host_buffer = renderer.GetTransientsBuffer();
     pass.SetVertexBuffer(
@@ -84,23 +84,21 @@
     FS::FragInfo frag_info;
     const float* matrix = color_matrix.array;
     frag_info.color_v = Vector4(matrix[4], matrix[9], matrix[14], matrix[19]);
-    // clang-format off
-    frag_info.color_m = Matrix(
-        matrix[0], matrix[5], matrix[10], matrix[15],
-        matrix[1], matrix[6], matrix[11], matrix[16],
-        matrix[2], matrix[7], matrix[12], matrix[17],
-        matrix[3], matrix[8], matrix[13], matrix[18]
+    frag_info.color_m = Matrix(matrix[0], matrix[5], matrix[10], matrix[15],  //
+                               matrix[1], matrix[6], matrix[11], matrix[16],  //
+                               matrix[2], matrix[7], matrix[12], matrix[17],  //
+                               matrix[3], matrix[8], matrix[13], matrix[18]   //
     );
-    // clang-format on
     frag_info.input_alpha =
         absorb_opacity == ColorFilterContents::AbsorbOpacity::kYes
             ? input_snapshot->opacity
             : 1.0f;
+    frag_info.output_alpha = 1;
+
     raw_ptr<const Sampler> sampler =
         renderer.GetContext()->GetSamplerLibrary()->GetSampler({});
     FS::BindInputTexture(pass, input_snapshot->texture, sampler);
     FS::BindFragInfo(pass, host_buffer.EmplaceUniform(frag_info));
-
     VS::BindFrameInfo(pass, host_buffer.EmplaceUniform(frame_info));
 
     return pass.Draw().ok();
diff --git a/engine/src/flutter/impeller/entity/contents/pipelines.h b/engine/src/flutter/impeller/entity/contents/pipelines.h
index 290e90c..f3998a7 100644
--- a/engine/src/flutter/impeller/entity/contents/pipelines.h
+++ b/engine/src/flutter/impeller/entity/contents/pipelines.h
@@ -99,7 +99,7 @@
 using BlendSoftLightPipeline = AdvancedBlendPipelineHandle;
 using BorderMaskBlurPipeline = RenderPipelineHandle<FilterPositionUvVertexShader, BorderMaskBlurFragmentShader>;
 using ClipPipeline = RenderPipelineHandle<ClipVertexShader, ClipFragmentShader>;
-using ColorMatrixColorFilterPipeline = RenderPipelineHandle<FilterPositionVertexShader, ColorMatrixColorFilterFragmentShader>;
+using ColorMatrixColorFilterPipeline = RenderPipelineHandle<FilterPositionUvVertexShader, ColorMatrixColorFilterFragmentShader>;
 using ConicalGradientFillConicalPipeline = GradientPipelineHandle<ConicalGradientFillConicalFragmentShader>;
 using ConicalGradientFillRadialPipeline = GradientPipelineHandle<ConicalGradientFillRadialFragmentShader>;
 using ConicalGradientFillStripPipeline = GradientPipelineHandle<ConicalGradientFillStripFragmentShader>;
diff --git a/engine/src/flutter/impeller/entity/shaders/filters/color_matrix_color_filter.frag b/engine/src/flutter/impeller/entity/shaders/filters/color_matrix_color_filter.frag
index b8f2da6..c9e74b1 100644
--- a/engine/src/flutter/impeller/entity/shaders/filters/color_matrix_color_filter.frag
+++ b/engine/src/flutter/impeller/entity/shaders/filters/color_matrix_color_filter.frag
@@ -35,6 +35,7 @@
   mat4 color_m;
   f16vec4 color_v;
   float16_t input_alpha;
+  float16_t output_alpha;
 }
 frag_info;
 
@@ -54,5 +55,5 @@
                 float16_t(0), float16_t(1.0));
 
   // premultiply the outputs
-  frag_color = f16vec4(color.rgb * color.a, color.a);
+  frag_color = IPHalfPremultiply(color) * frag_info.output_alpha;
 }
diff --git a/engine/src/flutter/impeller/tools/malioc.json b/engine/src/flutter/impeller/tools/malioc.json
index ed4cb61..83636d9 100644
--- a/engine/src/flutter/impeller/tools/malioc.json
+++ b/engine/src/flutter/impeller/tools/malioc.json
@@ -409,13 +409,11 @@
           "performance": {
             "longest_path_bound_pipelines": [
               "arith_total",
-              "arith_fma",
-              "varying",
-              "texture"
+              "arith_fma"
             ],
             "longest_path_cycles": [
-              0.25,
-              0.25,
+              0.28125,
+              0.28125,
               0.0625,
               0.0625,
               0.0,
@@ -433,13 +431,11 @@
             ],
             "shortest_path_bound_pipelines": [
               "arith_total",
-              "arith_fma",
-              "varying",
-              "texture"
+              "arith_fma"
             ],
             "shortest_path_cycles": [
-              0.25,
-              0.25,
+              0.28125,
+              0.28125,
               0.0625,
               0.0625,
               0.0,
@@ -448,13 +444,11 @@
             ],
             "total_bound_pipelines": [
               "arith_total",
-              "arith_fma",
-              "varying",
-              "texture"
+              "arith_fma"
             ],
             "total_cycles": [
-              0.25,
-              0.25,
+              0.28125,
+              0.28125,
               0.0625,
               0.0625,
               0.0,
@@ -464,8 +458,8 @@
           },
           "stack_spill_bytes": 0,
           "thread_occupancy": 100,
-          "uniform_registers_used": 14,
-          "work_registers_used": 10
+          "uniform_registers_used": 16,
+          "work_registers_used": 9
         }
       }
     }
@@ -2232,13 +2226,11 @@
           "performance": {
             "longest_path_bound_pipelines": [
               "arith_total",
-              "arith_fma",
-              "varying",
-              "texture"
+              "arith_fma"
             ],
             "longest_path_cycles": [
-              0.25,
-              0.25,
+              0.28125,
+              0.28125,
               0.09375,
               0.0625,
               0.0,
@@ -2256,13 +2248,11 @@
             ],
             "shortest_path_bound_pipelines": [
               "arith_total",
-              "arith_fma",
-              "varying",
-              "texture"
+              "arith_fma"
             ],
             "shortest_path_cycles": [
-              0.25,
-              0.25,
+              0.28125,
+              0.28125,
               0.0625,
               0.0625,
               0.0,
@@ -2271,13 +2261,11 @@
             ],
             "total_bound_pipelines": [
               "arith_total",
-              "arith_fma",
-              "varying",
-              "texture"
+              "arith_fma"
             ],
             "total_cycles": [
-              0.25,
-              0.25,
+              0.28125,
+              0.28125,
               0.09375,
               0.0625,
               0.0,
@@ -2305,7 +2293,7 @@
               "arithmetic"
             ],
             "longest_path_cycles": [
-              2.640000104904175,
+              2.9700000286102295,
               1.0,
               1.0
             ],
@@ -2318,7 +2306,7 @@
               "arithmetic"
             ],
             "shortest_path_cycles": [
-              2.640000104904175,
+              2.9700000286102295,
               1.0,
               1.0
             ],
@@ -2326,7 +2314,7 @@
               "arithmetic"
             ],
             "total_cycles": [
-              3.0,
+              3.3333332538604736,
               1.0,
               1.0
             ]