Decouple SkColorFilters and Ganesh GPU code

This moves all asFragmentProcessor code to GrFragmentProcessors.cpp

In order to do this, I needed to extract some class definitions
to headers and rename some files/filters to be more consistent
so we can use a macro like SK_ALL_COLOR_FILTERS.

I decided to expose some fields in the color filters as
public (but still internal to Skia) methods instead of defining
friend functions, as those definitions were long and unwieldy.

As I was doing this, I enforced IWYU on the files I modified
to help streamline #includes.

Suggested Review order:
 - SkColorFIlterBase.h
 - SkColorFilter.cpp -> SkComposeColorFilter,
   SkWorkingFormatColorFilter, SkColorSpaceXformColorFilter
 - SkTableColorFilter.cpp -> SkTableColorFilter.h,
   GrColorTableEffect
 - SkRuntimeEffect.cpp -> SkRuntimeColorFilter
 - SkColorFilter_Matrix.cpp -> SkMatrixColorFilter
 - Other colorfilters to see .cpp -> .h
 - GrFragmentProcessors.cpp
 - Remaining files.

Change-Id: Ifce96cf854166a46a81a4a354154a00d4c300e1d
Bug: skia:14317
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/703678
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Kevin Lubick <kjlubick@google.com>
diff --git a/gn/core.gni b/gn/core.gni
index 071972c..c8cf337 100644
--- a/gn/core.gni
+++ b/gn/core.gni
@@ -273,6 +273,8 @@
   "$_src/core/SkBlendMode.cpp",
   "$_src/core/SkBlendModeBlender.cpp",
   "$_src/core/SkBlendModeBlender.h",
+  "$_src/core/SkBlendModeColorFilter.cpp",
+  "$_src/core/SkBlendModeColorFilter.h",
   "$_src/core/SkBlendModePriv.h",
   "$_src/core/SkBlenderBase.h",
   "$_src/core/SkBlitBWMaskTemplate.h",
@@ -306,11 +308,14 @@
   "$_src/core/SkColorFilter.cpp",
   "$_src/core/SkColorFilterBase.h",
   "$_src/core/SkColorFilterPriv.h",
-  "$_src/core/SkColorFilter_Matrix.cpp",
   "$_src/core/SkColorSpace.cpp",
   "$_src/core/SkColorSpacePriv.h",
+  "$_src/core/SkColorSpaceXformColorFilter.cpp",
+  "$_src/core/SkColorSpaceXformColorFilter.h",
   "$_src/core/SkColorSpaceXformSteps.cpp",
   "$_src/core/SkColorSpaceXformSteps.h",
+  "$_src/core/SkComposeColorFilter.cpp",
+  "$_src/core/SkComposeColorFilter.h",
   "$_src/core/SkCompressedDataUtils.cpp",
   "$_src/core/SkCompressedDataUtils.h",
   "$_src/core/SkContourMeasure.cpp",
@@ -420,6 +425,8 @@
   "$_src/core/SkMaskGamma.cpp",
   "$_src/core/SkMaskGamma.h",
   "$_src/core/SkMatrix.cpp",
+  "$_src/core/SkMatrixColorFilter.cpp",
+  "$_src/core/SkMatrixColorFilter.h",
   "$_src/core/SkMatrixInvert.cpp",
   "$_src/core/SkMatrixInvert.h",
   "$_src/core/SkMatrixPriv.h",
@@ -434,7 +441,6 @@
   "$_src/core/SkMipmapAccessor.h",
   "$_src/core/SkMipmapBuilder.cpp",
   "$_src/core/SkMipmapBuilder.h",
-  "$_src/core/SkModeColorFilter.cpp",
   "$_src/core/SkNextID.h",
   "$_src/core/SkOSFile.h",
   "$_src/core/SkOpts.cpp",
@@ -505,6 +511,8 @@
   "$_src/core/SkResourceCache.h",
   "$_src/core/SkRuntimeBlender.cpp",
   "$_src/core/SkRuntimeBlender.h",
+  "$_src/core/SkRuntimeColorFilter.cpp",
+  "$_src/core/SkRuntimeColorFilter.h",
   "$_src/core/SkRuntimeEffect.cpp",
   "$_src/core/SkRuntimeEffectPriv.h",
   "$_src/core/SkSLTypeShared.cpp",
@@ -579,6 +587,8 @@
   "$_src/core/SkVertState.h",
   "$_src/core/SkVertices.cpp",
   "$_src/core/SkVerticesPriv.h",
+  "$_src/core/SkWorkingFormatColorFilter.cpp",
+  "$_src/core/SkWorkingFormatColorFilter.h",
   "$_src/core/SkWriteBuffer.cpp",
   "$_src/core/SkWriteBuffer.h",
   "$_src/core/SkWritePixelsRec.cpp",
diff --git a/gn/effects.gni b/gn/effects.gni
index 3ce20a9..ebb2ba5 100644
--- a/gn/effects.gni
+++ b/gn/effects.gni
@@ -64,6 +64,7 @@
   "$_src/effects/SkShaderMaskFilterImpl.cpp",
   "$_src/effects/SkShaderMaskFilterImpl.h",
   "$_src/effects/SkTableColorFilter.cpp",
+  "$_src/effects/SkTableColorFilter.h",
   "$_src/effects/SkTableMaskFilter.cpp",
   "$_src/effects/SkTrimPE.h",
   "$_src/effects/SkTrimPathEffect.cpp",
diff --git a/gn/gpu.gni b/gn/gpu.gni
index fa2858d..6753645 100644
--- a/gn/gpu.gni
+++ b/gn/gpu.gni
@@ -364,6 +364,8 @@
   "$_src/gpu/ganesh/effects/GrBitmapTextGeoProc.h",
   "$_src/gpu/ganesh/effects/GrBlendFragmentProcessor.cpp",
   "$_src/gpu/ganesh/effects/GrBlendFragmentProcessor.h",
+  "$_src/gpu/ganesh/effects/GrColorTableEffect.cpp",
+  "$_src/gpu/ganesh/effects/GrColorTableEffect.h",
   "$_src/gpu/ganesh/effects/GrConvexPolyEffect.cpp",
   "$_src/gpu/ganesh/effects/GrConvexPolyEffect.h",
   "$_src/gpu/ganesh/effects/GrCoverageSetOpXP.cpp",
diff --git a/gn/utils.gni b/gn/utils.gni
index 5b14b81..5023ebc 100644
--- a/gn/utils.gni
+++ b/gn/utils.gni
@@ -73,6 +73,7 @@
   "$_src/utils/SkFloatToDecimal.h",
   "$_src/utils/SkFloatUtils.h",
   "$_src/utils/SkGaussianColorFilter.cpp",
+  "$_src/utils/SkGaussianColorFilter.h",
   "$_src/utils/SkJSON.cpp",
   "$_src/utils/SkJSON.h",
   "$_src/utils/SkJSONWriter.cpp",
diff --git a/include/core/SkColorFilter.h b/include/core/SkColorFilter.h
index 1e0f6ea..6e01a2f 100644
--- a/include/core/SkColorFilter.h
+++ b/include/core/SkColorFilter.h
@@ -8,12 +8,18 @@
 #ifndef SkColorFilter_DEFINED
 #define SkColorFilter_DEFINED
 
-#include "include/core/SkBlendMode.h"
 #include "include/core/SkColor.h"
 #include "include/core/SkFlattenable.h"
+#include "include/core/SkRefCnt.h"
+#include "include/private/base/SkAPI.h"
+
+#include <cstddef>
+#include <cstdint>
 
 class SkColorMatrix;
 class SkColorSpace;
+enum class SkBlendMode;
+struct SkDeserialProcs;
 
 /**
 *  ColorFilters are optional objects in the drawing pipeline. When present in
diff --git a/modules/sksg/include/SkSGColorFilter.h b/modules/sksg/include/SkSGColorFilter.h
index a0f2c51..c7befe9 100644
--- a/modules/sksg/include/SkSGColorFilter.h
+++ b/modules/sksg/include/SkSGColorFilter.h
@@ -65,7 +65,7 @@
 };
 
 /**
- * Concrete SkModeColorFilter Effect node.
+ * Concrete SkBlendModeColorFilter Effect node.
  */
 class ModeColorFilter final : public ColorFilter {
 public:
diff --git a/public.bzl b/public.bzl
index dea2449..636be01 100644
--- a/public.bzl
+++ b/public.bzl
@@ -357,11 +357,11 @@
     "src/core/SkATrace.h",
     "src/core/SkAdvancedTypefaceMetrics.h",
     "src/core/SkAlphaRuns.cpp",
+    "src/core/SkAlphaRuns.h",
     "src/core/SkAnalyticEdge.cpp",
     "src/core/SkAnalyticEdge.h",
     "src/core/SkAnnotation.cpp",
     "src/core/SkAnnotationKeys.h",
-    "src/core/SkAlphaRuns.h",
     "src/core/SkAutoBlitterChoose.h",
     "src/core/SkAutoPixmapStorage.cpp",
     "src/core/SkAutoPixmapStorage.h",
@@ -410,11 +410,14 @@
     "src/core/SkColorFilter.cpp",
     "src/core/SkColorFilterBase.h",
     "src/core/SkColorFilterPriv.h",
-    "src/core/SkColorFilter_Matrix.cpp",
     "src/core/SkColorSpace.cpp",
     "src/core/SkColorSpacePriv.h",
+    "src/core/SkColorSpaceXformColorFilter.cpp",
+    "src/core/SkColorSpaceXformColorFilter.h",
     "src/core/SkColorSpaceXformSteps.cpp",
     "src/core/SkColorSpaceXformSteps.h",
+    "src/core/SkComposeColorFilter.cpp",
+    "src/core/SkComposeColorFilter.h",
     "src/core/SkCompressedDataUtils.cpp",
     "src/core/SkCompressedDataUtils.h",
     "src/core/SkContourMeasure.cpp",
@@ -524,6 +527,8 @@
     "src/core/SkMaskGamma.cpp",
     "src/core/SkMaskGamma.h",
     "src/core/SkMatrix.cpp",
+    "src/core/SkMatrixColorFilter.cpp",
+    "src/core/SkMatrixColorFilter.h",
     "src/core/SkMatrixInvert.cpp",
     "src/core/SkMatrixInvert.h",
     "src/core/SkMatrixPriv.h",
@@ -538,7 +543,8 @@
     "src/core/SkMipmapAccessor.h",
     "src/core/SkMipmapBuilder.cpp",
     "src/core/SkMipmapBuilder.h",
-    "src/core/SkModeColorFilter.cpp",
+    "src/core/SkBlendModeColorFilter.cpp",
+    "src/core/SkBlendModeColorFilter.h",
     "src/core/SkNextID.h",
     "src/core/SkOSFile.h",
     "src/core/SkOpts.cpp",
@@ -624,6 +630,8 @@
     "src/core/SkResourceCache.h",
     "src/core/SkRuntimeBlender.cpp",
     "src/core/SkRuntimeBlender.h",
+    "src/core/SkRuntimeColorFilter.cpp",
+    "src/core/SkRuntimeColorFilter.h",
     "src/core/SkRuntimeEffect.cpp",
     "src/core/SkRuntimeEffectPriv.h",
     "src/core/SkSLTypeShared.cpp",
@@ -698,6 +706,8 @@
     "src/core/SkVertState.h",
     "src/core/SkVertices.cpp",
     "src/core/SkVerticesPriv.h",
+    "src/core/SkWorkingFormatColorFilter.cpp",
+    "src/core/SkWorkingFormatColorFilter.h",
     "src/core/SkWriteBuffer.cpp",
     "src/core/SkWriteBuffer.h",
     "src/core/SkWritePixelsRec.cpp",
@@ -736,6 +746,7 @@
     "src/effects/SkShaderMaskFilterImpl.cpp",
     "src/effects/SkShaderMaskFilterImpl.h",
     "src/effects/SkTableColorFilter.cpp",
+    "src/effects/SkTableColorFilter.h",
     "src/effects/SkTableMaskFilter.cpp",
     "src/effects/SkTrimPE.h",
     "src/effects/SkTrimPathEffect.cpp",
@@ -1041,6 +1052,8 @@
     "src/gpu/ganesh/effects/GrBitmapTextGeoProc.h",
     "src/gpu/ganesh/effects/GrBlendFragmentProcessor.cpp",
     "src/gpu/ganesh/effects/GrBlendFragmentProcessor.h",
+    "src/gpu/ganesh/effects/GrColorTableEffect.cpp",
+    "src/gpu/ganesh/effects/GrColorTableEffect.h",
     "src/gpu/ganesh/effects/GrConvexPolyEffect.cpp",
     "src/gpu/ganesh/effects/GrConvexPolyEffect.h",
     "src/gpu/ganesh/effects/GrCoverageSetOpXP.cpp",
@@ -1696,6 +1709,7 @@
     "src/utils/SkFloatToDecimal.h",
     "src/utils/SkFloatUtils.h",
     "src/utils/SkGaussianColorFilter.cpp",
+    "src/utils/SkGaussianColorFilter.h",
     "src/utils/SkJSON.cpp",
     "src/utils/SkJSON.h",
     "src/utils/SkJSONWriter.cpp",
diff --git a/src/core/BUILD.bazel b/src/core/BUILD.bazel
index 7b99be0..3e80d7d 100644
--- a/src/core/BUILD.bazel
+++ b/src/core/BUILD.bazel
@@ -86,11 +86,14 @@
     "SkColorFilter.cpp",
     "SkColorFilterBase.h",
     "SkColorFilterPriv.h",
-    "SkColorFilter_Matrix.cpp",
     "SkColorSpace.cpp",
     "SkColorSpacePriv.h",
+    "SkColorSpaceXformColorFilter.cpp",
+    "SkColorSpaceXformColorFilter.h",
     "SkColorSpaceXformSteps.cpp",
     "SkColorSpaceXformSteps.h",
+    "SkComposeColorFilter.cpp",
+    "SkComposeColorFilter.h",
     "SkCompressedDataUtils.cpp",
     "SkCompressedDataUtils.h",
     "SkContourMeasure.cpp",
@@ -194,6 +197,8 @@
     "SkMaskGamma.cpp",
     "SkMaskGamma.h",
     "SkMatrix.cpp",
+    "SkMatrixColorFilter.cpp",
+    "SkMatrixColorFilter.h",
     "SkMatrixPriv.h",
     "SkMatrixProvider.h",
     "SkMatrixUtils.h",
@@ -206,7 +211,8 @@
     "SkMipmapAccessor.h",
     "SkMipmapBuilder.cpp",
     "SkMipmapBuilder.h",
-    "SkModeColorFilter.cpp",
+    "SkBlendModeColorFilter.cpp",
+    "SkBlendModeColorFilter.h",
     "SkNextID.h",
     "SkOSFile.h",
     "SkOpts.cpp",
@@ -340,6 +346,8 @@
     "SkVertState.h",
     "SkVertices.cpp",
     "SkVerticesPriv.h",
+    "SkWorkingFormatColorFilter.cpp",
+    "SkWorkingFormatColorFilter.h",
     "SkWriteBuffer.cpp",
     "SkWriteBuffer.h",
     "SkWritePixelsRec.cpp",
@@ -370,6 +378,8 @@
     "SkRuntimeEffect.cpp",
     "SkRuntimeBlender.cpp",
     "SkRuntimeBlender.h",
+    "SkRuntimeColorFilter.cpp",
+    "SkRuntimeColorFilter.h",
     "SkSLTypeShared.cpp",
     "SkSLTypeShared.h",
 ]
diff --git a/src/core/SkBlendModeColorFilter.cpp b/src/core/SkBlendModeColorFilter.cpp
new file mode 100644
index 0000000..a7a2ddd
--- /dev/null
+++ b/src/core/SkBlendModeColorFilter.cpp
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2006 The Android Open Source Project
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/core/SkBlendModeColorFilter.h"
+
+#include "include/core/SkAlphaType.h"
+#include "include/core/SkBlendMode.h"
+#include "include/core/SkColorFilter.h"
+#include "include/core/SkColorSpace.h"
+#include "include/core/SkRefCnt.h"
+#include "include/private/SkColorData.h"
+#include "src/core/SkBlendModePriv.h"
+#include "src/core/SkColorFilterBase.h"
+#include "src/core/SkColorSpacePriv.h"
+#include "src/core/SkColorSpaceXformSteps.h"
+#include "src/core/SkEffectPriv.h"
+#include "src/core/SkPicturePriv.h"
+#include "src/core/SkRasterPipeline.h"
+#include "src/core/SkRasterPipelineOpList.h"
+#include "src/core/SkReadBuffer.h"
+#include "src/core/SkValidationUtils.h"
+#include "src/core/SkWriteBuffer.h"
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyContext.h"
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#endif
+
+template <SkAlphaType kDstAT = kPremul_SkAlphaType>
+static SkRGBA4f<kDstAT> map_color(const SkColor4f& c, SkColorSpace* src, SkColorSpace* dst) {
+    SkRGBA4f<kDstAT> color = {c.fR, c.fG, c.fB, c.fA};
+    SkColorSpaceXformSteps(src, kUnpremul_SkAlphaType, dst, kDstAT).apply(color.vec());
+    return color;
+}
+
+SkBlendModeColorFilter::SkBlendModeColorFilter(const SkColor4f& color, SkBlendMode mode)
+        : fColor(color), fMode(mode) {}
+
+bool SkBlendModeColorFilter::onAsAColorMode(SkColor* color, SkBlendMode* mode) const {
+    if (color) {
+        *color = fColor.toSkColor();
+    }
+    if (mode) {
+        *mode = fMode;
+    }
+    return true;
+}
+
+bool SkBlendModeColorFilter::onIsAlphaUnchanged() const {
+    switch (fMode) {
+        case SkBlendMode::kDst:      //!< [Da, Dc]
+        case SkBlendMode::kSrcATop:  //!< [Da, Sc * Da + (1 - Sa) * Dc]
+            return true;
+        default:
+            break;
+    }
+    return false;
+}
+
+void SkBlendModeColorFilter::flatten(SkWriteBuffer& buffer) const {
+    buffer.writeColor4f(fColor);
+    buffer.writeUInt((int)fMode);
+}
+
+sk_sp<SkFlattenable> SkBlendModeColorFilter::CreateProc(SkReadBuffer& buffer) {
+    if (buffer.isVersionLT(SkPicturePriv::kBlend4fColorFilter)) {
+        // Color is 8-bit, sRGB
+        SkColor color = buffer.readColor();
+        SkBlendMode mode = (SkBlendMode)buffer.readUInt();
+        return SkColorFilters::Blend(SkColor4f::FromColor(color), /*sRGB*/ nullptr, mode);
+    } else {
+        // Color is 32-bit, sRGB
+        SkColor4f color;
+        buffer.readColor4f(&color);
+        SkBlendMode mode = (SkBlendMode)buffer.readUInt();
+        return SkColorFilters::Blend(color, /*sRGB*/ nullptr, mode);
+    }
+}
+
+bool SkBlendModeColorFilter::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
+    rec.fPipeline->append(SkRasterPipelineOp::move_src_dst);
+    SkPMColor4f color = map_color(fColor, sk_srgb_singleton(), rec.fDstCS);
+    rec.fPipeline->append_constant_color(rec.fAlloc, color.vec());
+    SkBlendMode_AppendStages(fMode, rec.fPipeline);
+    return true;
+}
+
+#if defined(SK_ENABLE_SKVM)
+skvm::Color SkBlendModeColorFilter::onProgram(skvm::Builder* p,
+                                              skvm::Color c,
+                                              const SkColorInfo& dstInfo,
+                                              skvm::Uniforms* uniforms,
+                                              SkArenaAlloc*) const {
+    SkPMColor4f color = map_color(fColor, sk_srgb_singleton(), dstInfo.colorSpace());
+    // The blend program operates on this as if it were premul but the API takes an SkColor4f
+    skvm::Color dst = c, src = p->uniformColor({color.fR, color.fG, color.fB, color.fA}, uniforms);
+    return p->blend(fMode, src, dst);
+}
+#endif
+
+#if defined(SK_GRAPHITE)
+void SkBlendModeColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
+                                      skgpu::graphite::PaintParamsKeyBuilder* builder,
+                                      skgpu::graphite::PipelineDataGatherer* gatherer) const {
+    using namespace skgpu::graphite;
+
+    SkPMColor4f color =
+            map_color(fColor, sk_srgb_singleton(), keyContext.dstColorInfo().colorSpace());
+    AddColorBlendBlock(keyContext, builder, gatherer, fMode, color);
+}
+
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+
+sk_sp<SkColorFilter> SkColorFilters::Blend(const SkColor4f& color,
+                                           sk_sp<SkColorSpace> colorSpace,
+                                           SkBlendMode mode) {
+    if (!SkIsValidMode(mode)) {
+        return nullptr;
+    }
+
+    // First map to sRGB to simplify storage in the actual SkColorFilter instance, staying unpremul
+    // until the final dst color space is known when actually filtering.
+    SkColor4f srgb = map_color<kUnpremul_SkAlphaType>(color, colorSpace.get(), sk_srgb_singleton());
+
+    // Next collapse some modes if possible
+    float alpha = srgb.fA;
+    if (SkBlendMode::kClear == mode) {
+        srgb = SkColors::kTransparent;
+        mode = SkBlendMode::kSrc;
+    } else if (SkBlendMode::kSrcOver == mode) {
+        if (0.f == alpha) {
+            mode = SkBlendMode::kDst;
+        } else if (1.f == alpha) {
+            mode = SkBlendMode::kSrc;
+        }
+        // else just stay srcover
+    }
+
+    // Finally weed out combinations that are noops, and just return null
+    if (SkBlendMode::kDst == mode ||
+        (0.f == alpha && (SkBlendMode::kSrcOver == mode ||
+                          SkBlendMode::kDstOver == mode ||
+                          SkBlendMode::kDstOut == mode ||
+                          SkBlendMode::kSrcATop == mode ||
+                          SkBlendMode::kXor == mode ||
+                          SkBlendMode::kDarken == mode)) ||
+            (1.f == alpha && SkBlendMode::kDstIn == mode)) {
+        return nullptr;
+    }
+
+    return sk_sp<SkColorFilter>(new SkBlendModeColorFilter(srgb, mode));
+}
+
+sk_sp<SkColorFilter> SkColorFilters::Blend(SkColor color, SkBlendMode mode) {
+    return Blend(SkColor4f::FromColor(color), /*sRGB*/ nullptr, mode);
+}
+
+void SkRegisterModeColorFilterFlattenable() {
+    SK_REGISTER_FLATTENABLE(SkBlendModeColorFilter);
+    // Previous name
+    SkFlattenable::Register("SkModeColorFilter", SkBlendModeColorFilter::CreateProc);
+}
diff --git a/src/core/SkBlendModeColorFilter.h b/src/core/SkBlendModeColorFilter.h
new file mode 100644
index 0000000..1097777
--- /dev/null
+++ b/src/core/SkBlendModeColorFilter.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef SkBlendModeColorFilter_DEFINED
+#define SkBlendModeColorFilter_DEFINED
+
+#include "include/core/SkColor.h"
+#include "include/core/SkFlattenable.h"
+#include "src/core/SkColorFilterBase.h"
+
+class SkReadBuffer;
+class SkWriteBuffer;
+enum class SkBlendMode;
+struct SkStageRec;
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyContext.h"
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#endif
+
+class SkBlendModeColorFilter final : public SkColorFilterBase {
+public:
+    SkBlendModeColorFilter(const SkColor4f& color, SkBlendMode mode);
+
+    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
+
+    bool onIsAlphaUnchanged() const override;
+
+#if defined(SK_GRAPHITE)
+    void addToKey(const skgpu::graphite::KeyContext&,
+                  skgpu::graphite::PaintParamsKeyBuilder*,
+                  skgpu::graphite::PipelineDataGatherer*) const override;
+#endif
+    SkColorFilterBase::Type type() const override { return SkColorFilterBase::Type::kBlendMode; }
+
+    SkColor4f color() const { return fColor; }
+    SkBlendMode mode() const { return fMode; }
+
+private:
+    friend void ::SkRegisterModeColorFilterFlattenable();
+    SK_FLATTENABLE_HOOKS(SkBlendModeColorFilter)
+
+    void flatten(SkWriteBuffer&) const override;
+    bool onAsAColorMode(SkColor*, SkBlendMode*) const override;
+
+#if defined(SK_ENABLE_SKVM)
+    skvm::Color onProgram(skvm::Builder*,
+                          skvm::Color,
+                          const SkColorInfo&,
+                          skvm::Uniforms*,
+                          SkArenaAlloc*) const override;
+#endif
+
+    SkColor4f fColor;  // always stored in sRGB
+    SkBlendMode fMode;
+};
+
+#endif
diff --git a/src/core/SkColorFilter.cpp b/src/core/SkColorFilter.cpp
index 576bf38..c757474 100644
--- a/src/core/SkColorFilter.cpp
+++ b/src/core/SkColorFilter.cpp
@@ -4,31 +4,36 @@
  * Use of this source code is governed by a BSD-style license that can be
  * found in the LICENSE file.
  */
+#include "include/core/SkColorFilter.h"
 
+#include "include/core/SkAlphaType.h"
+#include "include/core/SkColor.h"
+#include "include/core/SkColorSpace.h"  // IWYU pragma: keep
+#include "include/core/SkColorType.h"
+#include "include/core/SkData.h"
+#include "include/core/SkFlattenable.h"
+#include "include/core/SkMatrix.h"
 #include "include/core/SkRefCnt.h"
-#include "include/core/SkString.h"
-#include "include/core/SkUnPreMultiply.h"
+#include "include/core/SkScalar.h"
+#include "include/core/SkSurfaceProps.h"
 #include "include/effects/SkRuntimeEffect.h"
-#include "include/private/base/SkTDArray.h"
-#include "modules/skcms/skcms.h"
+#include "include/private/SkColorData.h"
+#include "include/private/base/SkAssert.h"
 #include "src/base/SkArenaAlloc.h"
-#include "src/base/SkNoDestructor.h"
 #include "src/core/SkColorFilterBase.h"
-#include "src/core/SkColorFilterPriv.h"
-#include "src/core/SkColorSpacePriv.h"
 #include "src/core/SkColorSpaceXformSteps.h"
+#include "src/core/SkEffectPriv.h"
 #include "src/core/SkMatrixProvider.h"
 #include "src/core/SkRasterPipeline.h"
-#include "src/core/SkReadBuffer.h"
+#include "src/core/SkRasterPipelineOpContexts.h"
+#include "src/core/SkRasterPipelineOpList.h"
 #include "src/core/SkRuntimeEffectPriv.h"
-#include "src/core/SkVM.h"
-#include "src/core/SkWriteBuffer.h"
 
-#if defined(SK_GANESH)
-#include "src/gpu/ganesh/GrColorInfo.h"
-#include "src/gpu/ganesh/GrColorSpaceXform.h"
-#include "src/gpu/ganesh/GrFragmentProcessor.h"
-#endif
+#include <cstddef>
+#include <iterator>
+
+enum class SkBlendMode;
+struct SkDeserialProcs;
 
 #if defined(SK_GRAPHITE)
 #include "src/gpu/graphite/KeyContext.h"
@@ -65,15 +70,6 @@
     return false;
 }
 
-#if defined(SK_GANESH)
-GrFPResult SkColorFilterBase::asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                                  GrRecordingContext* context,
-                                                  const GrColorInfo& dstColorInfo,
-                                                  const SkSurfaceProps& props) const {
-    // This color filter doesn't implement `asFragmentProcessor`.
-    return GrFPFailure(std::move(inputFP));
-}
-#endif
 #if defined(SK_ENABLE_SKVM)
 skvm::Color SkColorFilterBase::program(skvm::Builder* p, skvm::Color c,
                                        const SkColorInfo& dst,
@@ -159,430 +155,6 @@
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class SkComposeColorFilter final : public SkColorFilterBase {
-public:
-    bool onIsAlphaUnchanged() const override {
-        // Can only claim alphaunchanged support if both our proxys do.
-        return fOuter->isAlphaUnchanged() && fInner->isAlphaUnchanged();
-    }
-
-    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override {
-        bool innerIsOpaque = shaderIsOpaque;
-        if (!fInner->isAlphaUnchanged()) {
-            innerIsOpaque = false;
-        }
-        return fInner->appendStages(rec, shaderIsOpaque) &&
-               fOuter->appendStages(rec, innerIsOpaque);
-    }
-
-#if defined(SK_ENABLE_SKVM)
-    skvm::Color onProgram(skvm::Builder* p, skvm::Color c,
-                          const SkColorInfo& dst,
-                          skvm::Uniforms* uniforms, SkArenaAlloc* alloc) const override {
-               c = fInner->program(p, c, dst, uniforms, alloc);
-        return c ? fOuter->program(p, c, dst, uniforms, alloc) : skvm::Color{};
-    }
-#endif
-#if defined(SK_GANESH)
-    GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrRecordingContext* context,
-                                   const GrColorInfo& dstColorInfo,
-                                   const SkSurfaceProps& props) const override {
-        // Unfortunately, we need to clone the input before we know we need it. This lets us return
-        // the original FP if either internal color filter fails.
-        auto inputClone = inputFP ? inputFP->clone() : nullptr;
-
-        auto [innerSuccess, innerFP] =
-                fInner->asFragmentProcessor(std::move(inputFP), context, dstColorInfo, props);
-        if (!innerSuccess) {
-            return GrFPFailure(std::move(inputClone));
-        }
-
-        auto [outerSuccess, outerFP] =
-                fOuter->asFragmentProcessor(std::move(innerFP), context, dstColorInfo, props);
-        if (!outerSuccess) {
-            return GrFPFailure(std::move(inputClone));
-        }
-
-        return GrFPSuccess(std::move(outerFP));
-    }
-#endif
-
-#if defined(SK_GRAPHITE)
-    void addToKey(const skgpu::graphite::KeyContext& keyContext,
-                  skgpu::graphite::PaintParamsKeyBuilder* builder,
-                  skgpu::graphite::PipelineDataGatherer* gatherer) const override {
-        using namespace skgpu::graphite;
-
-        ComposeColorFilterBlock::BeginBlock(keyContext, builder, gatherer);
-
-        as_CFB(fInner)->addToKey(keyContext, builder, gatherer);
-        as_CFB(fOuter)->addToKey(keyContext, builder, gatherer);
-
-        builder->endBlock();
-    }
-#endif // SK_GRAPHITE
-
-protected:
-    void flatten(SkWriteBuffer& buffer) const override {
-        buffer.writeFlattenable(fOuter.get());
-        buffer.writeFlattenable(fInner.get());
-    }
-
-private:
-    friend void ::SkRegisterComposeColorFilterFlattenable();
-    SK_FLATTENABLE_HOOKS(SkComposeColorFilter)
-
-    SkComposeColorFilter(sk_sp<SkColorFilter> outer, sk_sp<SkColorFilter> inner)
-        : fOuter(as_CFB_sp(std::move(outer)))
-        , fInner(as_CFB_sp(std::move(inner)))
-    {}
-
-    sk_sp<SkColorFilterBase> fOuter;
-    sk_sp<SkColorFilterBase> fInner;
-
-    friend class SkColorFilter;
-
-    using INHERITED = SkColorFilter;
-};
-
-sk_sp<SkFlattenable> SkComposeColorFilter::CreateProc(SkReadBuffer& buffer) {
-    sk_sp<SkColorFilter> outer(buffer.readColorFilter());
-    sk_sp<SkColorFilter> inner(buffer.readColorFilter());
-    return outer ? outer->makeComposed(std::move(inner)) : inner;
-}
-
-sk_sp<SkColorFilter> SkColorFilter::makeComposed(sk_sp<SkColorFilter> inner) const {
-    if (!inner) {
-        return sk_ref_sp(this);
-    }
-
-    return sk_sp<SkColorFilter>(new SkComposeColorFilter(sk_ref_sp(this), std::move(inner)));
-}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class ColorSpaceXformColorFilter final : public SkColorFilterBase {
-public:
-    ColorSpaceXformColorFilter(sk_sp<SkColorSpace> src, sk_sp<SkColorSpace> dst)
-            : fSrc(std::move(src))
-            , fDst(std::move(dst))
-            , fSteps(
-                      // We handle premul/unpremul separately, so here just always upm->upm.
-                      fSrc.get(),
-                      kUnpremul_SkAlphaType,
-                      fDst.get(),
-                      kUnpremul_SkAlphaType)
-
-    {}
-
-#if defined(SK_GANESH)
-    GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrRecordingContext* context,
-                                   const GrColorInfo& dstColorInfo,
-                                   const SkSurfaceProps& props) const override {
-        // wish our caller would let us know if our input was opaque...
-        constexpr SkAlphaType alphaType = kPremul_SkAlphaType;
-        return GrFPSuccess(GrColorSpaceXformEffect::Make(
-                std::move(inputFP), fSrc.get(), alphaType, fDst.get(), alphaType));
-        SkUNREACHABLE;
-    }
-#endif
-
-#if defined(SK_GRAPHITE)
-    void addToKey(const skgpu::graphite::KeyContext& keyContext,
-                  skgpu::graphite::PaintParamsKeyBuilder* builder,
-                  skgpu::graphite::PipelineDataGatherer* gatherer) const override {
-        using namespace skgpu::graphite;
-
-        constexpr SkAlphaType alphaType = kPremul_SkAlphaType;
-        ColorSpaceTransformBlock::ColorSpaceTransformData data(
-                fSrc.get(), alphaType, fDst.get(), alphaType);
-        ColorSpaceTransformBlock::BeginBlock(keyContext, builder, gatherer, &data);
-        builder->endBlock();
-    }
-#endif
-
-    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override {
-        if (!shaderIsOpaque) {
-            rec.fPipeline->append(SkRasterPipelineOp::unpremul);
-        }
-
-        fSteps.apply(rec.fPipeline);
-
-        if (!shaderIsOpaque) {
-            rec.fPipeline->append(SkRasterPipelineOp::premul);
-        }
-        return true;
-    }
-
-#if defined(SK_ENABLE_SKVM)
-    skvm::Color onProgram(skvm::Builder* p, skvm::Color c, const SkColorInfo& dst,
-                          skvm::Uniforms* uniforms, SkArenaAlloc* alloc) const override {
-        return premul(fSteps.program(p, uniforms, unpremul(c)));
-    }
-#endif
-
-protected:
-    void flatten(SkWriteBuffer& buffer) const override {
-        buffer.writeDataAsByteArray(fSrc->serialize().get());
-        buffer.writeDataAsByteArray(fDst->serialize().get());
-    }
-
-private:
-    friend void ::SkRegisterColorSpaceXformColorFilterFlattenable();
-    SK_FLATTENABLE_HOOKS(ColorSpaceXformColorFilter)
-    static sk_sp<SkFlattenable> LegacyGammaOnlyCreateProc(SkReadBuffer& buffer);
-
-    const sk_sp<SkColorSpace> fSrc;
-    const sk_sp<SkColorSpace> fDst;
-    SkColorSpaceXformSteps fSteps;
-
-    friend class SkColorFilter;
-    using INHERITED = SkColorFilterBase;
-};
-
-sk_sp<SkFlattenable> ColorSpaceXformColorFilter::LegacyGammaOnlyCreateProc(SkReadBuffer& buffer) {
-    uint32_t dir = buffer.read32();
-    if (!buffer.validate(dir <= 1)) {
-        return nullptr;
-    }
-    if (dir == 0) {
-      return SkColorFilters::LinearToSRGBGamma();
-    }
-	return SkColorFilters::SRGBToLinearGamma();
-}
-
-sk_sp<SkFlattenable> ColorSpaceXformColorFilter::CreateProc(SkReadBuffer& buffer) {
-    sk_sp<SkColorSpace> colorSpaces[2];
-    for (int i = 0; i < 2; ++i) {
-        auto data = buffer.readByteArrayAsData();
-        if (!buffer.validate(data != nullptr)) {
-            return nullptr;
-        }
-        colorSpaces[i] = SkColorSpace::Deserialize(data->data(), data->size());
-        if (!buffer.validate(colorSpaces[i] != nullptr)) {
-            return nullptr;
-        }
-    }
-    return sk_sp<SkFlattenable>(
-            new ColorSpaceXformColorFilter(std::move(colorSpaces[0]), std::move(colorSpaces[1])));
-}
-
-sk_sp<SkColorFilter> SkColorFilters::LinearToSRGBGamma() {
-    static SkNoDestructor<ColorSpaceXformColorFilter> gSingleton(SkColorSpace::MakeSRGBLinear(),
-                                                                 SkColorSpace::MakeSRGB());
-    return sk_ref_sp(gSingleton.get());
-}
-
-sk_sp<SkColorFilter> SkColorFilters::SRGBToLinearGamma() {
-    static SkNoDestructor<ColorSpaceXformColorFilter> gSingleton(SkColorSpace::MakeSRGB(),
-                                                                 SkColorSpace::MakeSRGBLinear());
-    return sk_ref_sp(gSingleton.get());
-}
-
-sk_sp<SkColorFilter> SkColorFilterPriv::MakeColorSpaceXform(sk_sp<SkColorSpace> src,
-                                                            sk_sp<SkColorSpace> dst) {
-    return sk_make_sp<ColorSpaceXformColorFilter>(std::move(src), std::move(dst));
-}
-
-class SkWorkingFormatColorFilter final : public SkColorFilterBase {
-public:
-    SkWorkingFormatColorFilter(sk_sp<SkColorFilter>          child,
-                               const skcms_TransferFunction* tf,
-                               const skcms_Matrix3x3*        gamut,
-                               const SkAlphaType*            at) {
-        fChild = std::move(child);
-        if (tf)    { fTF    = *tf;    fUseDstTF    = false; }
-        if (gamut) { fGamut = *gamut; fUseDstGamut = false; }
-        if (at)    { fAT    = *at;    fUseDstAT    = false; }
-    }
-
-    sk_sp<SkColorSpace> workingFormat(const sk_sp<SkColorSpace>& dstCS, SkAlphaType* at) const {
-        skcms_TransferFunction tf    = fTF;
-        skcms_Matrix3x3        gamut = fGamut;
-
-        if (fUseDstTF   ) { SkAssertResult(dstCS->isNumericalTransferFn(&tf)); }
-        if (fUseDstGamut) { SkAssertResult(dstCS->toXYZD50             (&gamut)); }
-
-        *at = fUseDstAT ? kPremul_SkAlphaType : fAT;
-        return SkColorSpace::MakeRGB(tf, gamut);
-    }
-
-#if defined(SK_GANESH)
-    GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrRecordingContext* context,
-                                   const GrColorInfo& dstColorInfo,
-                                   const SkSurfaceProps& props) const override {
-        sk_sp<SkColorSpace> dstCS = dstColorInfo.refColorSpace();
-        if (!dstCS) { dstCS = SkColorSpace::MakeSRGB(); }
-
-        SkAlphaType workingAT;
-        sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
-
-        GrColorInfo dst = {dstColorInfo.colorType(), dstColorInfo.alphaType(), dstCS},
-                working = {dstColorInfo.colorType(), workingAT, workingCS};
-
-        auto [ok, fp] = as_CFB(fChild)->asFragmentProcessor(
-                GrColorSpaceXformEffect::Make(std::move(inputFP), dst,working), context, working,
-                                              props);
-
-        return ok ? GrFPSuccess(GrColorSpaceXformEffect::Make(std::move(fp), working,dst))
-                  : GrFPFailure(std::move(fp));
-    }
-#endif
-
-#if defined(SK_GRAPHITE)
-    void addToKey(const skgpu::graphite::KeyContext& keyContext,
-                  skgpu::graphite::PaintParamsKeyBuilder* builder,
-                  skgpu::graphite::PipelineDataGatherer* gatherer) const override {
-        using namespace skgpu::graphite;
-
-        const SkAlphaType dstAT = keyContext.dstColorInfo().alphaType();
-        sk_sp<SkColorSpace> dstCS = keyContext.dstColorInfo().refColorSpace();
-        if (!dstCS) {
-            dstCS = SkColorSpace::MakeSRGB();
-        }
-
-        SkAlphaType workingAT;
-        sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
-
-        ColorSpaceTransformBlock::ColorSpaceTransformData data1(
-                dstCS.get(), dstAT, workingCS.get(), workingAT);
-        ColorSpaceTransformBlock::BeginBlock(keyContext, builder, gatherer, &data1);
-        builder->endBlock();
-
-        as_CFB(fChild)->addToKey(keyContext, builder, gatherer);
-
-        ColorSpaceTransformBlock::ColorSpaceTransformData data2(
-                workingCS.get(), workingAT, dstCS.get(), dstAT);
-        ColorSpaceTransformBlock::BeginBlock(keyContext, builder, gatherer, &data2);
-        builder->endBlock();
-    }
-#endif
-
-    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override {
-        sk_sp<SkColorSpace> dstCS = sk_ref_sp(rec.fDstCS);
-
-        if (!dstCS) { dstCS = SkColorSpace::MakeSRGB(); }
-
-        SkAlphaType workingAT;
-        sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
-
-        SkColorInfo dst = {rec.fDstColorType, kPremul_SkAlphaType, dstCS},
-                working = {rec.fDstColorType, workingAT, workingCS};
-
-        const auto* dstToWorking = rec.fAlloc->make<SkColorSpaceXformSteps>(dst, working);
-        const auto* workingToDst = rec.fAlloc->make<SkColorSpaceXformSteps>(working, dst);
-
-        // Any SkSL effects might reference the paint color, which is already in the destination
-        // color space. We need to transform it to the working space for consistency.
-        SkColor4f paintColorInWorkingSpace = rec.fPaintColor;
-        dstToWorking->apply(paintColorInWorkingSpace.vec());
-
-        SkStageRec workingRec = {rec.fPipeline,
-                                 rec.fAlloc,
-                                 rec.fDstColorType,
-                                 workingCS.get(),
-                                 paintColorInWorkingSpace,
-                                 rec.fSurfaceProps};
-
-        dstToWorking->apply(rec.fPipeline);
-        if (!as_CFB(fChild)->appendStages(workingRec, shaderIsOpaque)) {
-            return false;
-        }
-        workingToDst->apply(rec.fPipeline);
-        return true;
-    }
-
-#if defined(SK_ENABLE_SKVM)
-    skvm::Color onProgram(skvm::Builder* p, skvm::Color c, const SkColorInfo& rawDst,
-                          skvm::Uniforms* uniforms, SkArenaAlloc* alloc) const override {
-        sk_sp<SkColorSpace> dstCS = rawDst.refColorSpace();
-        if (!dstCS) { dstCS = SkColorSpace::MakeSRGB(); }
-
-        SkAlphaType workingAT;
-        sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
-
-        SkColorInfo dst = {rawDst.colorType(), kPremul_SkAlphaType, dstCS},
-                working = {rawDst.colorType(), workingAT, workingCS};
-
-        c = SkColorSpaceXformSteps{dst,working}.program(p, uniforms, c);
-        c = as_CFB(fChild)->program(p, c, working, uniforms, alloc);
-        return c ? SkColorSpaceXformSteps{working,dst}.program(p, uniforms, c)
-                 : c;
-    }
-#endif
-
-    SkPMColor4f onFilterColor4f(const SkPMColor4f& origColor,
-                                SkColorSpace* rawDstCS) const override {
-        sk_sp<SkColorSpace> dstCS = sk_ref_sp(rawDstCS);
-        if (!dstCS) { dstCS = SkColorSpace::MakeSRGB(); }
-
-        SkAlphaType workingAT;
-        sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
-
-        SkColorInfo dst = {kUnknown_SkColorType, kPremul_SkAlphaType, dstCS},
-                working = {kUnknown_SkColorType, workingAT, workingCS};
-
-        SkPMColor4f color = origColor;
-        SkColorSpaceXformSteps{dst,working}.apply(color.vec());
-        color = as_CFB(fChild)->onFilterColor4f(color, working.colorSpace());
-        SkColorSpaceXformSteps{working,dst}.apply(color.vec());
-        return color;
-    }
-
-    bool onIsAlphaUnchanged() const override { return fChild->isAlphaUnchanged(); }
-
-private:
-    friend void ::SkRegisterWorkingFormatColorFilterFlattenable();
-    SK_FLATTENABLE_HOOKS(SkWorkingFormatColorFilter)
-
-    void flatten(SkWriteBuffer& buffer) const override {
-        buffer.writeFlattenable(fChild.get());
-        buffer.writeBool(fUseDstTF);
-        buffer.writeBool(fUseDstGamut);
-        buffer.writeBool(fUseDstAT);
-        if (!fUseDstTF)    { buffer.writeScalarArray(&fTF.g, 7); }
-        if (!fUseDstGamut) { buffer.writeScalarArray(&fGamut.vals[0][0], 9); }
-        if (!fUseDstAT)    { buffer.writeInt(fAT); }
-    }
-
-    sk_sp<SkColorFilter>   fChild;
-    skcms_TransferFunction fTF;     bool fUseDstTF    = true;
-    skcms_Matrix3x3        fGamut;  bool fUseDstGamut = true;
-    SkAlphaType            fAT;     bool fUseDstAT    = true;
-};
-
-sk_sp<SkFlattenable> SkWorkingFormatColorFilter::CreateProc(SkReadBuffer& buffer) {
-    sk_sp<SkColorFilter> child = buffer.readColorFilter();
-    bool useDstTF    = buffer.readBool(),
-         useDstGamut = buffer.readBool(),
-         useDstAT    = buffer.readBool();
-
-    skcms_TransferFunction tf;
-    skcms_Matrix3x3        gamut;
-    SkAlphaType            at;
-
-    if (!useDstTF)    { buffer.readScalarArray(&tf.g, 7); }
-    if (!useDstGamut) { buffer.readScalarArray(&gamut.vals[0][0], 9); }
-    if (!useDstAT)    { at = buffer.read32LE(kLastEnum_SkAlphaType); }
-
-    return SkColorFilterPriv::WithWorkingFormat(std::move(child),
-                                                useDstTF    ? nullptr : &tf,
-                                                useDstGamut ? nullptr : &gamut,
-                                                useDstAT    ? nullptr : &at);
-}
-
-sk_sp<SkColorFilter> SkColorFilterPriv::WithWorkingFormat(sk_sp<SkColorFilter> child,
-                                                          const skcms_TransferFunction* tf,
-                                                          const skcms_Matrix3x3* gamut,
-                                                          const SkAlphaType* at) {
-    return sk_make_sp<SkWorkingFormatColorFilter>(std::move(child), tf, gamut, at);
-}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
 sk_sp<SkColorFilter> SkColorFilters::Lerp(float weight, sk_sp<SkColorFilter> cf0,
                                                         sk_sp<SkColorFilter> cf1) {
 #ifdef SK_ENABLE_SKSL
@@ -604,15 +176,15 @@
         return cf1;
     }
 
-    static const SkRuntimeEffect* effect = SkMakeCachedRuntimeEffect(
-        SkRuntimeEffect::MakeForColorFilter,
-        "uniform colorFilter cf0;"
-        "uniform colorFilter cf1;"
-        "uniform half   weight;"
-        "half4 main(half4 color) {"
-            "return mix(cf0.eval(color), cf1.eval(color), weight);"
-        "}"
-    ).release();
+    static const SkRuntimeEffect* effect =
+            SkMakeCachedRuntimeEffect(SkRuntimeEffect::MakeForColorFilter,
+                                      "uniform colorFilter cf0;"
+                                      "uniform colorFilter cf1;"
+                                      "uniform half weight;"
+                                      "half4 main(half4 color) {"
+                                      "return mix(cf0.eval(color), cf1.eval(color), weight);"
+                                      "}")
+                    .release();
     SkASSERT(effect);
 
     sk_sp<SkColorFilter> inputs[] = {cf0,cf1};
@@ -624,19 +196,3 @@
 #endif
 }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-void SkRegisterComposeColorFilterFlattenable() {
-    SK_REGISTER_FLATTENABLE(SkComposeColorFilter);
-}
-
-void SkRegisterColorSpaceXformColorFilterFlattenable() {
-    SK_REGISTER_FLATTENABLE(ColorSpaceXformColorFilter);
-    // TODO(ccameron): Remove after grace period for SKPs to stop using old serialization.
-    SkFlattenable::Register("SkSRGBGammaColorFilter",
-                            ColorSpaceXformColorFilter::LegacyGammaOnlyCreateProc);
-}
-
-void SkRegisterWorkingFormatColorFilterFlattenable() {
-    SK_REGISTER_FLATTENABLE(SkWorkingFormatColorFilter);
-}
diff --git a/src/core/SkColorFilterBase.h b/src/core/SkColorFilterBase.h
index a7eaae8..d7f0780 100644
--- a/src/core/SkColorFilterBase.h
+++ b/src/core/SkColorFilterBase.h
@@ -33,6 +33,16 @@
 class PipelineDataGatherer;
 }
 
+#define SK_ALL_COLOR_FILTERS(M) \
+    M(BlendMode)                \
+    M(ColorSpaceXform)          \
+    M(Compose)                  \
+    M(Gaussian)                 \
+    M(Matrix)                   \
+    M(Runtime)                  \
+    M(Table)                    \
+    M(WorkingFormat)
+
 class SkColorFilterBase : public SkColorFilter {
 public:
     SK_WARN_UNUSED_RESULT
@@ -48,21 +58,16 @@
     */
     virtual bool onIsAlphaUnchanged() const { return false; }
 
-#if defined(SK_GANESH)
-    /**
-     *  A subclass may implement this factory function to work with the GPU backend. It returns
-     *  a GrFragmentProcessor that implements the color filter in GPU shader code.
-     *
-     *  The fragment processor receives a input FP that generates a premultiplied input color, and
-     *  produces a premultiplied output color.
-     *
-     *  A GrFPFailure indicates that the color filter isn't implemented for the GPU backend.
-     */
-    virtual GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                           GrRecordingContext* context,
-                                           const GrColorInfo& dstColorInfo,
-                                           const SkSurfaceProps& props) const;
-#endif
+    enum class Type {
+        // Used for stubs/tests
+        kNoop,
+#define M(type) k##type,
+        SK_ALL_COLOR_FILTERS(M)
+#undef M
+
+    };
+
+    virtual Type type() const = 0;
 
     bool affectsTransparentBlack() const {
         return this->filterColor(SK_ColorTRANSPARENT) != SK_ColorTRANSPARENT;
@@ -134,11 +139,10 @@
     return sk_sp<SkColorFilterBase>(static_cast<SkColorFilterBase*>(filter.release()));
 }
 
-
 void SkRegisterComposeColorFilterFlattenable();
 void SkRegisterMatrixColorFilterFlattenable();
 void SkRegisterModeColorFilterFlattenable();
-void SkRegisterColorSpaceXformColorFilterFlattenable();
+void SkRegisterSkColorSpaceXformColorFilterFlattenable();
 void SkRegisterTableColorFilterFlattenable();
 void SkRegisterWorkingFormatColorFilterFlattenable();
 
diff --git a/src/core/SkColorFilter_Matrix.cpp b/src/core/SkColorFilter_Matrix.cpp
deleted file mode 100644
index b71c422..0000000
--- a/src/core/SkColorFilter_Matrix.cpp
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * Copyright 2011 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "include/core/SkRefCnt.h"
-#include "include/core/SkUnPreMultiply.h"
-#include "include/effects/SkColorMatrix.h"
-#include "include/effects/SkRuntimeEffect.h"
-#include "include/private/SkColorData.h"
-#include "src/core/SkColorFilterBase.h"
-#include "src/core/SkColorSpacePriv.h"
-#include "src/core/SkRasterPipeline.h"
-#include "src/core/SkReadBuffer.h"
-#include "src/core/SkRuntimeEffectPriv.h"
-#include "src/core/SkVM.h"
-#include "src/core/SkWriteBuffer.h"
-
-#if defined(SK_GRAPHITE)
-#include "src/gpu/graphite/KeyHelpers.h"
-#include "src/gpu/graphite/PaintParamsKey.h"
-#endif // SK_GRAPHITE
-
-static bool is_alpha_unchanged(const float matrix[20]) {
-    const float* srcA = matrix + 15;
-
-    return SkScalarNearlyZero (srcA[0])
-        && SkScalarNearlyZero (srcA[1])
-        && SkScalarNearlyZero (srcA[2])
-        && SkScalarNearlyEqual(srcA[3], 1)
-        && SkScalarNearlyZero (srcA[4]);
-}
-
-class SkColorFilter_Matrix final : public SkColorFilterBase {
-public:
-    enum class Domain : uint8_t { kRGBA, kHSLA };
-
-    explicit SkColorFilter_Matrix(const float array[20], Domain);
-
-    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
-
-    bool onIsAlphaUnchanged() const override { return fAlphaIsUnchanged; }
-
-#if defined(SK_GANESH)
-    GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrRecordingContext*,
-                                   const GrColorInfo&,
-                                   const SkSurfaceProps&) const override;
-#endif
-#if defined(SK_GRAPHITE)
-    void addToKey(const skgpu::graphite::KeyContext&,
-                  skgpu::graphite::PaintParamsKeyBuilder*,
-                  skgpu::graphite::PipelineDataGatherer*) const override;
-#endif
-
-private:
-    friend void ::SkRegisterMatrixColorFilterFlattenable();
-    SK_FLATTENABLE_HOOKS(SkColorFilter_Matrix)
-
-    void flatten(SkWriteBuffer&) const override;
-    bool onAsAColorMatrix(float matrix[20]) const override;
-
-#if defined(SK_ENABLE_SKVM)
-    skvm::Color onProgram(skvm::Builder*, skvm::Color,
-                          const SkColorInfo& dst,
-                          skvm::Uniforms* uniforms, SkArenaAlloc*) const override;
-#endif
-
-    float  fMatrix[20];
-    bool   fAlphaIsUnchanged;
-    Domain fDomain;
-};
-
-SkColorFilter_Matrix::SkColorFilter_Matrix(const float array[20], Domain domain)
-        : fAlphaIsUnchanged(is_alpha_unchanged(array))
-        , fDomain(domain) {
-    memcpy(fMatrix, array, 20 * sizeof(float));
-}
-
-void SkColorFilter_Matrix::flatten(SkWriteBuffer& buffer) const {
-    SkASSERT(sizeof(fMatrix)/sizeof(float) == 20);
-    buffer.writeScalarArray(fMatrix, 20);
-
-    // RGBA flag
-    buffer.writeBool(fDomain == Domain::kRGBA);
-}
-
-sk_sp<SkFlattenable> SkColorFilter_Matrix::CreateProc(SkReadBuffer& buffer) {
-    float matrix[20];
-    if (!buffer.readScalarArray(matrix, 20)) {
-        return nullptr;
-    }
-
-    auto   is_rgba = buffer.readBool();
-    return is_rgba ? SkColorFilters::Matrix(matrix)
-                   : SkColorFilters::HSLAMatrix(matrix);
-}
-
-bool SkColorFilter_Matrix::onAsAColorMatrix(float matrix[20]) const {
-    if (matrix) {
-        memcpy(matrix, fMatrix, 20 * sizeof(float));
-    }
-    return true;
-}
-
-bool SkColorFilter_Matrix::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
-    const bool willStayOpaque = shaderIsOpaque && fAlphaIsUnchanged,
-                         hsla = fDomain == Domain::kHSLA;
-
-    SkRasterPipeline* p = rec.fPipeline;
-    if (!shaderIsOpaque) { p->append(SkRasterPipelineOp::unpremul); }
-    if (           hsla) { p->append(SkRasterPipelineOp::rgb_to_hsl); }
-    if (           true) { p->append(SkRasterPipelineOp::matrix_4x5, fMatrix); }
-    if (           hsla) { p->append(SkRasterPipelineOp::hsl_to_rgb); }
-    if (           true) { p->append(SkRasterPipelineOp::clamp_01); }
-    if (!willStayOpaque) { p->append(SkRasterPipelineOp::premul); }
-    return true;
-}
-
-#if defined(SK_ENABLE_SKVM)
-skvm::Color SkColorFilter_Matrix::onProgram(skvm::Builder* p, skvm::Color c,
-                                            const SkColorInfo& /*dst*/,
-                                            skvm::Uniforms* uniforms, SkArenaAlloc*) const {
-    auto apply_matrix = [&](auto xyzw) {
-        auto dot = [&](int j) {
-            auto custom_mad = [&](float f, skvm::F32 m, skvm::F32 a) {
-                // skvm::Builder won't fold f*0 == 0, but we shouldn't encounter NaN here.
-                // While looking, also simplify f == ±1.  Anything else becomes a uniform.
-                return f ==  0.0f ? a
-                     : f == +1.0f ? a + m
-                     : f == -1.0f ? a - m
-                     : m * p->uniformF(uniforms->pushF(f)) + a;
-            };
-
-            // Similarly, let skvm::Builder fold away the additive bias when zero.
-            const float b = fMatrix[4+j*5];
-            skvm::F32 bias = b == 0.0f ? p->splat(0.0f)
-                                       : p->uniformF(uniforms->pushF(b));
-
-            auto [x,y,z,w] = xyzw;
-            return custom_mad(fMatrix[0+j*5], x,
-                   custom_mad(fMatrix[1+j*5], y,
-                   custom_mad(fMatrix[2+j*5], z,
-                   custom_mad(fMatrix[3+j*5], w, bias))));
-        };
-        return std::make_tuple(dot(0), dot(1), dot(2), dot(3));
-    };
-
-    c = unpremul(c);
-
-    if (fDomain == Domain::kHSLA) {
-        auto [h,s,l,a] = apply_matrix(p->to_hsla(c));
-        c = p->to_rgba({h,s,l,a});
-    } else {
-        auto [r,g,b,a] = apply_matrix(c);
-        c = {r,g,b,a};
-    }
-
-    return premul(clamp01(c));
-}
-#endif
-
-#if defined(SK_GANESH)
-#include "src/gpu/ganesh/effects/GrSkSLFP.h"
-
-static std::unique_ptr<GrFragmentProcessor> rgb_to_hsl(std::unique_ptr<GrFragmentProcessor> child) {
-    static const SkRuntimeEffect* effect = SkMakeRuntimeEffect(SkRuntimeEffect::MakeForColorFilter,
-        "half4 main(half4 color) {"
-            "return $rgb_to_hsl(color.rgb, color.a);"
-        "}"
-    );
-    SkASSERT(SkRuntimeEffectPriv::SupportsConstantOutputForConstantInput(effect));
-    return GrSkSLFP::Make(effect, "RgbToHsl", std::move(child),
-                          GrSkSLFP::OptFlags::kPreservesOpaqueInput);
-}
-
-static std::unique_ptr<GrFragmentProcessor> hsl_to_rgb(std::unique_ptr<GrFragmentProcessor> child) {
-    static const SkRuntimeEffect* effect = SkMakeRuntimeEffect(SkRuntimeEffect::MakeForColorFilter,
-        "half4 main(half4 color) {"
-            "return $hsl_to_rgb(color.rgb, color.a);"
-        "}"
-    );
-    SkASSERT(SkRuntimeEffectPriv::SupportsConstantOutputForConstantInput(effect));
-    return GrSkSLFP::Make(effect, "HslToRgb", std::move(child),
-                          GrSkSLFP::OptFlags::kPreservesOpaqueInput);
-}
-
-GrFPResult SkColorFilter_Matrix::asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> fp,
-                                                     GrRecordingContext*,
-                                                     const GrColorInfo&,
-                                                     const SkSurfaceProps&) const {
-    switch (fDomain) {
-        case Domain::kRGBA:
-            fp = GrFragmentProcessor::ColorMatrix(std::move(fp), fMatrix,
-                                                  /* unpremulInput = */  true,
-                                                  /* clampRGBOutput = */ true,
-                                                  /* premulOutput = */   true);
-            break;
-
-        case Domain::kHSLA:
-            fp = rgb_to_hsl(std::move(fp));
-            fp = GrFragmentProcessor::ColorMatrix(std::move(fp), fMatrix,
-                                                  /* unpremulInput = */  false,
-                                                  /* clampRGBOutput = */ false,
-                                                  /* premulOutput = */   false);
-            fp = hsl_to_rgb(std::move(fp));
-            break;
-    }
-
-    return GrFPSuccess(std::move(fp));
-}
-#endif // defined(SK_GANESH)
-
-#if defined(SK_GRAPHITE)
-void SkColorFilter_Matrix::addToKey(const skgpu::graphite::KeyContext& keyContext,
-                                    skgpu::graphite::PaintParamsKeyBuilder* builder,
-                                    skgpu::graphite::PipelineDataGatherer* gatherer) const {
-    using namespace skgpu::graphite;
-
-    MatrixColorFilterBlock::MatrixColorFilterData matrixCFData(fMatrix,
-                                                               fDomain == Domain::kHSLA);
-
-    MatrixColorFilterBlock::BeginBlock(keyContext, builder, gatherer, &matrixCFData);
-    builder->endBlock();
-}
-#endif // SK_GRAPHITE
-
-///////////////////////////////////////////////////////////////////////////////
-
-static sk_sp<SkColorFilter> MakeMatrix(const float array[20],
-                                       SkColorFilter_Matrix::Domain domain) {
-    if (!sk_floats_are_finite(array, 20)) {
-        return nullptr;
-    }
-    return sk_make_sp<SkColorFilter_Matrix>(array, domain);
-}
-
-sk_sp<SkColorFilter> SkColorFilters::Matrix(const float array[20]) {
-    return MakeMatrix(array, SkColorFilter_Matrix::Domain::kRGBA);
-}
-
-sk_sp<SkColorFilter> SkColorFilters::Matrix(const SkColorMatrix& cm) {
-    return MakeMatrix(cm.fMat.data(), SkColorFilter_Matrix::Domain::kRGBA);
-}
-
-sk_sp<SkColorFilter> SkColorFilters::HSLAMatrix(const float array[20]) {
-    return MakeMatrix(array, SkColorFilter_Matrix::Domain::kHSLA);
-}
-
-sk_sp<SkColorFilter> SkColorFilters::HSLAMatrix(const SkColorMatrix& cm) {
-    return MakeMatrix(cm.fMat.data(), SkColorFilter_Matrix::Domain::kHSLA);
-}
-
-void SkRegisterMatrixColorFilterFlattenable() {
-    SK_REGISTER_FLATTENABLE(SkColorFilter_Matrix);
-}
diff --git a/src/core/SkColorSpaceXformColorFilter.cpp b/src/core/SkColorSpaceXformColorFilter.cpp
new file mode 100644
index 0000000..a914e55
--- /dev/null
+++ b/src/core/SkColorSpaceXformColorFilter.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/core/SkColorSpaceXformColorFilter.h"
+
+#include "include/core/SkAlphaType.h"
+#include "include/core/SkColorFilter.h"
+#include "include/core/SkData.h"
+#include "include/core/SkRefCnt.h"
+#include "src/base/SkNoDestructor.h"
+#include "src/core/SkColorFilterBase.h"
+#include "src/core/SkColorFilterPriv.h"
+#include "src/core/SkColorSpaceXformSteps.h"
+#include "src/core/SkEffectPriv.h"
+#include "src/core/SkRasterPipeline.h"
+#include "src/core/SkRasterPipelineOpList.h"
+#include "src/core/SkReadBuffer.h"
+#include "src/core/SkWriteBuffer.h"
+
+#include <cstdint>
+#include <utility>
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyContext.h"
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#endif
+
+SkColorSpaceXformColorFilter::SkColorSpaceXformColorFilter(sk_sp<SkColorSpace> src,
+                                                           sk_sp<SkColorSpace> dst)
+        : fSrc(std::move(src))
+        , fDst(std::move(dst))
+        , fSteps(  // We handle premul/unpremul separately, so here just always upm->upm.
+                  fSrc.get(),
+                  kUnpremul_SkAlphaType,
+                  fDst.get(),
+                  kUnpremul_SkAlphaType) {}
+
+#if defined(SK_GRAPHITE)
+void SkColorSpaceXformColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
+                                            skgpu::graphite::PaintParamsKeyBuilder* builder,
+                                            skgpu::graphite::PipelineDataGatherer* gatherer) const {
+    using namespace skgpu::graphite;
+
+    constexpr SkAlphaType alphaType = kPremul_SkAlphaType;
+    ColorSpaceTransformBlock::ColorSpaceTransformData data(
+            fSrc.get(), alphaType, fDst.get(), alphaType);
+    ColorSpaceTransformBlock::BeginBlock(keyContext, builder, gatherer, &data);
+    builder->endBlock();
+}
+#endif
+
+bool SkColorSpaceXformColorFilter::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
+    if (!shaderIsOpaque) {
+        rec.fPipeline->append(SkRasterPipelineOp::unpremul);
+    }
+
+    fSteps.apply(rec.fPipeline);
+
+    if (!shaderIsOpaque) {
+        rec.fPipeline->append(SkRasterPipelineOp::premul);
+    }
+    return true;
+}
+
+#if defined(SK_ENABLE_SKVM)
+skvm::Color SkColorSpaceXformColorFilter::onProgram(skvm::Builder* p,
+                                                    skvm::Color c,
+                                                    const SkColorInfo& dst,
+                                                    skvm::Uniforms* uniforms,
+                                                    SkArenaAlloc* alloc) const {
+    return premul(fSteps.program(p, uniforms, unpremul(c)));
+}
+#endif
+
+void SkColorSpaceXformColorFilter::flatten(SkWriteBuffer& buffer) const {
+    buffer.writeDataAsByteArray(fSrc->serialize().get());
+    buffer.writeDataAsByteArray(fDst->serialize().get());
+}
+
+sk_sp<SkFlattenable> SkColorSpaceXformColorFilter::LegacyGammaOnlyCreateProc(SkReadBuffer& buffer) {
+    uint32_t dir = buffer.read32();
+    if (!buffer.validate(dir <= 1)) {
+        return nullptr;
+    }
+    if (dir == 0) {
+        return SkColorFilters::LinearToSRGBGamma();
+    }
+    return SkColorFilters::SRGBToLinearGamma();
+}
+
+sk_sp<SkFlattenable> SkColorSpaceXformColorFilter::CreateProc(SkReadBuffer& buffer) {
+    sk_sp<SkColorSpace> colorSpaces[2];
+    for (int i = 0; i < 2; ++i) {
+        auto data = buffer.readByteArrayAsData();
+        if (!buffer.validate(data != nullptr)) {
+            return nullptr;
+        }
+        colorSpaces[i] = SkColorSpace::Deserialize(data->data(), data->size());
+        if (!buffer.validate(colorSpaces[i] != nullptr)) {
+            return nullptr;
+        }
+    }
+    return sk_sp<SkFlattenable>(
+            new SkColorSpaceXformColorFilter(std::move(colorSpaces[0]), std::move(colorSpaces[1])));
+}
+
+sk_sp<SkColorFilter> SkColorFilters::LinearToSRGBGamma() {
+    static SkNoDestructor<SkColorSpaceXformColorFilter> gSingleton(SkColorSpace::MakeSRGBLinear(),
+                                                                   SkColorSpace::MakeSRGB());
+    return sk_ref_sp(gSingleton.get());
+}
+
+sk_sp<SkColorFilter> SkColorFilters::SRGBToLinearGamma() {
+    static SkNoDestructor<SkColorSpaceXformColorFilter> gSingleton(SkColorSpace::MakeSRGB(),
+                                                                   SkColorSpace::MakeSRGBLinear());
+    return sk_ref_sp(gSingleton.get());
+}
+
+sk_sp<SkColorFilter> SkColorFilterPriv::MakeColorSpaceXform(sk_sp<SkColorSpace> src,
+                                                            sk_sp<SkColorSpace> dst) {
+    return sk_make_sp<SkColorSpaceXformColorFilter>(std::move(src), std::move(dst));
+}
+
+void SkRegisterSkColorSpaceXformColorFilterFlattenable() {
+    SK_REGISTER_FLATTENABLE(SkColorSpaceXformColorFilter);
+    // Previous name
+    SkFlattenable::Register("ColorSpaceXformColorFilter", SkColorSpaceXformColorFilter::CreateProc);
+    // TODO(ccameron): Remove after grace period for SKPs to stop using old serialization.
+    SkFlattenable::Register("SkSRGBGammaColorFilter",
+                            SkColorSpaceXformColorFilter::LegacyGammaOnlyCreateProc);
+}
diff --git a/src/core/SkColorSpaceXformColorFilter.h b/src/core/SkColorSpaceXformColorFilter.h
new file mode 100644
index 0000000..d1f8f7f
--- /dev/null
+++ b/src/core/SkColorSpaceXformColorFilter.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef SkColorSpaceXformColorFilter_DEFINED
+#define SkColorSpaceXformColorFilter_DEFINED
+
+#include "include/core/SkColorSpace.h"
+#include "include/core/SkFlattenable.h"
+#include "include/core/SkRefCnt.h"
+#include "src/core/SkColorFilterBase.h"
+#include "src/core/SkColorSpaceXformSteps.h"
+
+class SkReadBuffer;
+class SkWriteBuffer;
+struct SkStageRec;
+
+class SkColorSpaceXformColorFilter final : public SkColorFilterBase {
+public:
+    SkColorSpaceXformColorFilter(sk_sp<SkColorSpace> src, sk_sp<SkColorSpace> dst);
+
+#if defined(SK_GRAPHITE)
+    void addToKey(const skgpu::graphite::KeyContext& keyContext,
+                  skgpu::graphite::PaintParamsKeyBuilder* builder,
+                  skgpu::graphite::PipelineDataGatherer* gatherer) const override;
+#endif
+
+    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
+
+#if defined(SK_ENABLE_SKVM)
+    skvm::Color onProgram(skvm::Builder* p,
+                          skvm::Color c,
+                          const SkColorInfo& dst,
+                          skvm::Uniforms* uniforms,
+                          SkArenaAlloc* alloc) const override;
+#endif
+
+    SkColorFilterBase::Type type() const override {
+        return SkColorFilterBase::Type::kColorSpaceXform;
+    }
+
+    sk_sp<SkColorSpace> src() const { return fSrc; }
+    sk_sp<SkColorSpace> dst() const { return fDst; }
+
+protected:
+    void flatten(SkWriteBuffer& buffer) const override;
+
+private:
+    friend void ::SkRegisterSkColorSpaceXformColorFilterFlattenable();
+    SK_FLATTENABLE_HOOKS(SkColorSpaceXformColorFilter)
+    static sk_sp<SkFlattenable> LegacyGammaOnlyCreateProc(SkReadBuffer& buffer);
+
+    const sk_sp<SkColorSpace> fSrc;
+    const sk_sp<SkColorSpace> fDst;
+    SkColorSpaceXformSteps fSteps;
+
+    friend class SkColorFilter;
+    using INHERITED = SkColorFilterBase;
+};
+
+#endif
diff --git a/src/core/SkComposeColorFilter.cpp b/src/core/SkComposeColorFilter.cpp
new file mode 100644
index 0000000..5697add
--- /dev/null
+++ b/src/core/SkComposeColorFilter.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/core/SkComposeColorFilter.h"
+
+#include "include/core/SkRefCnt.h"
+#include "src/core/SkColorFilterBase.h"
+#include "src/core/SkReadBuffer.h"
+#include "src/core/SkWriteBuffer.h"
+
+#include <utility>
+struct SkStageRec;
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyContext.h"
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#endif
+
+SkComposeColorFilter::SkComposeColorFilter(sk_sp<SkColorFilter> outer, sk_sp<SkColorFilter> inner)
+        : fOuter(as_CFB_sp(std::move(outer))), fInner(as_CFB_sp(std::move(inner))) {}
+
+bool SkComposeColorFilter::onIsAlphaUnchanged() const {
+    // Can only claim alphaunchanged support if both our proxys do.
+    return fOuter->isAlphaUnchanged() && fInner->isAlphaUnchanged();
+}
+
+bool SkComposeColorFilter::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
+    bool innerIsOpaque = shaderIsOpaque;
+    if (!fInner->isAlphaUnchanged()) {
+        innerIsOpaque = false;
+    }
+    return fInner->appendStages(rec, shaderIsOpaque) && fOuter->appendStages(rec, innerIsOpaque);
+}
+
+#if defined(SK_ENABLE_SKVM)
+skvm::Color SkComposeColorFilter::onProgram(skvm::Builder* p,
+                                            skvm::Color c,
+                                            const SkColorInfo& dst,
+                                            skvm::Uniforms* uniforms,
+                                            SkArenaAlloc* alloc) const {
+    c = fInner->program(p, c, dst, uniforms, alloc);
+    return c ? fOuter->program(p, c, dst, uniforms, alloc) : skvm::Color{};
+}
+#endif
+
+#if defined(SK_GRAPHITE)
+void SkComposeColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
+                                    skgpu::graphite::PaintParamsKeyBuilder* builder,
+                                    skgpu::graphite::PipelineDataGatherer* gatherer) const {
+    using namespace skgpu::graphite;
+
+    ComposeColorFilterBlock::BeginBlock(keyContext, builder, gatherer);
+
+    as_CFB(fInner)->addToKey(keyContext, builder, gatherer);
+    as_CFB(fOuter)->addToKey(keyContext, builder, gatherer);
+
+    builder->endBlock();
+}
+#endif  // SK_GRAPHITE
+
+void SkComposeColorFilter::flatten(SkWriteBuffer& buffer) const {
+    buffer.writeFlattenable(fOuter.get());
+    buffer.writeFlattenable(fInner.get());
+}
+
+sk_sp<SkFlattenable> SkComposeColorFilter::CreateProc(SkReadBuffer& buffer) {
+    sk_sp<SkColorFilter> outer(buffer.readColorFilter());
+    sk_sp<SkColorFilter> inner(buffer.readColorFilter());
+    return outer ? outer->makeComposed(std::move(inner)) : inner;
+}
+
+sk_sp<SkColorFilter> SkColorFilter::makeComposed(sk_sp<SkColorFilter> inner) const {
+    if (!inner) {
+        return sk_ref_sp(this);
+    }
+
+    return sk_sp<SkColorFilter>(new SkComposeColorFilter(sk_ref_sp(this), std::move(inner)));
+}
+
+void SkRegisterComposeColorFilterFlattenable() { SK_REGISTER_FLATTENABLE(SkComposeColorFilter); }
diff --git a/src/core/SkComposeColorFilter.h b/src/core/SkComposeColorFilter.h
new file mode 100644
index 0000000..42cb8bb
--- /dev/null
+++ b/src/core/SkComposeColorFilter.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef SkComposeColorFilter_DEFINED
+#define SkComposeColorFilter_DEFINED
+
+#include "include/core/SkColorFilter.h"
+#include "include/core/SkFlattenable.h"
+#include "include/core/SkRefCnt.h"
+#include "src/core/SkColorFilterBase.h"
+
+class SkReadBuffer;
+class SkWriteBuffer;
+struct SkStageRec;
+
+class SkComposeColorFilter final : public SkColorFilterBase {
+public:
+    bool onIsAlphaUnchanged() const override;
+
+    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
+
+    SkColorFilterBase::Type type() const override { return SkColorFilterBase::Type::kCompose; }
+
+#if defined(SK_ENABLE_SKVM)
+    skvm::Color onProgram(skvm::Builder* p,
+                          skvm::Color c,
+                          const SkColorInfo& dst,
+                          skvm::Uniforms* uniforms,
+                          SkArenaAlloc* alloc) const override;
+#endif
+
+#if defined(SK_GRAPHITE)
+    void addToKey(const skgpu::graphite::KeyContext& keyContext,
+                  skgpu::graphite::PaintParamsKeyBuilder* builder,
+                  skgpu::graphite::PipelineDataGatherer* gatherer) const override;
+#endif  // SK_GRAPHITE
+
+    sk_sp<SkColorFilterBase> outer() const { return fOuter; }
+    sk_sp<SkColorFilterBase> inner() const { return fInner; }
+
+protected:
+    void flatten(SkWriteBuffer& buffer) const override;
+
+private:
+    friend void ::SkRegisterComposeColorFilterFlattenable();
+    SK_FLATTENABLE_HOOKS(SkComposeColorFilter)
+
+    SkComposeColorFilter(sk_sp<SkColorFilter> outer, sk_sp<SkColorFilter> inner);
+
+    sk_sp<SkColorFilterBase> fOuter;
+    sk_sp<SkColorFilterBase> fInner;
+
+    friend class SkColorFilter;
+
+    using INHERITED = SkColorFilter;
+};
+
+#endif
diff --git a/src/core/SkMatrixColorFilter.cpp b/src/core/SkMatrixColorFilter.cpp
new file mode 100644
index 0000000..9712011
--- /dev/null
+++ b/src/core/SkMatrixColorFilter.cpp
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/core/SkMatrixColorFilter.h"
+
+#include "include/core/SkColorFilter.h"
+#include "include/core/SkRefCnt.h"
+#include "include/core/SkScalar.h"
+#include "include/effects/SkColorMatrix.h"
+#include "include/private/base/SkAssert.h"
+#include "include/private/base/SkFloatingPoint.h"
+#include "src/core/SkColorFilterBase.h"
+#include "src/core/SkEffectPriv.h"
+#include "src/core/SkRasterPipeline.h"
+#include "src/core/SkRasterPipelineOpList.h"
+#include "src/core/SkReadBuffer.h"
+#include "src/core/SkWriteBuffer.h"
+
+#include <array>
+#include <cstring>
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#endif  // SK_GRAPHITE
+
+static bool is_alpha_unchanged(const float matrix[20]) {
+    const float* srcA = matrix + 15;
+
+    return SkScalarNearlyZero(srcA[0]) && SkScalarNearlyZero(srcA[1]) &&
+           SkScalarNearlyZero(srcA[2]) && SkScalarNearlyEqual(srcA[3], 1) &&
+           SkScalarNearlyZero(srcA[4]);
+}
+
+SkMatrixColorFilter::SkMatrixColorFilter(const float array[20], Domain domain)
+        : fAlphaIsUnchanged(is_alpha_unchanged(array)), fDomain(domain) {
+    memcpy(fMatrix, array, 20 * sizeof(float));
+}
+
+void SkMatrixColorFilter::flatten(SkWriteBuffer& buffer) const {
+    SkASSERT(sizeof(fMatrix) / sizeof(float) == 20);
+    buffer.writeScalarArray(fMatrix, 20);
+
+    // RGBA flag
+    buffer.writeBool(fDomain == Domain::kRGBA);
+}
+
+sk_sp<SkFlattenable> SkMatrixColorFilter::CreateProc(SkReadBuffer& buffer) {
+    float matrix[20];
+    if (!buffer.readScalarArray(matrix, 20)) {
+        return nullptr;
+    }
+
+    auto is_rgba = buffer.readBool();
+    return is_rgba ? SkColorFilters::Matrix(matrix) : SkColorFilters::HSLAMatrix(matrix);
+}
+
+bool SkMatrixColorFilter::onAsAColorMatrix(float matrix[20]) const {
+    if (matrix) {
+        memcpy(matrix, fMatrix, 20 * sizeof(float));
+    }
+    return true;
+}
+
+bool SkMatrixColorFilter::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
+    const bool willStayOpaque = shaderIsOpaque && fAlphaIsUnchanged,
+               hsla = fDomain == Domain::kHSLA;
+
+    SkRasterPipeline* p = rec.fPipeline;
+    if (!shaderIsOpaque) {
+        p->append(SkRasterPipelineOp::unpremul);
+    }
+    if (hsla) {
+        p->append(SkRasterPipelineOp::rgb_to_hsl);
+    }
+    if (true) {
+        p->append(SkRasterPipelineOp::matrix_4x5, fMatrix);
+    }
+    if (hsla) {
+        p->append(SkRasterPipelineOp::hsl_to_rgb);
+    }
+    if (true) {
+        p->append(SkRasterPipelineOp::clamp_01);
+    }
+    if (!willStayOpaque) {
+        p->append(SkRasterPipelineOp::premul);
+    }
+    return true;
+}
+
+#if defined(SK_ENABLE_SKVM)
+skvm::Color SkMatrixColorFilter::onProgram(skvm::Builder* p,
+                                           skvm::Color c,
+                                           const SkColorInfo& /*dst*/,
+                                           skvm::Uniforms* uniforms,
+                                           SkArenaAlloc*) const {
+    auto apply_matrix = [&](auto xyzw) {
+        auto dot = [&](int j) {
+            auto custom_mad = [&](float f, skvm::F32 m, skvm::F32 a) {
+                // skvm::Builder won't fold f*0 == 0, but we shouldn't encounter NaN here.
+                // While looking, also simplify f == ±1.  Anything else becomes a uniform.
+                return f == 0.0f
+                               ? a
+                               : f == +1.0f ? a + m
+                                            : f == -1.0f ? a - m
+                                                         : m * p->uniformF(uniforms->pushF(f)) + a;
+            };
+
+            // Similarly, let skvm::Builder fold away the additive bias when zero.
+            const float b = fMatrix[4 + j * 5];
+            skvm::F32 bias = b == 0.0f ? p->splat(0.0f) : p->uniformF(uniforms->pushF(b));
+
+            auto [x, y, z, w] = xyzw;
+            return custom_mad(fMatrix[0 + j * 5],
+                              x,
+                              custom_mad(fMatrix[1 + j * 5],
+                                         y,
+                                         custom_mad(fMatrix[2 + j * 5],
+                                                    z,
+                                                    custom_mad(fMatrix[3 + j * 5], w, bias))));
+        };
+        return std::make_tuple(dot(0), dot(1), dot(2), dot(3));
+    };
+
+    c = unpremul(c);
+
+    if (fDomain == Domain::kHSLA) {
+        auto [h, s, l, a] = apply_matrix(p->to_hsla(c));
+        c = p->to_rgba({h, s, l, a});
+    } else {
+        auto [r, g, b, a] = apply_matrix(c);
+        c = {r, g, b, a};
+    }
+
+    return premul(clamp01(c));
+}
+#endif
+
+#if defined(SK_GRAPHITE)
+void SkMatrixColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
+                                   skgpu::graphite::PaintParamsKeyBuilder* builder,
+                                   skgpu::graphite::PipelineDataGatherer* gatherer) const {
+    using namespace skgpu::graphite;
+
+    MatrixColorFilterBlock::MatrixColorFilterData matrixCFData(fMatrix, fDomain == Domain::kHSLA);
+
+    MatrixColorFilterBlock::BeginBlock(keyContext, builder, gatherer, &matrixCFData);
+    builder->endBlock();
+}
+#endif  // SK_GRAPHITE
+
+///////////////////////////////////////////////////////////////////////////////
+
+static sk_sp<SkColorFilter> MakeMatrix(const float array[20], SkMatrixColorFilter::Domain domain) {
+    if (!sk_floats_are_finite(array, 20)) {
+        return nullptr;
+    }
+    return sk_make_sp<SkMatrixColorFilter>(array, domain);
+}
+
+sk_sp<SkColorFilter> SkColorFilters::Matrix(const float array[20]) {
+    return MakeMatrix(array, SkMatrixColorFilter::Domain::kRGBA);
+}
+
+sk_sp<SkColorFilter> SkColorFilters::Matrix(const SkColorMatrix& cm) {
+    return MakeMatrix(cm.fMat.data(), SkMatrixColorFilter::Domain::kRGBA);
+}
+
+sk_sp<SkColorFilter> SkColorFilters::HSLAMatrix(const float array[20]) {
+    return MakeMatrix(array, SkMatrixColorFilter::Domain::kHSLA);
+}
+
+sk_sp<SkColorFilter> SkColorFilters::HSLAMatrix(const SkColorMatrix& cm) {
+    return MakeMatrix(cm.fMat.data(), SkMatrixColorFilter::Domain::kHSLA);
+}
+
+void SkRegisterMatrixColorFilterFlattenable() {
+    SK_REGISTER_FLATTENABLE(SkMatrixColorFilter);
+    // Previous name
+    SkFlattenable::Register("SkColorFilter_Matrix", SkMatrixColorFilter::CreateProc);
+}
diff --git a/src/core/SkMatrixColorFilter.h b/src/core/SkMatrixColorFilter.h
new file mode 100644
index 0000000..5b1cbe0
--- /dev/null
+++ b/src/core/SkMatrixColorFilter.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkMatrixColorFilter_DEFINED
+#define SkMatrixColorFilter_DEFINED
+
+#include "include/core/SkFlattenable.h"
+#include "src/core/SkColorFilterBase.h"
+
+#include <cstdint>
+
+class SkReadBuffer;
+class SkWriteBuffer;
+struct SkStageRec;
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#endif  // SK_GRAPHITE
+
+class SkMatrixColorFilter final : public SkColorFilterBase {
+public:
+    enum class Domain : uint8_t { kRGBA, kHSLA };
+
+    explicit SkMatrixColorFilter(const float array[20], Domain);
+
+    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
+
+    bool onIsAlphaUnchanged() const override { return fAlphaIsUnchanged; }
+
+    SkColorFilterBase::Type type() const override { return SkColorFilterBase::Type::kMatrix; }
+
+#if defined(SK_GRAPHITE)
+    void addToKey(const skgpu::graphite::KeyContext&,
+                  skgpu::graphite::PaintParamsKeyBuilder*,
+                  skgpu::graphite::PipelineDataGatherer*) const override;
+#endif
+
+    Domain domain() const { return fDomain; }
+    const float* matrix() const { return fMatrix; }
+
+private:
+    friend void ::SkRegisterMatrixColorFilterFlattenable();
+    SK_FLATTENABLE_HOOKS(SkMatrixColorFilter)
+
+    void flatten(SkWriteBuffer&) const override;
+    bool onAsAColorMatrix(float matrix[20]) const override;
+
+#if defined(SK_ENABLE_SKVM)
+    skvm::Color onProgram(skvm::Builder*,
+                          skvm::Color,
+                          const SkColorInfo& dst,
+                          skvm::Uniforms* uniforms,
+                          SkArenaAlloc*) const override;
+#endif
+
+    float fMatrix[20];
+    bool fAlphaIsUnchanged;
+    Domain fDomain;
+};
+
+#endif
diff --git a/src/core/SkModeColorFilter.cpp b/src/core/SkModeColorFilter.cpp
deleted file mode 100644
index 65b3baf..0000000
--- a/src/core/SkModeColorFilter.cpp
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright 2006 The Android Open Source Project
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "include/core/SkColorFilter.h"
-#include "include/private/SkColorData.h"
-#include "src/base/SkArenaAlloc.h"
-#include "src/core/SkBlendModePriv.h"
-#include "src/core/SkBlitRow.h"
-#include "src/core/SkColorFilterBase.h"
-#include "src/core/SkColorSpacePriv.h"
-#include "src/core/SkColorSpaceXformSteps.h"
-#include "src/core/SkRasterPipeline.h"
-#include "src/core/SkReadBuffer.h"
-#include "src/core/SkVM.h"
-#include "src/core/SkValidationUtils.h"
-#include "src/core/SkWriteBuffer.h"
-
-#if defined(SK_GRAPHITE)
-#include "src/gpu/graphite/KeyContext.h"
-#include "src/gpu/graphite/KeyHelpers.h"
-#include "src/gpu/graphite/PaintParamsKey.h"
-#endif
-
-template <SkAlphaType kDstAT = kPremul_SkAlphaType>
-static SkRGBA4f<kDstAT> map_color(const SkColor4f& c, SkColorSpace* src, SkColorSpace* dst) {
-    SkRGBA4f<kDstAT> color = {c.fR, c.fG, c.fB, c.fA};
-    SkColorSpaceXformSteps(src, kUnpremul_SkAlphaType,
-                           dst, kDstAT).apply(color.vec());
-    return color;
-}
-
-//////////////////////////////////////////////////////////////////////////////////////////////////
-
-class SkModeColorFilter final : public SkColorFilterBase {
-public:
-    SkModeColorFilter(const SkColor4f& color, SkBlendMode mode);
-
-    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
-
-    bool onIsAlphaUnchanged() const override;
-
-#if defined(SK_GANESH)
-    GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrRecordingContext*,
-                                   const GrColorInfo&,
-                                   const SkSurfaceProps&) const override;
-#endif
-#if defined(SK_GRAPHITE)
-    void addToKey(const skgpu::graphite::KeyContext&,
-                  skgpu::graphite::PaintParamsKeyBuilder*,
-                  skgpu::graphite::PipelineDataGatherer*) const override;
-#endif
-
-private:
-    friend void ::SkRegisterModeColorFilterFlattenable();
-    SK_FLATTENABLE_HOOKS(SkModeColorFilter)
-
-    void flatten(SkWriteBuffer&) const override;
-    bool onAsAColorMode(SkColor*, SkBlendMode*) const override;
-
-#if defined(SK_ENABLE_SKVM)
-    skvm::Color onProgram(skvm::Builder*, skvm::Color,
-                          const SkColorInfo&, skvm::Uniforms*, SkArenaAlloc*) const override;
-#endif
-
-    SkColor4f   fColor; // always stored in sRGB
-    SkBlendMode fMode;
-};
-
-SkModeColorFilter::SkModeColorFilter(const SkColor4f& color,
-                                     SkBlendMode mode)
-        : fColor(color)
-        , fMode(mode) {}
-
-bool SkModeColorFilter::onAsAColorMode(SkColor* color, SkBlendMode* mode) const {
-    if (color) {
-        *color = fColor.toSkColor();
-    }
-    if (mode) {
-        *mode = fMode;
-    }
-    return true;
-}
-
-bool SkModeColorFilter::onIsAlphaUnchanged() const {
-    switch (fMode) {
-        case SkBlendMode::kDst:      //!< [Da, Dc]
-        case SkBlendMode::kSrcATop:  //!< [Da, Sc * Da + (1 - Sa) * Dc]
-            return true;
-        default:
-            break;
-    }
-    return false;
-}
-
-void SkModeColorFilter::flatten(SkWriteBuffer& buffer) const {
-    buffer.writeColor4f(fColor);
-    buffer.writeUInt((int) fMode);
-}
-
-sk_sp<SkFlattenable> SkModeColorFilter::CreateProc(SkReadBuffer& buffer) {
-    if (buffer.isVersionLT(SkPicturePriv::kBlend4fColorFilter)) {
-        // Color is 8-bit, sRGB
-        SkColor color = buffer.readColor();
-        SkBlendMode mode = (SkBlendMode)buffer.readUInt();
-        return SkColorFilters::Blend(SkColor4f::FromColor(color), /*sRGB*/nullptr, mode);
-    } else {
-        // Color is 32-bit, sRGB
-        SkColor4f color;
-        buffer.readColor4f(&color);
-        SkBlendMode mode = (SkBlendMode)buffer.readUInt();
-        return SkColorFilters::Blend(color, /*sRGB*/nullptr, mode);
-    }
-}
-
-bool SkModeColorFilter::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
-    rec.fPipeline->append(SkRasterPipelineOp::move_src_dst);
-    SkPMColor4f color = map_color(fColor, sk_srgb_singleton(), rec.fDstCS);
-    rec.fPipeline->append_constant_color(rec.fAlloc, color.vec());
-    SkBlendMode_AppendStages(fMode, rec.fPipeline);
-    return true;
-}
-
-#if defined(SK_ENABLE_SKVM)
-skvm::Color SkModeColorFilter::onProgram(skvm::Builder* p, skvm::Color c,
-                                         const SkColorInfo& dstInfo,
-                                         skvm::Uniforms* uniforms, SkArenaAlloc*) const {
-    SkPMColor4f color = map_color(fColor, sk_srgb_singleton(), dstInfo.colorSpace());
-    // The blend program operates on this as if it were premul but the API takes an SkColor4f
-    skvm::Color dst = c,
-                src = p->uniformColor({color.fR, color.fG, color.fB, color.fA}, uniforms);
-    return p->blend(fMode, src,dst);
-}
-#endif
-
-///////////////////////////////////////////////////////////////////////////////
-#if defined(SK_GANESH)
-#include "src/gpu/Blend.h"
-#include "src/gpu/ganesh/GrColorInfo.h"
-#include "src/gpu/ganesh/GrFragmentProcessor.h"
-#include "src/gpu/ganesh/SkGr.h"
-#include "src/gpu/ganesh/effects/GrBlendFragmentProcessor.h"
-
-GrFPResult SkModeColorFilter::asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                                  GrRecordingContext*,
-                                                  const GrColorInfo& dstColorInfo,
-                                                  const SkSurfaceProps& props) const {
-    if (fMode == SkBlendMode::kDst) {
-        // If the blend mode is "dest," the blend color won't factor into it at all.
-        // We can return the input FP as-is.
-        return GrFPSuccess(std::move(inputFP));
-    }
-
-    SkDEBUGCODE(const bool fpHasConstIO = !inputFP || inputFP->hasConstantOutputForConstantInput();)
-
-    SkPMColor4f color = map_color(fColor, sk_srgb_singleton(), dstColorInfo.colorSpace());
-
-    auto colorFP = GrFragmentProcessor::MakeColor(color);
-    auto xferFP = GrBlendFragmentProcessor::Make(std::move(colorFP), std::move(inputFP), fMode);
-
-    if (xferFP == nullptr) {
-        // This is only expected to happen if the blend mode is "dest" and the input FP is null.
-        // Since we already did an early-out in the "dest" blend mode case, we shouldn't get here.
-        SkDEBUGFAIL("GrBlendFragmentProcessor::Make returned null unexpectedly");
-        return GrFPFailure(nullptr);
-    }
-
-    // With a solid color input this should always be able to compute the blended color
-    // (at least for coeff modes).
-    // Occasionally, we even do better than we started; specifically, in "src" blend mode, we end up
-    // ditching the input FP entirely, which turns a non-constant operation into a constant one.
-    SkASSERT(fMode > SkBlendMode::kLastCoeffMode ||
-             xferFP->hasConstantOutputForConstantInput() >= fpHasConstIO);
-
-    return GrFPSuccess(std::move(xferFP));
-}
-
-#endif
-
-#if defined(SK_GRAPHITE)
-void SkModeColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
-                                 skgpu::graphite::PaintParamsKeyBuilder* builder,
-                                 skgpu::graphite::PipelineDataGatherer* gatherer) const {
-    using namespace skgpu::graphite;
-
-    SkPMColor4f color = map_color(fColor, sk_srgb_singleton(),
-                                  keyContext.dstColorInfo().colorSpace());
-    AddColorBlendBlock(keyContext, builder, gatherer, fMode, color);
-}
-
-#endif
-
-///////////////////////////////////////////////////////////////////////////////
-
-sk_sp<SkColorFilter> SkColorFilters::Blend(const SkColor4f& color,
-                                           sk_sp<SkColorSpace> colorSpace,
-                                           SkBlendMode mode) {
-    if (!SkIsValidMode(mode)) {
-        return nullptr;
-    }
-
-    // First map to sRGB to simplify storage in the actual SkColorFilter instance, staying unpremul
-    // until the final dst color space is known when actually filtering.
-    SkColor4f srgb = map_color<kUnpremul_SkAlphaType>(
-            color, colorSpace.get(), sk_srgb_singleton());
-
-    // Next collapse some modes if possible
-    float alpha = srgb.fA;
-    if (SkBlendMode::kClear == mode) {
-        srgb = SkColors::kTransparent;
-        mode = SkBlendMode::kSrc;
-    } else if (SkBlendMode::kSrcOver == mode) {
-        if (0.f == alpha) {
-            mode = SkBlendMode::kDst;
-        } else if (1.f == alpha) {
-            mode = SkBlendMode::kSrc;
-        }
-        // else just stay srcover
-    }
-
-    // Finally weed out combinations that are noops, and just return null
-    if (SkBlendMode::kDst == mode ||
-        (0.f == alpha && (SkBlendMode::kSrcOver == mode ||
-                          SkBlendMode::kDstOver == mode ||
-                          SkBlendMode::kDstOut == mode ||
-                          SkBlendMode::kSrcATop == mode ||
-                          SkBlendMode::kXor == mode ||
-                          SkBlendMode::kDarken == mode)) ||
-            (1.f == alpha && SkBlendMode::kDstIn == mode)) {
-        return nullptr;
-    }
-
-    return sk_sp<SkColorFilter>(new SkModeColorFilter(srgb, mode));
-}
-
-sk_sp<SkColorFilter> SkColorFilters::Blend(SkColor color, SkBlendMode mode) {
-    return Blend(SkColor4f::FromColor(color), /*sRGB*/nullptr, mode);
-}
-
-void SkRegisterModeColorFilterFlattenable() {
-    SK_REGISTER_FLATTENABLE(SkModeColorFilter);
-}
diff --git a/src/core/SkRuntimeColorFilter.cpp b/src/core/SkRuntimeColorFilter.cpp
new file mode 100644
index 0000000..8f39df2
--- /dev/null
+++ b/src/core/SkRuntimeColorFilter.cpp
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/core/SkRuntimeColorFilter.h"
+
+#include "include/core/SkCapabilities.h"
+#include "include/core/SkData.h"
+#include "include/core/SkMatrix.h"
+#include "include/core/SkString.h"
+#include "include/effects/SkRuntimeEffect.h"
+#include "include/private/SkColorData.h"
+#include "include/private/SkSLSampleUsage.h"
+#include "include/private/base/SkDebug.h"
+#include "include/private/base/SkTArray.h"
+#include "src/core/SkColorFilterBase.h"
+#include "src/core/SkEffectPriv.h"
+#include "src/core/SkReadBuffer.h"
+#include "src/core/SkRuntimeEffectPriv.h"
+#include "src/core/SkWriteBuffer.h"
+#include "src/shaders/SkShaderBase.h"
+#include "src/sksl/codegen/SkSLRasterPipelineBuilder.h"
+
+#include <string>
+#include <utility>
+
+#if defined(SK_ENABLE_SKVM)
+#include "src/core/SkFilterColorProgram.h"
+#endif
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyContext.h"
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#endif
+
+class SkColorSpace;
+
+SkRuntimeColorFilter::SkRuntimeColorFilter(sk_sp<SkRuntimeEffect> effect,
+                                           sk_sp<const SkData> uniforms,
+                                           SkSpan<SkRuntimeEffect::ChildPtr> children)
+        : fEffect(std::move(effect))
+        , fUniforms(std::move(uniforms))
+        , fChildren(children.begin(), children.end()) {}
+
+#if defined(SK_GRAPHITE)
+void SkRuntimeColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
+                                    skgpu::graphite::PaintParamsKeyBuilder* builder,
+                                    skgpu::graphite::PipelineDataGatherer* gatherer) const {
+    using namespace skgpu::graphite;
+
+    sk_sp<const SkData> uniforms = SkRuntimeEffectPriv::TransformUniforms(
+            fEffect->uniforms(), fUniforms, keyContext.dstColorInfo().colorSpace());
+    SkASSERT(uniforms);
+
+    RuntimeEffectBlock::BeginBlock(keyContext, builder, gatherer, {fEffect, std::move(uniforms)});
+
+    SkRuntimeEffectPriv::AddChildrenToKey(
+            fChildren, fEffect->children(), keyContext, builder, gatherer);
+
+    builder->endBlock();
+}
+#endif
+
+bool SkRuntimeColorFilter::appendStages(const SkStageRec& rec, bool) const {
+#ifdef SK_ENABLE_SKSL_IN_RASTER_PIPELINE
+    if (!SkRuntimeEffectPriv::CanDraw(SkCapabilities::RasterBackend().get(), fEffect.get())) {
+        // SkRP has support for many parts of #version 300 already, but for now, we restrict its
+        // usage in runtime effects to just #version 100.
+        return false;
+    }
+    if (const SkSL::RP::Program* program = fEffect->getRPProgram(/*debugTrace=*/nullptr)) {
+        SkSpan<const float> uniforms =
+                SkRuntimeEffectPriv::UniformsAsSpan(fEffect->uniforms(),
+                                                    fUniforms,
+                                                    /*alwaysCopyIntoAlloc=*/false,
+                                                    rec.fDstCS,
+                                                    rec.fAlloc);
+        SkShaderBase::MatrixRec matrix(SkMatrix::I());
+        matrix.markCTMApplied();
+        RuntimeEffectRPCallbacks callbacks(rec, matrix, fChildren, fEffect->fSampleUsages);
+        bool success = program->appendStages(rec.fPipeline, rec.fAlloc, &callbacks, uniforms);
+        return success;
+    }
+#endif
+    return false;
+}
+
+#if defined(SK_ENABLE_SKVM)
+skvm::Color SkRuntimeColorFilter::onProgram(skvm::Builder* p,
+                                            skvm::Color c,
+                                            const SkColorInfo& colorInfo,
+                                            skvm::Uniforms* uniforms,
+                                            SkArenaAlloc* alloc) const {
+    SkASSERT(SkRuntimeEffectPriv::CanDraw(SkCapabilities::RasterBackend().get(), fEffect.get()));
+
+    sk_sp<const SkData> inputs = SkRuntimeEffectPriv::TransformUniforms(
+            fEffect->uniforms(), fUniforms, colorInfo.colorSpace());
+    SkASSERT(inputs);
+
+    SkShaderBase::MatrixRec mRec(SkMatrix::I());
+    mRec.markTotalMatrixInvalid();
+    RuntimeEffectVMCallbacks callbacks(p, uniforms, alloc, fChildren, mRec, c, colorInfo);
+    std::vector<skvm::Val> uniform =
+            SkRuntimeEffectPriv::MakeSkVMUniforms(p, uniforms, fEffect->uniformSize(), *inputs);
+
+    // There should be no way for the color filter to use device coords, but we need to supply
+    // something. (Uninitialized values can trigger asserts in skvm::Builder).
+    skvm::Coord zeroCoord = {p->splat(0.0f), p->splat(0.0f)};
+    return SkSL::ProgramToSkVM(*fEffect->fBaseProgram,
+                               fEffect->fMain,
+                               p,
+                               /*debugTrace=*/nullptr,
+                               SkSpan(uniform),
+                               /*device=*/zeroCoord,
+                               /*local=*/zeroCoord,
+                               c,
+                               c,
+                               &callbacks);
+}
+#endif
+
+SkPMColor4f SkRuntimeColorFilter::onFilterColor4f(const SkPMColor4f& color,
+                                                  SkColorSpace* dstCS) const {
+#if defined(SK_ENABLE_SKVM)
+    // Get the generic program for filtering a single color
+    if (const SkFilterColorProgram* program = fEffect->getFilterColorProgram()) {
+        // Get our specific uniform values
+        sk_sp<const SkData> inputs =
+                SkRuntimeEffectPriv::TransformUniforms(fEffect->uniforms(), fUniforms, dstCS);
+        SkASSERT(inputs);
+
+        auto evalChild = [&](int index, SkPMColor4f inColor) {
+            const auto& child = fChildren[index];
+
+            // SkFilterColorProgram::Make has guaranteed that any children will be color filters.
+            SkASSERT(!child.shader());
+            SkASSERT(!child.blender());
+            if (SkColorFilter* colorFilter = child.colorFilter()) {
+                return as_CFB(colorFilter)->onFilterColor4f(inColor, dstCS);
+            }
+            return inColor;
+        };
+
+        return program->eval(color, inputs->data(), evalChild);
+    }
+#endif
+    // We were unable to build a cached (per-effect) program. Use the base-class fallback,
+    // which builds a program for the specific filter instance.
+    return SkColorFilterBase::onFilterColor4f(color, dstCS);
+}
+
+bool SkRuntimeColorFilter::onIsAlphaUnchanged() const {
+#ifdef SK_ENABLE_SKSL_IN_RASTER_PIPELINE
+    return fEffect->isAlphaUnchanged();
+#else
+    return fEffect->getFilterColorProgram() && fEffect->isAlphaUnchanged();
+#endif
+}
+
+void SkRuntimeColorFilter::flatten(SkWriteBuffer& buffer) const {
+    buffer.writeString(fEffect->source().c_str());
+    buffer.writeDataAsByteArray(fUniforms.get());
+    SkRuntimeEffectPriv::WriteChildEffects(buffer, fChildren);
+}
+
+SkRuntimeEffect* SkRuntimeColorFilter::asRuntimeEffect() const { return fEffect.get(); }
+
+sk_sp<SkFlattenable> SkRuntimeColorFilter::CreateProc(SkReadBuffer& buffer) {
+    if (!buffer.validate(buffer.allowSkSL())) {
+        return nullptr;
+    }
+
+    SkString sksl;
+    buffer.readString(&sksl);
+    sk_sp<SkData> uniforms = buffer.readByteArrayAsData();
+
+    auto effect = SkMakeCachedRuntimeEffect(SkRuntimeEffect::MakeForColorFilter, std::move(sksl));
+#if !SK_LENIENT_SKSL_DESERIALIZATION
+    if (!buffer.validate(effect != nullptr)) {
+        return nullptr;
+    }
+#endif
+
+    skia_private::STArray<4, SkRuntimeEffect::ChildPtr> children;
+    if (!SkRuntimeEffectPriv::ReadChildEffects(buffer, effect.get(), &children)) {
+        return nullptr;
+    }
+
+#if SK_LENIENT_SKSL_DESERIALIZATION
+    if (!effect) {
+        SkDebugf("Serialized SkSL failed to compile. Ignoring/dropping SkSL color filter.\n");
+        return nullptr;
+    }
+#endif
+
+    return effect->makeColorFilter(std::move(uniforms), SkSpan(children));
+}
diff --git a/src/core/SkRuntimeColorFilter.h b/src/core/SkRuntimeColorFilter.h
new file mode 100644
index 0000000..2b0dd74
--- /dev/null
+++ b/src/core/SkRuntimeColorFilter.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkRuntimeColorFilter_DEFINED
+#define SkRuntimeColorFilter_DEFINED
+
+#include "include/core/SkData.h"
+#include "include/core/SkFlattenable.h"
+#include "include/core/SkRefCnt.h"
+#include "include/effects/SkRuntimeEffect.h"
+#include "include/private/SkColorData.h"
+#include "include/private/base/SkDebug.h"
+#include "include/private/base/SkSpan_impl.h"
+#include "src/core/SkColorFilterBase.h"
+
+#include <vector>
+
+class SkColorSpace;
+class SkReadBuffer;
+class SkWriteBuffer;
+struct SkStageRec;
+
+class SkRuntimeColorFilter : public SkColorFilterBase {
+public:
+    SkRuntimeColorFilter(sk_sp<SkRuntimeEffect> effect,
+                         sk_sp<const SkData> uniforms,
+                         SkSpan<SkRuntimeEffect::ChildPtr> children);
+
+#if defined(SK_GRAPHITE)
+    void addToKey(const skgpu::graphite::KeyContext& keyContext,
+                  skgpu::graphite::PaintParamsKeyBuilder* builder,
+                  skgpu::graphite::PipelineDataGatherer* gatherer) const override;
+#endif
+
+    bool appendStages(const SkStageRec& rec, bool) const override;
+
+#if defined(SK_ENABLE_SKVM)
+    skvm::Color onProgram(skvm::Builder* p,
+                          skvm::Color c,
+                          const SkColorInfo& colorInfo,
+                          skvm::Uniforms* uniforms,
+                          SkArenaAlloc* alloc) const override;
+#endif
+
+    SkPMColor4f onFilterColor4f(const SkPMColor4f& color, SkColorSpace* dstCS) const override;
+
+    bool onIsAlphaUnchanged() const override;
+
+    void flatten(SkWriteBuffer& buffer) const override;
+
+    SkRuntimeEffect* asRuntimeEffect() const override;
+
+    SkColorFilterBase::Type type() const override { return SkColorFilterBase::Type::kRuntime; }
+
+    SK_FLATTENABLE_HOOKS(SkRuntimeColorFilter)
+
+    sk_sp<SkRuntimeEffect> effect() const { return fEffect; }
+    sk_sp<const SkData> uniforms() const { return fUniforms; }
+    std::vector<SkRuntimeEffect::ChildPtr> children() const { return fChildren; }
+
+private:
+    sk_sp<SkRuntimeEffect> fEffect;
+    sk_sp<const SkData> fUniforms;
+    std::vector<SkRuntimeEffect::ChildPtr> fChildren;
+};
+
+#endif
diff --git a/src/core/SkRuntimeEffect.cpp b/src/core/SkRuntimeEffect.cpp
index a2d3946..770f853 100644
--- a/src/core/SkRuntimeEffect.cpp
+++ b/src/core/SkRuntimeEffect.cpp
@@ -18,7 +18,6 @@
 #include "include/core/SkImageInfo.h"
 #include "include/core/SkPaint.h"
 #include "include/core/SkSurface.h"
-#include "include/private/SkColorData.h"
 #include "include/private/base/SkAlign.h"
 #include "include/private/base/SkDebug.h"
 #include "include/private/base/SkMutex.h"
@@ -40,6 +39,7 @@
 #include "src/core/SkRasterPipelineOpList.h"
 #include "src/core/SkReadBuffer.h"
 #include "src/core/SkRuntimeBlender.h"
+#include "src/core/SkRuntimeColorFilter.h"
 #include "src/core/SkRuntimeEffectPriv.h"
 #include "src/core/SkStreamPriv.h"
 #include "src/core/SkWriteBuffer.h"
@@ -69,7 +69,6 @@
 #include <tuple>
 
 class SkColorSpace;
-class SkSurfaceProps;
 struct SkIPoint;
 
 #if defined(SK_GANESH)
@@ -948,198 +947,6 @@
 
 #endif  // defined(SK_ENABLE_SKVM)
 
-class SkRuntimeColorFilter : public SkColorFilterBase {
-public:
-    SkRuntimeColorFilter(sk_sp<SkRuntimeEffect> effect,
-                         sk_sp<const SkData> uniforms,
-                         SkSpan<SkRuntimeEffect::ChildPtr> children)
-            : fEffect(std::move(effect))
-            , fUniforms(std::move(uniforms))
-            , fChildren(children.begin(), children.end()) {}
-
-#if defined(SK_GANESH)
-    GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrRecordingContext* context,
-                                   const GrColorInfo& colorInfo,
-                                   const SkSurfaceProps& props) const override {
-        sk_sp<const SkData> uniforms = SkRuntimeEffectPriv::TransformUniforms(
-                fEffect->uniforms(),
-                fUniforms,
-                colorInfo.colorSpace());
-        SkASSERT(uniforms);
-
-        GrFPArgs childArgs(context, &colorInfo, props);
-        return GrFragmentProcessors::make_effect_fp(fEffect,
-                                                    "runtime_color_filter",
-                                                    std::move(uniforms),
-                                                    std::move(inputFP),
-                                                    /*destColorFP=*/nullptr,
-                                                    SkSpan(fChildren),
-                                                    childArgs);
-    }
-#endif
-
-#if defined(SK_GRAPHITE)
-    void addToKey(const skgpu::graphite::KeyContext& keyContext,
-                  skgpu::graphite::PaintParamsKeyBuilder* builder,
-                  skgpu::graphite::PipelineDataGatherer* gatherer) const override {
-        using namespace skgpu::graphite;
-
-        sk_sp<const SkData> uniforms = SkRuntimeEffectPriv::TransformUniforms(
-                fEffect->uniforms(),
-                fUniforms,
-                keyContext.dstColorInfo().colorSpace());
-        SkASSERT(uniforms);
-
-        RuntimeEffectBlock::BeginBlock(keyContext, builder, gatherer,
-                                       { fEffect, std::move(uniforms) });
-
-        SkRuntimeEffectPriv::AddChildrenToKey(fChildren, fEffect->children(), keyContext, builder,
-                                              gatherer);
-
-        builder->endBlock();
-    }
-#endif
-
-    bool appendStages(const SkStageRec& rec, bool) const override {
-#ifdef SK_ENABLE_SKSL_IN_RASTER_PIPELINE
-        if (!SkRuntimeEffectPriv::CanDraw(SkCapabilities::RasterBackend().get(), fEffect.get())) {
-            // SkRP has support for many parts of #version 300 already, but for now, we restrict its
-            // usage in runtime effects to just #version 100.
-            return false;
-        }
-        if (const SkSL::RP::Program* program = fEffect->getRPProgram(/*debugTrace=*/nullptr)) {
-            SkSpan<const float> uniforms = SkRuntimeEffectPriv::UniformsAsSpan(
-                    fEffect->uniforms(),
-                    fUniforms,
-                    /*alwaysCopyIntoAlloc=*/false,
-                    rec.fDstCS,
-                    rec.fAlloc);
-            SkShaderBase::MatrixRec matrix(SkMatrix::I());
-            matrix.markCTMApplied();
-            RuntimeEffectRPCallbacks callbacks(rec, matrix, fChildren, fEffect->fSampleUsages);
-            bool success = program->appendStages(rec.fPipeline, rec.fAlloc, &callbacks, uniforms);
-            return success;
-        }
-#endif
-        return false;
-    }
-
-#if defined(SK_ENABLE_SKVM)
-    skvm::Color onProgram(skvm::Builder* p, skvm::Color c,
-                          const SkColorInfo& colorInfo,
-                          skvm::Uniforms* uniforms, SkArenaAlloc* alloc) const override {
-        SkASSERT(SkRuntimeEffectPriv::CanDraw(SkCapabilities::RasterBackend().get(),
-                                              fEffect.get()));
-
-        sk_sp<const SkData> inputs = SkRuntimeEffectPriv::TransformUniforms(
-                fEffect->uniforms(),
-                fUniforms,
-                colorInfo.colorSpace());
-        SkASSERT(inputs);
-
-        SkShaderBase::MatrixRec mRec(SkMatrix::I());
-        mRec.markTotalMatrixInvalid();
-        RuntimeEffectVMCallbacks callbacks(p, uniforms, alloc, fChildren, mRec, c, colorInfo);
-        std::vector<skvm::Val> uniform = SkRuntimeEffectPriv::MakeSkVMUniforms(
-                p, uniforms, fEffect->uniformSize(), *inputs);
-
-        // There should be no way for the color filter to use device coords, but we need to supply
-        // something. (Uninitialized values can trigger asserts in skvm::Builder).
-        skvm::Coord zeroCoord = { p->splat(0.0f), p->splat(0.0f) };
-        return SkSL::ProgramToSkVM(*fEffect->fBaseProgram, fEffect->fMain, p,/*debugTrace=*/nullptr,
-                                   SkSpan(uniform), /*device=*/zeroCoord, /*local=*/zeroCoord,
-                                   c, c, &callbacks);
-    }
-#endif
-
-    SkPMColor4f onFilterColor4f(const SkPMColor4f& color, SkColorSpace* dstCS) const override {
-#if defined(SK_ENABLE_SKVM)
-        // Get the generic program for filtering a single color
-        if (const SkFilterColorProgram* program = fEffect->getFilterColorProgram()) {
-            // Get our specific uniform values
-            sk_sp<const SkData> inputs = SkRuntimeEffectPriv::TransformUniforms(
-                    fEffect->uniforms(),
-                    fUniforms,
-                    dstCS);
-            SkASSERT(inputs);
-
-            auto evalChild = [&](int index, SkPMColor4f inColor) {
-                const auto& child = fChildren[index];
-
-                // SkFilterColorProgram::Make has guaranteed that any children will be color filters.
-                SkASSERT(!child.shader());
-                SkASSERT(!child.blender());
-                if (SkColorFilter* colorFilter = child.colorFilter()) {
-                    return as_CFB(colorFilter)->onFilterColor4f(inColor, dstCS);
-                }
-                return inColor;
-            };
-
-            return program->eval(color, inputs->data(), evalChild);
-        }
-#endif
-        // We were unable to build a cached (per-effect) program. Use the base-class fallback,
-        // which builds a program for the specific filter instance.
-        return SkColorFilterBase::onFilterColor4f(color, dstCS);
-    }
-
-    bool onIsAlphaUnchanged() const override {
-#ifdef SK_ENABLE_SKSL_IN_RASTER_PIPELINE
-        return fEffect->isAlphaUnchanged();
-#else
-        return fEffect->getFilterColorProgram() &&
-               fEffect->isAlphaUnchanged();
-#endif
-    }
-
-    void flatten(SkWriteBuffer& buffer) const override {
-        buffer.writeString(fEffect->source().c_str());
-        buffer.writeDataAsByteArray(fUniforms.get());
-        SkRuntimeEffectPriv::WriteChildEffects(buffer, fChildren);
-    }
-
-    SkRuntimeEffect* asRuntimeEffect() const override { return fEffect.get(); }
-
-    SK_FLATTENABLE_HOOKS(SkRuntimeColorFilter)
-
-private:
-    sk_sp<SkRuntimeEffect> fEffect;
-    sk_sp<const SkData> fUniforms;
-    std::vector<SkRuntimeEffect::ChildPtr> fChildren;
-};
-
-sk_sp<SkFlattenable> SkRuntimeColorFilter::CreateProc(SkReadBuffer& buffer) {
-    if (!buffer.validate(buffer.allowSkSL())) {
-        return nullptr;
-    }
-
-    SkString sksl;
-    buffer.readString(&sksl);
-    sk_sp<SkData> uniforms = buffer.readByteArrayAsData();
-
-    auto effect = SkMakeCachedRuntimeEffect(SkRuntimeEffect::MakeForColorFilter, std::move(sksl));
-#if !SK_LENIENT_SKSL_DESERIALIZATION
-    if (!buffer.validate(effect != nullptr)) {
-        return nullptr;
-    }
-#endif
-
-    STArray<4, SkRuntimeEffect::ChildPtr> children;
-    if (!SkRuntimeEffectPriv::ReadChildEffects(buffer, effect.get(), &children)) {
-        return nullptr;
-    }
-
-#if SK_LENIENT_SKSL_DESERIALIZATION
-    if (!effect) {
-        SkDebugf("Serialized SkSL failed to compile. Ignoring/dropping SkSL color filter.\n");
-        return nullptr;
-    }
-#endif
-
-    return effect->makeColorFilter(std::move(uniforms), SkSpan(children));
-}
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 using UniformsCallback = SkRuntimeEffectPriv::UniformsCallback;
diff --git a/src/core/SkVMBlitter.cpp b/src/core/SkVMBlitter.cpp
index 284363e..710c4a7 100644
--- a/src/core/SkVMBlitter.cpp
+++ b/src/core/SkVMBlitter.cpp
@@ -53,6 +53,8 @@
             return c;
         }
 
+        SkColorFilterBase::Type type() const override { return SkColorFilterBase::Type::kNoop; }
+
         bool appendStages(const SkStageRec&, bool) const override { return true; }
 
         // Only created here, should never be flattened / unflattened.
diff --git a/src/core/SkWorkingFormatColorFilter.cpp b/src/core/SkWorkingFormatColorFilter.cpp
new file mode 100644
index 0000000..fefc7e2
--- /dev/null
+++ b/src/core/SkWorkingFormatColorFilter.cpp
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/core/SkWorkingFormatColorFilter.h"
+
+#include "include/core/SkAlphaType.h"
+#include "include/core/SkColor.h"
+#include "include/core/SkColorSpace.h"
+#include "include/core/SkColorType.h"
+#include "include/core/SkImageInfo.h"
+#include "include/core/SkRefCnt.h"
+#include "include/private/base/SkAssert.h"
+#include "modules/skcms/skcms.h"
+#include "src/base/SkArenaAlloc.h"
+#include "src/core/SkColorFilterBase.h"
+#include "src/core/SkColorFilterPriv.h"
+#include "src/core/SkColorSpaceXformSteps.h"
+#include "src/core/SkEffectPriv.h"
+#include "src/core/SkReadBuffer.h"
+#include "src/core/SkWriteBuffer.h"
+
+#include <utility>
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyContext.h"
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#endif
+
+SkWorkingFormatColorFilter::SkWorkingFormatColorFilter(sk_sp<SkColorFilter> child,
+                                                       const skcms_TransferFunction* tf,
+                                                       const skcms_Matrix3x3* gamut,
+                                                       const SkAlphaType* at) {
+    fChild = std::move(child);
+    if (tf) {
+        fTF = *tf;
+        fUseDstTF = false;
+    }
+    if (gamut) {
+        fGamut = *gamut;
+        fUseDstGamut = false;
+    }
+    if (at) {
+        fAT = *at;
+        fUseDstAT = false;
+    }
+}
+
+sk_sp<SkColorSpace> SkWorkingFormatColorFilter::workingFormat(const sk_sp<SkColorSpace>& dstCS,
+                                                              SkAlphaType* at) const {
+    skcms_TransferFunction tf = fTF;
+    skcms_Matrix3x3 gamut = fGamut;
+
+    if (fUseDstTF) {
+        SkAssertResult(dstCS->isNumericalTransferFn(&tf));
+    }
+    if (fUseDstGamut) {
+        SkAssertResult(dstCS->toXYZD50(&gamut));
+    }
+
+    *at = fUseDstAT ? kPremul_SkAlphaType : fAT;
+    return SkColorSpace::MakeRGB(tf, gamut);
+}
+
+#if defined(SK_GRAPHITE)
+void SkWorkingFormatColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
+                                          skgpu::graphite::PaintParamsKeyBuilder* builder,
+                                          skgpu::graphite::PipelineDataGatherer* gatherer) const {
+    using namespace skgpu::graphite;
+
+    const SkAlphaType dstAT = keyContext.dstColorInfo().alphaType();
+    sk_sp<SkColorSpace> dstCS = keyContext.dstColorInfo().refColorSpace();
+    if (!dstCS) {
+        dstCS = SkColorSpace::MakeSRGB();
+    }
+
+    SkAlphaType workingAT;
+    sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
+
+    ColorSpaceTransformBlock::ColorSpaceTransformData data1(
+            dstCS.get(), dstAT, workingCS.get(), workingAT);
+    ColorSpaceTransformBlock::BeginBlock(keyContext, builder, gatherer, &data1);
+    builder->endBlock();
+
+    as_CFB(fChild)->addToKey(keyContext, builder, gatherer);
+
+    ColorSpaceTransformBlock::ColorSpaceTransformData data2(
+            workingCS.get(), workingAT, dstCS.get(), dstAT);
+    ColorSpaceTransformBlock::BeginBlock(keyContext, builder, gatherer, &data2);
+    builder->endBlock();
+}
+#endif
+
+bool SkWorkingFormatColorFilter::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
+    sk_sp<SkColorSpace> dstCS = sk_ref_sp(rec.fDstCS);
+
+    if (!dstCS) {
+        dstCS = SkColorSpace::MakeSRGB();
+    }
+
+    SkAlphaType workingAT;
+    sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
+
+    SkColorInfo dst = {rec.fDstColorType, kPremul_SkAlphaType, dstCS},
+                working = {rec.fDstColorType, workingAT, workingCS};
+
+    const auto* dstToWorking = rec.fAlloc->make<SkColorSpaceXformSteps>(dst, working);
+    const auto* workingToDst = rec.fAlloc->make<SkColorSpaceXformSteps>(working, dst);
+
+    // Any SkSL effects might reference the paint color, which is already in the destination
+    // color space. We need to transform it to the working space for consistency.
+    SkColor4f paintColorInWorkingSpace = rec.fPaintColor;
+    dstToWorking->apply(paintColorInWorkingSpace.vec());
+
+    SkStageRec workingRec = {rec.fPipeline,
+                             rec.fAlloc,
+                             rec.fDstColorType,
+                             workingCS.get(),
+                             paintColorInWorkingSpace,
+                             rec.fSurfaceProps};
+
+    dstToWorking->apply(rec.fPipeline);
+    if (!as_CFB(fChild)->appendStages(workingRec, shaderIsOpaque)) {
+        return false;
+    }
+    workingToDst->apply(rec.fPipeline);
+    return true;
+}
+
+#if defined(SK_ENABLE_SKVM)
+skvm::Color SkWorkingFormatColorFilter::onProgram(skvm::Builder* p,
+                                                  skvm::Color c,
+                                                  const SkColorInfo& rawDst,
+                                                  skvm::Uniforms* uniforms,
+                                                  SkArenaAlloc* alloc) const {
+    sk_sp<SkColorSpace> dstCS = rawDst.refColorSpace();
+    if (!dstCS) {
+        dstCS = SkColorSpace::MakeSRGB();
+    }
+
+    SkAlphaType workingAT;
+    sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
+
+    SkColorInfo dst = {rawDst.colorType(), kPremul_SkAlphaType, dstCS},
+                working = {rawDst.colorType(), workingAT, workingCS};
+
+    c = SkColorSpaceXformSteps{dst, working}.program(p, uniforms, c);
+    c = as_CFB(fChild)->program(p, c, working, uniforms, alloc);
+    return c ? SkColorSpaceXformSteps{working, dst}.program(p, uniforms, c) : c;
+}
+#endif
+
+SkPMColor4f SkWorkingFormatColorFilter::onFilterColor4f(const SkPMColor4f& origColor,
+                                                        SkColorSpace* rawDstCS) const {
+    sk_sp<SkColorSpace> dstCS = sk_ref_sp(rawDstCS);
+    if (!dstCS) {
+        dstCS = SkColorSpace::MakeSRGB();
+    }
+
+    SkAlphaType workingAT;
+    sk_sp<SkColorSpace> workingCS = this->workingFormat(dstCS, &workingAT);
+
+    SkColorInfo dst = {kUnknown_SkColorType, kPremul_SkAlphaType, dstCS},
+                working = {kUnknown_SkColorType, workingAT, workingCS};
+
+    SkPMColor4f color = origColor;
+    SkColorSpaceXformSteps{dst, working}.apply(color.vec());
+    color = as_CFB(fChild)->onFilterColor4f(color, working.colorSpace());
+    SkColorSpaceXformSteps{working, dst}.apply(color.vec());
+    return color;
+}
+
+bool SkWorkingFormatColorFilter::onIsAlphaUnchanged() const { return fChild->isAlphaUnchanged(); }
+
+void SkWorkingFormatColorFilter::flatten(SkWriteBuffer& buffer) const {
+    buffer.writeFlattenable(fChild.get());
+    buffer.writeBool(fUseDstTF);
+    buffer.writeBool(fUseDstGamut);
+    buffer.writeBool(fUseDstAT);
+    if (!fUseDstTF) {
+        buffer.writeScalarArray(&fTF.g, 7);
+    }
+    if (!fUseDstGamut) {
+        buffer.writeScalarArray(&fGamut.vals[0][0], 9);
+    }
+    if (!fUseDstAT) {
+        buffer.writeInt(fAT);
+    }
+}
+
+sk_sp<SkFlattenable> SkWorkingFormatColorFilter::CreateProc(SkReadBuffer& buffer) {
+    sk_sp<SkColorFilter> child = buffer.readColorFilter();
+    bool useDstTF = buffer.readBool(), useDstGamut = buffer.readBool(),
+         useDstAT = buffer.readBool();
+
+    skcms_TransferFunction tf;
+    skcms_Matrix3x3 gamut;
+    SkAlphaType at;
+
+    if (!useDstTF) {
+        buffer.readScalarArray(&tf.g, 7);
+    }
+    if (!useDstGamut) {
+        buffer.readScalarArray(&gamut.vals[0][0], 9);
+    }
+    if (!useDstAT) {
+        at = buffer.read32LE(kLastEnum_SkAlphaType);
+    }
+
+    return SkColorFilterPriv::WithWorkingFormat(std::move(child),
+                                                useDstTF ? nullptr : &tf,
+                                                useDstGamut ? nullptr : &gamut,
+                                                useDstAT ? nullptr : &at);
+}
+
+sk_sp<SkColorFilter> SkColorFilterPriv::WithWorkingFormat(sk_sp<SkColorFilter> child,
+                                                          const skcms_TransferFunction* tf,
+                                                          const skcms_Matrix3x3* gamut,
+                                                          const SkAlphaType* at) {
+    return sk_make_sp<SkWorkingFormatColorFilter>(std::move(child), tf, gamut, at);
+}
+
+void SkRegisterWorkingFormatColorFilterFlattenable() {
+    SK_REGISTER_FLATTENABLE(SkWorkingFormatColorFilter);
+}
diff --git a/src/core/SkWorkingFormatColorFilter.h b/src/core/SkWorkingFormatColorFilter.h
new file mode 100644
index 0000000..1ce1692
--- /dev/null
+++ b/src/core/SkWorkingFormatColorFilter.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef SkWorkingFormatColorFilter_DEFINED
+#define SkWorkingFormatColorFilter_DEFINED
+
+#include "include/core/SkColorFilter.h"
+#include "include/core/SkFlattenable.h"
+#include "include/core/SkRefCnt.h"
+#include "include/private/SkColorData.h"
+#include "modules/skcms/skcms.h"
+#include "src/core/SkColorFilterBase.h"
+
+class SkColorSpace;
+class SkReadBuffer;
+class SkWriteBuffer;
+enum SkAlphaType : int;
+struct SkStageRec;
+
+class SkWorkingFormatColorFilter final : public SkColorFilterBase {
+public:
+    SkWorkingFormatColorFilter(sk_sp<SkColorFilter> child,
+                               const skcms_TransferFunction* tf,
+                               const skcms_Matrix3x3* gamut,
+                               const SkAlphaType* at);
+
+    sk_sp<SkColorSpace> workingFormat(const sk_sp<SkColorSpace>& dstCS, SkAlphaType* at) const;
+
+    SkColorFilterBase::Type type() const override {
+        return SkColorFilterBase::Type::kWorkingFormat;
+    }
+
+#if defined(SK_GRAPHITE)
+    void addToKey(const skgpu::graphite::KeyContext& keyContext,
+                  skgpu::graphite::PaintParamsKeyBuilder* builder,
+                  skgpu::graphite::PipelineDataGatherer* gatherer) const override;
+#endif
+
+    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
+
+#if defined(SK_ENABLE_SKVM)
+    skvm::Color onProgram(skvm::Builder* p,
+                          skvm::Color c,
+                          const SkColorInfo& rawDst,
+                          skvm::Uniforms* uniforms,
+                          SkArenaAlloc* alloc) const override;
+#endif
+
+    SkPMColor4f onFilterColor4f(const SkPMColor4f& origColor,
+                                SkColorSpace* rawDstCS) const override;
+
+    bool onIsAlphaUnchanged() const override;
+
+    sk_sp<SkColorFilter> child() const { return fChild; }
+
+private:
+    friend void ::SkRegisterWorkingFormatColorFilterFlattenable();
+    SK_FLATTENABLE_HOOKS(SkWorkingFormatColorFilter)
+
+    void flatten(SkWriteBuffer& buffer) const override;
+
+    sk_sp<SkColorFilter> fChild;
+    skcms_TransferFunction fTF;
+    bool fUseDstTF = true;
+    skcms_Matrix3x3 fGamut;
+    bool fUseDstGamut = true;
+    SkAlphaType fAT;
+    bool fUseDstAT = true;
+};
+
+#endif
diff --git a/src/effects/BUILD.bazel b/src/effects/BUILD.bazel
index 691d50d..d98e818 100644
--- a/src/effects/BUILD.bazel
+++ b/src/effects/BUILD.bazel
@@ -26,6 +26,7 @@
     "SkShaderMaskFilterImpl.cpp",
     "SkShaderMaskFilterImpl.h",
     "SkTableColorFilter.cpp",
+    "SkTableColorFilter.h",
     "SkTableMaskFilter.cpp",
     "SkTrimPE.h",
     "SkTrimPathEffect.cpp",
diff --git a/src/effects/SkTableColorFilter.cpp b/src/effects/SkTableColorFilter.cpp
index 7b869e0..265bcde 100644
--- a/src/effects/SkTableColorFilter.cpp
+++ b/src/effects/SkTableColorFilter.cpp
@@ -5,15 +5,14 @@
  * found in the LICENSE file.
  */
 
-#include "include/core/SkAlphaType.h"
+#include "src/effects/SkTableColorFilter.h"
+
 #include "include/core/SkBitmap.h"
 #include "include/core/SkColorFilter.h"
 #include "include/core/SkFlattenable.h"
 #include "include/core/SkImageInfo.h"
 #include "include/core/SkRefCnt.h"
-#include "include/core/SkString.h"
 #include "include/core/SkTypes.h"
-#include "include/private/SkSLSampleUsage.h"
 #include "src/base/SkArenaAlloc.h"
 #include "src/core/SkColorFilterBase.h"
 #include "src/core/SkEffectPriv.h"
@@ -24,9 +23,6 @@
 #include "src/core/SkWriteBuffer.h"
 
 #include <cstdint>
-#include <memory>
-#include <tuple>
-#include <utility>
 
 #if defined(SK_GRAPHITE)
 #include "src/gpu/graphite/Image_Graphite.h"
@@ -41,119 +37,70 @@
 }
 #endif
 
-#if defined(SK_GANESH)
-#include "include/gpu/GpuTypes.h"
-#include "include/gpu/GrTypes.h"
-#include "src/gpu/ganesh/GrColorInfo.h"
-#include "src/gpu/ganesh/GrFragmentProcessor.h"
-#include "src/gpu/ganesh/GrProcessorUnitTest.h"
-#include "src/gpu/ganesh/GrSurfaceProxyView.h"
-#include "src/gpu/ganesh/SkGr.h"
-#include "src/gpu/ganesh/effects/GrTextureEffect.h"
-#include "src/gpu/ganesh/glsl/GrGLSLFragmentShaderBuilder.h"
-
-class GrRecordingContext;
-struct GrShaderCaps;
-namespace skgpu { class KeyBuilder; }
-#endif
-
-#if GR_TEST_UTILS
-#include "include/core/SkColorSpace.h"
-#include "include/core/SkSurfaceProps.h"
-#include "include/private/base/SkTo.h"
-#include "include/private/gpu/ganesh/GrTypesPriv.h"
-#include "src/base/SkRandom.h"
-#include "src/gpu/ganesh/GrTestUtils.h"
-#else
-class SkSurfaceProps;
-#endif
-
 #if defined(SK_ENABLE_SKSL) && defined(SK_ENABLE_SKVM)
 #include "src/core/SkVM.h"
 #endif
 
-class SkTableColorFilter final : public SkColorFilterBase {
-public:
-    SkTableColorFilter(const uint8_t tableA[],
-                       const uint8_t tableR[],
-                       const uint8_t tableG[],
-                       const uint8_t tableB[]) {
-        fBitmap.allocPixels(SkImageInfo::MakeA8(256, 4));
-        uint8_t *a = fBitmap.getAddr8(0,0),
-                *r = fBitmap.getAddr8(0,1),
-                *g = fBitmap.getAddr8(0,2),
-                *b = fBitmap.getAddr8(0,3);
-        for (int i = 0; i < 256; i++) {
-            a[i] = tableA ? tableA[i] : i;
-            r[i] = tableR ? tableR[i] : i;
-            g[i] = tableG ? tableG[i] : i;
-            b[i] = tableB ? tableB[i] : i;
-        }
-        fBitmap.setImmutable();
+SkTableColorFilter::SkTableColorFilter(const uint8_t tableA[],
+                                       const uint8_t tableR[],
+                                       const uint8_t tableG[],
+                                       const uint8_t tableB[]) {
+    fBitmap.allocPixels(SkImageInfo::MakeA8(256, 4));
+    uint8_t *a = fBitmap.getAddr8(0, 0), *r = fBitmap.getAddr8(0, 1), *g = fBitmap.getAddr8(0, 2),
+            *b = fBitmap.getAddr8(0, 3);
+    for (int i = 0; i < 256; i++) {
+        a[i] = tableA ? tableA[i] : i;
+        r[i] = tableR ? tableR[i] : i;
+        g[i] = tableG ? tableG[i] : i;
+        b[i] = tableB ? tableB[i] : i;
+    }
+    fBitmap.setImmutable();
+}
+
+bool SkTableColorFilter::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
+    SkRasterPipeline* p = rec.fPipeline;
+    if (!shaderIsOpaque) {
+        p->append(SkRasterPipelineOp::unpremul);
     }
 
-#if defined(SK_GANESH)
-    GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrRecordingContext*, const GrColorInfo&,
-                                   const SkSurfaceProps&) const override;
-#endif
+    SkRasterPipeline_TablesCtx* tables = rec.fAlloc->make<SkRasterPipeline_TablesCtx>();
+    tables->a = fBitmap.getAddr8(0, 0);
+    tables->r = fBitmap.getAddr8(0, 1);
+    tables->g = fBitmap.getAddr8(0, 2);
+    tables->b = fBitmap.getAddr8(0, 3);
+    p->append(SkRasterPipelineOp::byte_tables, tables);
 
-#if defined(SK_GRAPHITE)
-    void addToKey(const skgpu::graphite::KeyContext&,
-                  skgpu::graphite::PaintParamsKeyBuilder*,
-                  skgpu::graphite::PipelineDataGatherer*) const override;
-#endif
-
-    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override {
-        SkRasterPipeline* p = rec.fPipeline;
-        if (!shaderIsOpaque) {
-            p->append(SkRasterPipelineOp::unpremul);
-        }
-
-        SkRasterPipeline_TablesCtx* tables = rec.fAlloc->make<SkRasterPipeline_TablesCtx>();
-        tables->a = fBitmap.getAddr8(0, 0);
-        tables->r = fBitmap.getAddr8(0, 1);
-        tables->g = fBitmap.getAddr8(0, 2);
-        tables->b = fBitmap.getAddr8(0, 3);
-        p->append(SkRasterPipelineOp::byte_tables, tables);
-
-        bool definitelyOpaque = shaderIsOpaque && tables->a[0xff] == 0xff;
-        if (!definitelyOpaque) {
-            p->append(SkRasterPipelineOp::premul);
-        }
-        return true;
+    bool definitelyOpaque = shaderIsOpaque && tables->a[0xff] == 0xff;
+    if (!definitelyOpaque) {
+        p->append(SkRasterPipelineOp::premul);
     }
+    return true;
+}
 
 #if defined(SK_ENABLE_SKVM)
-    skvm::Color onProgram(skvm::Builder* p, skvm::Color c,
-                          const SkColorInfo& dst,
-                          skvm::Uniforms* uniforms, SkArenaAlloc*) const override {
+skvm::Color SkTableColorFilter::onProgram(skvm::Builder* p,
+                                          skvm::Color c,
+                                          const SkColorInfo& dst,
+                                          skvm::Uniforms* uniforms,
+                                          SkArenaAlloc*) const {
+    auto apply_table_to_component = [&](skvm::F32 c, const uint8_t* bytePtr) -> skvm::F32 {
+        skvm::I32 index = to_unorm(8, clamp01(c));
+        skvm::Uniform table = uniforms->pushPtr(bytePtr);
+        return from_unorm(8, gather8(table, index));
+    };
 
-        auto apply_table_to_component = [&](skvm::F32 c, const uint8_t* bytePtr) -> skvm::F32 {
-            skvm::I32     index = to_unorm(8, clamp01(c));
-            skvm::Uniform table = uniforms->pushPtr(bytePtr);
-            return from_unorm(8, gather8(table, index));
-        };
-
-        c = unpremul(c);
-        c.a = apply_table_to_component(c.a, fBitmap.getAddr8(0,0));
-        c.r = apply_table_to_component(c.r, fBitmap.getAddr8(0,1));
-        c.g = apply_table_to_component(c.g, fBitmap.getAddr8(0,2));
-        c.b = apply_table_to_component(c.b, fBitmap.getAddr8(0,3));
-        return premul(c);
-    }
+    c = unpremul(c);
+    c.a = apply_table_to_component(c.a, fBitmap.getAddr8(0, 0));
+    c.r = apply_table_to_component(c.r, fBitmap.getAddr8(0, 1));
+    c.g = apply_table_to_component(c.g, fBitmap.getAddr8(0, 2));
+    c.b = apply_table_to_component(c.b, fBitmap.getAddr8(0, 3));
+    return premul(c);
+}
 #endif
 
-    void flatten(SkWriteBuffer& buffer) const override {
-        buffer.writeByteArray(fBitmap.getAddr8(0,0), 4*256);
-    }
-
-private:
-    friend void ::SkRegisterTableColorFilterFlattenable();
-    SK_FLATTENABLE_HOOKS(SkTableColorFilter)
-
-    SkBitmap fBitmap;
-};
+void SkTableColorFilter::flatten(SkWriteBuffer& buffer) const {
+    buffer.writeByteArray(fBitmap.getAddr8(0, 0), 4 * 256);
+}
 
 sk_sp<SkFlattenable> SkTableColorFilter::CreateProc(SkReadBuffer& buffer) {
     uint8_t argb[4*256];
@@ -163,141 +110,6 @@
     return nullptr;
 }
 
-#if defined(SK_GANESH)
-
-class ColorTableEffect : public GrFragmentProcessor {
-public:
-    static std::unique_ptr<GrFragmentProcessor> Make(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                                     GrRecordingContext* context,
-                                                     const SkBitmap& bitmap);
-
-    ~ColorTableEffect() override {}
-
-    const char* name() const override { return "ColorTableEffect"; }
-
-    std::unique_ptr<GrFragmentProcessor> clone() const override {
-        return std::unique_ptr<GrFragmentProcessor>(new ColorTableEffect(*this));
-    }
-
-    inline static constexpr int kTexEffectFPIndex = 0;
-    inline static constexpr int kInputFPIndex = 1;
-
-private:
-    std::unique_ptr<ProgramImpl> onMakeProgramImpl() const override;
-
-    void onAddToKey(const GrShaderCaps&, skgpu::KeyBuilder*) const override {}
-
-    bool onIsEqual(const GrFragmentProcessor&) const override { return true; }
-
-    ColorTableEffect(std::unique_ptr<GrFragmentProcessor> inputFP, GrSurfaceProxyView view);
-
-    explicit ColorTableEffect(const ColorTableEffect& that);
-
-    GR_DECLARE_FRAGMENT_PROCESSOR_TEST
-
-    using INHERITED = GrFragmentProcessor;
-};
-
-ColorTableEffect::ColorTableEffect(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrSurfaceProxyView view)
-        // Not bothering with table-specific optimizations.
-        : INHERITED(kColorTableEffect_ClassID, kNone_OptimizationFlags) {
-    this->registerChild(GrTextureEffect::Make(std::move(view), kUnknown_SkAlphaType),
-                        SkSL::SampleUsage::Explicit());
-    this->registerChild(std::move(inputFP));
-}
-
-ColorTableEffect::ColorTableEffect(const ColorTableEffect& that)
-        : INHERITED(that) {}
-
-std::unique_ptr<GrFragmentProcessor::ProgramImpl> ColorTableEffect::onMakeProgramImpl() const {
-    class Impl : public ProgramImpl {
-    public:
-        void emitCode(EmitArgs& args) override {
-            GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder;
-            SkString inputColor = this->invokeChild(kInputFPIndex, args);
-            SkString a = this->invokeChild(kTexEffectFPIndex, args, "half2(coord.a, 0.5)");
-            SkString r = this->invokeChild(kTexEffectFPIndex, args, "half2(coord.r, 1.5)");
-            SkString g = this->invokeChild(kTexEffectFPIndex, args, "half2(coord.g, 2.5)");
-            SkString b = this->invokeChild(kTexEffectFPIndex, args, "half2(coord.b, 3.5)");
-            fragBuilder->codeAppendf(
-                    "half4 coord = 255 * unpremul(%s) + 0.5;\n"
-                    "half4 color = half4(%s.a, %s.a, %s.a, 1);\n"
-                    "return color * %s.a;\n",
-                    inputColor.c_str(), r.c_str(), g.c_str(), b.c_str(), a.c_str());
-        }
-    };
-
-    return std::make_unique<Impl>();
-}
-
-std::unique_ptr<GrFragmentProcessor> ColorTableEffect::Make(
-        std::unique_ptr<GrFragmentProcessor> inputFP,
-        GrRecordingContext* context, const SkBitmap& bitmap) {
-    SkASSERT(kPremul_SkAlphaType == bitmap.alphaType());
-    SkASSERT(bitmap.isImmutable());
-
-    auto view = std::get<0>(GrMakeCachedBitmapProxyView(context,
-                                                        bitmap,
-                                                        /*label=*/"MakeColorTableEffect",
-                                                        GrMipmapped::kNo));
-    if (!view) {
-        return nullptr;
-    }
-
-    return std::unique_ptr<GrFragmentProcessor>(new ColorTableEffect(std::move(inputFP),
-                                                                     std::move(view)));
-}
-
-///////////////////////////////////////////////////////////////////////////////
-
-GR_DEFINE_FRAGMENT_PROCESSOR_TEST(ColorTableEffect)
-
-#if GR_TEST_UTILS
-
-
-std::unique_ptr<GrFragmentProcessor> ColorTableEffect::TestCreate(GrProcessorTestData* d) {
-    int flags = 0;
-    uint8_t luts[256][4];
-    do {
-        for (int i = 0; i < 4; ++i) {
-            flags |= d->fRandom->nextBool() ? (1  << i): 0;
-        }
-    } while (!flags);
-    for (int i = 0; i < 4; ++i) {
-        if (flags & (1 << i)) {
-            for (int j = 0; j < 256; ++j) {
-                luts[j][i] = SkToU8(d->fRandom->nextBits(8));
-            }
-        }
-    }
-    auto filter(SkColorFilters::TableARGB(
-        (flags & (1 << 0)) ? luts[0] : nullptr,
-        (flags & (1 << 1)) ? luts[1] : nullptr,
-        (flags & (1 << 2)) ? luts[2] : nullptr,
-        (flags & (1 << 3)) ? luts[3] : nullptr
-    ));
-    sk_sp<SkColorSpace> colorSpace = GrTest::TestColorSpace(d->fRandom);
-    SkSurfaceProps props; // default props for testing
-    auto [success, fp] = as_CFB(filter)->asFragmentProcessor(
-            d->inputFP(), d->context(),
-            GrColorInfo(GrColorType::kRGBA_8888, kUnknown_SkAlphaType, std::move(colorSpace)),
-            props);
-    SkASSERT(success);
-    return std::move(fp);
-}
-#endif
-
-GrFPResult SkTableColorFilter::asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                                   GrRecordingContext* context,
-                                                   const GrColorInfo&,
-                                                   const SkSurfaceProps&) const {
-    auto cte = ColorTableEffect::Make(std::move(inputFP), context, fBitmap);
-    return cte ? GrFPSuccess(std::move(cte)) : GrFPFailure(nullptr);
-}
-
-#endif // defined(SK_GANESH)
-
 #if defined(SK_GRAPHITE)
 
 void SkTableColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
@@ -342,5 +154,6 @@
 
 void SkRegisterTableColorFilterFlattenable() {
     SK_REGISTER_FLATTENABLE(SkTableColorFilter);
+    // Previous name
     SkFlattenable::Register("SkTable_ColorFilter", SkTableColorFilter::CreateProc);
 }
diff --git a/src/effects/SkTableColorFilter.h b/src/effects/SkTableColorFilter.h
new file mode 100644
index 0000000..9eedd78
--- /dev/null
+++ b/src/effects/SkTableColorFilter.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef SkTableColorFilter_DEFINED
+#define SkTableColorFilter_DEFINED
+
+#include "include/core/SkBitmap.h"
+#include "include/core/SkFlattenable.h"
+#include "include/private/base/SkDebug.h"
+#include "src/core/SkColorFilterBase.h"
+
+#include <cstdint>
+
+class SkReadBuffer;
+class SkWriteBuffer;
+struct SkStageRec;
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/Image_Graphite.h"
+#include "src/gpu/graphite/KeyContext.h"
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/Log.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+#include "src/gpu/graphite/RecorderPriv.h"
+
+namespace skgpu::graphite {
+class PipelineDataGatherer;
+}
+#endif
+
+#if defined(SK_ENABLE_SKSL) && defined(SK_ENABLE_SKVM)
+#include "src/core/SkVM.h"
+#endif
+
+class SkTableColorFilter final : public SkColorFilterBase {
+public:
+    SkTableColorFilter(const uint8_t tableA[],
+                       const uint8_t tableR[],
+                       const uint8_t tableG[],
+                       const uint8_t tableB[]);
+
+    SkColorFilterBase::Type type() const override { return SkColorFilterBase::Type::kTable; }
+
+#if defined(SK_GRAPHITE)
+    void addToKey(const skgpu::graphite::KeyContext&,
+                  skgpu::graphite::PaintParamsKeyBuilder*,
+                  skgpu::graphite::PipelineDataGatherer*) const override;
+#endif
+
+    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
+
+#if defined(SK_ENABLE_SKVM)
+    skvm::Color onProgram(skvm::Builder* p,
+                          skvm::Color c,
+                          const SkColorInfo& dst,
+                          skvm::Uniforms* uniforms,
+                          SkArenaAlloc*) const override;
+#endif
+
+    void flatten(SkWriteBuffer& buffer) const override;
+
+    SkBitmap bitmap() const { return fBitmap; }
+
+private:
+    friend void ::SkRegisterTableColorFilterFlattenable();
+    SK_FLATTENABLE_HOOKS(SkTableColorFilter)
+
+    SkBitmap fBitmap;
+};
+
+#endif
diff --git a/src/gpu/ganesh/GrFragmentProcessors.cpp b/src/gpu/ganesh/GrFragmentProcessors.cpp
index 6be7343..8102f06 100644
--- a/src/gpu/ganesh/GrFragmentProcessors.cpp
+++ b/src/gpu/ganesh/GrFragmentProcessors.cpp
@@ -7,29 +7,46 @@
 
 #include "src/gpu/ganesh/GrFragmentProcessors.h"
 
-#include "include/core/SkColorSpace.h"  // IWYU pragma: keep
+#include "include/core/SkAlphaType.h"
+#include "include/core/SkBlendMode.h"
+#include "include/core/SkColor.h"
+#include "include/core/SkColorSpace.h"
 #include "include/core/SkData.h"
 #include "include/core/SkMatrix.h"
 #include "include/core/SkRefCnt.h"
 #include "include/effects/SkRuntimeEffect.h"
 #include "include/gpu/GrRecordingContext.h"
+#include "include/private/SkColorData.h"
 #include "include/private/base/SkAssert.h"
+#include "include/private/base/SkDebug.h"
 #include "include/private/base/SkTArray.h"
 #include "src/core/SkBlendModeBlender.h"
+#include "src/core/SkBlendModeColorFilter.h"
 #include "src/core/SkBlenderBase.h"
 #include "src/core/SkColorFilterBase.h"
+#include "src/core/SkColorSpacePriv.h"
+#include "src/core/SkColorSpaceXformColorFilter.h"
+#include "src/core/SkColorSpaceXformSteps.h"
+#include "src/core/SkComposeColorFilter.h"
 #include "src/core/SkMaskFilterBase.h"
+#include "src/core/SkMatrixColorFilter.h"
 #include "src/core/SkRuntimeBlender.h"
+#include "src/core/SkRuntimeColorFilter.h"
 #include "src/core/SkRuntimeEffectPriv.h"
+#include "src/core/SkWorkingFormatColorFilter.h"
 #include "src/effects/SkShaderMaskFilterImpl.h"
+#include "src/effects/SkTableColorFilter.h"
 #include "src/gpu/ganesh/GrCaps.h"
 #include "src/gpu/ganesh/GrColorInfo.h"
+#include "src/gpu/ganesh/GrColorSpaceXform.h"
 #include "src/gpu/ganesh/GrFPArgs.h"
 #include "src/gpu/ganesh/GrFragmentProcessor.h"
 #include "src/gpu/ganesh/GrRecordingContextPriv.h"
 #include "src/gpu/ganesh/effects/GrBlendFragmentProcessor.h"
+#include "src/gpu/ganesh/effects/GrColorTableEffect.h"
 #include "src/gpu/ganesh/effects/GrSkSLFP.h"
 #include "src/shaders/SkShaderBase.h"
+#include "src/utils/SkGaussianColorFilter.h"
 
 #include <memory>
 #include <optional>
@@ -89,11 +106,11 @@
             childFPs.push_back(std::move(childFP));
         } else if (type == ChildType::kColorFilter) {
             // Convert a SkColorFilter into a child FP.
-            auto [success, childFP] = as_CFB(child.colorFilter())
-                                              ->asFragmentProcessor(/*inputFP=*/nullptr,
-                                                                    childArgs.fContext,
-                                                                    *childArgs.fDstColorInfo,
-                                                                    childArgs.fSurfaceProps);
+            auto [success, childFP] = Make(childArgs.fContext,
+                                           child.colorFilter(),
+                                           /*inputFP=*/nullptr,
+                                           *childArgs.fDstColorInfo,
+                                           childArgs.fSurfaceProps);
             if (!success) {
                 return GrFPFailure(std::move(inputFP));
             }
@@ -180,6 +197,227 @@
     SkUNREACHABLE;
 }
 
+static SkPMColor4f map_color(const SkColor4f& c, SkColorSpace* src, SkColorSpace* dst) {
+    SkPMColor4f color = {c.fR, c.fG, c.fB, c.fA};
+    SkColorSpaceXformSteps(src, kUnpremul_SkAlphaType, dst, kPremul_SkAlphaType).apply(color.vec());
+    return color;
+}
+static GrFPResult make_colorfilter_fp(GrRecordingContext*,
+                                      const SkBlendModeColorFilter* filter,
+                                      std::unique_ptr<GrFragmentProcessor> inputFP,
+                                      const GrColorInfo& dstColorInfo,
+                                      const SkSurfaceProps& props) {
+    if (filter->mode() == SkBlendMode::kDst) {
+        // If the blend mode is "dest," the blend color won't factor into it at all.
+        // We can return the input FP as-is.
+        return GrFPSuccess(std::move(inputFP));
+    }
+
+    SkDEBUGCODE(const bool fpHasConstIO = !inputFP || inputFP->hasConstantOutputForConstantInput();)
+
+    SkPMColor4f color = map_color(filter->color(), sk_srgb_singleton(), dstColorInfo.colorSpace());
+
+    auto colorFP = GrFragmentProcessor::MakeColor(color);
+    auto xferFP =
+            GrBlendFragmentProcessor::Make(std::move(colorFP), std::move(inputFP), filter->mode());
+
+    if (xferFP == nullptr) {
+        // This is only expected to happen if the blend mode is "dest" and the input FP is null.
+        // Since we already did an early-out in the "dest" blend mode case, we shouldn't get here.
+        SkDEBUGFAIL("GrBlendFragmentProcessor::Make returned null unexpectedly");
+        return GrFPFailure(nullptr);
+    }
+
+    // With a solid color input this should always be able to compute the blended color
+    // (at least for coeff modes).
+    // Occasionally, we even do better than we started; specifically, in "src" blend mode, we end up
+    // ditching the input FP entirely, which turns a non-constant operation into a constant one.
+    SkASSERT(filter->mode() > SkBlendMode::kLastCoeffMode ||
+             xferFP->hasConstantOutputForConstantInput() >= fpHasConstIO);
+
+    return GrFPSuccess(std::move(xferFP));
+}
+
+static GrFPResult make_colorfilter_fp(GrRecordingContext* context,
+                                      const SkComposeColorFilter* filter,
+                                      std::unique_ptr<GrFragmentProcessor> inputFP,
+                                      const GrColorInfo& dstColorInfo,
+                                      const SkSurfaceProps& props) {
+    // Unfortunately, we need to clone the input before we know we need it. This lets us return
+    // the original FP if either internal color filter fails.
+    auto inputClone = inputFP ? inputFP->clone() : nullptr;
+
+    auto [innerSuccess, innerFP] =
+            Make(context, filter->inner().get(), std::move(inputFP), dstColorInfo, props);
+    if (!innerSuccess) {
+        return GrFPFailure(std::move(inputClone));
+    }
+
+    auto [outerSuccess, outerFP] =
+            Make(context, filter->outer().get(), std::move(innerFP), dstColorInfo, props);
+    if (!outerSuccess) {
+        return GrFPFailure(std::move(inputClone));
+    }
+
+    return GrFPSuccess(std::move(outerFP));
+}
+
+static GrFPResult make_colorfilter_fp(GrRecordingContext*,
+                                      const SkColorSpaceXformColorFilter* filter,
+                                      std::unique_ptr<GrFragmentProcessor> inputFP,
+                                      const GrColorInfo&,
+                                      const SkSurfaceProps&) {
+    // wish our caller would let us know if our input was opaque...
+    constexpr SkAlphaType alphaType = kPremul_SkAlphaType;
+    return GrFPSuccess(GrColorSpaceXformEffect::Make(
+            std::move(inputFP), filter->src().get(), alphaType, filter->dst().get(), alphaType));
+}
+
+static GrFPResult make_colorfilter_fp(GrRecordingContext*,
+                                      const SkGaussianColorFilter*,
+                                      std::unique_ptr<GrFragmentProcessor> inputFP,
+                                      const GrColorInfo&,
+                                      const SkSurfaceProps&) {
+    static const SkRuntimeEffect* effect =
+            SkMakeRuntimeEffect(SkRuntimeEffect::MakeForColorFilter,
+                                "half4 main(half4 inColor) {"
+                                "half factor = 1 - inColor.a;"
+                                "factor = exp(-factor * factor * 4) - 0.018;"
+                                "return half4(factor);"
+                                "}");
+    SkASSERT(SkRuntimeEffectPriv::SupportsConstantOutputForConstantInput(effect));
+    return GrFPSuccess(
+            GrSkSLFP::Make(effect, "gaussian_fp", std::move(inputFP), GrSkSLFP::OptFlags::kNone));
+}
+
+static std::unique_ptr<GrFragmentProcessor> rgb_to_hsl(std::unique_ptr<GrFragmentProcessor> child) {
+    static const SkRuntimeEffect* effect =
+            SkMakeRuntimeEffect(SkRuntimeEffect::MakeForColorFilter,
+                                "half4 main(half4 color) {"
+                                "return $rgb_to_hsl(color.rgb, color.a);"
+                                "}");
+    SkASSERT(SkRuntimeEffectPriv::SupportsConstantOutputForConstantInput(effect));
+    return GrSkSLFP::Make(
+            effect, "RgbToHsl", std::move(child), GrSkSLFP::OptFlags::kPreservesOpaqueInput);
+}
+
+static std::unique_ptr<GrFragmentProcessor> hsl_to_rgb(std::unique_ptr<GrFragmentProcessor> child) {
+    static const SkRuntimeEffect* effect =
+            SkMakeRuntimeEffect(SkRuntimeEffect::MakeForColorFilter,
+                                "half4 main(half4 color) {"
+                                "return $hsl_to_rgb(color.rgb, color.a);"
+                                "}");
+    SkASSERT(SkRuntimeEffectPriv::SupportsConstantOutputForConstantInput(effect));
+    return GrSkSLFP::Make(
+            effect, "HslToRgb", std::move(child), GrSkSLFP::OptFlags::kPreservesOpaqueInput);
+}
+
+static GrFPResult make_colorfilter_fp(GrRecordingContext*,
+                                      const SkMatrixColorFilter* filter,
+                                      std::unique_ptr<GrFragmentProcessor> inputFP,
+                                      const GrColorInfo&,
+                                      const SkSurfaceProps&) {
+    switch (filter->domain()) {
+        case SkMatrixColorFilter::Domain::kRGBA:
+            return GrFPSuccess(GrFragmentProcessor::ColorMatrix(std::move(inputFP),
+                                                                filter->matrix(),
+                                                                /* unpremulInput = */ true,
+                                                                /* clampRGBOutput = */ true,
+                                                                /* premulOutput = */ true));
+
+        case SkMatrixColorFilter::Domain::kHSLA: {
+            auto fp = rgb_to_hsl(std::move(inputFP));
+            fp = GrFragmentProcessor::ColorMatrix(std::move(fp),
+                                                  filter->matrix(),
+                                                  /* unpremulInput = */ false,
+                                                  /* clampRGBOutput = */ false,
+                                                  /* premulOutput = */ false);
+            return GrFPSuccess(hsl_to_rgb(std::move(fp)));
+        }
+    }
+    SkUNREACHABLE;
+}
+
+static GrFPResult make_colorfilter_fp(GrRecordingContext* context,
+                                      const SkRuntimeColorFilter* filter,
+                                      std::unique_ptr<GrFragmentProcessor> inputFP,
+                                      const GrColorInfo& colorInfo,
+                                      const SkSurfaceProps& props) {
+    sk_sp<const SkData> uniforms = SkRuntimeEffectPriv::TransformUniforms(
+            filter->effect()->uniforms(), filter->uniforms(), colorInfo.colorSpace());
+    SkASSERT(uniforms);
+
+    GrFPArgs childArgs(context, &colorInfo, props);
+    auto children = filter->children();
+    return make_effect_fp(filter->effect(),
+                          "runtime_color_filter",
+                          std::move(uniforms),
+                          std::move(inputFP),
+                          /*destColorFP=*/nullptr,
+                          SkSpan(children),
+                          childArgs);
+}
+
+static GrFPResult make_colorfilter_fp(GrRecordingContext* context,
+                                      const SkTableColorFilter* filter,
+                                      std::unique_ptr<GrFragmentProcessor> inputFP,
+                                      const GrColorInfo&,
+                                      const SkSurfaceProps&) {
+    auto cte = ColorTableEffect::Make(std::move(inputFP), context, filter->bitmap());
+    return cte ? GrFPSuccess(std::move(cte)) : GrFPFailure(nullptr);
+}
+
+static GrFPResult make_colorfilter_fp(GrRecordingContext* context,
+                                      const SkWorkingFormatColorFilter* filter,
+                                      std::unique_ptr<GrFragmentProcessor> inputFP,
+                                      const GrColorInfo& dstColorInfo,
+                                      const SkSurfaceProps& props) {
+    sk_sp<SkColorSpace> dstCS = dstColorInfo.refColorSpace();
+    if (!dstCS) {
+        dstCS = SkColorSpace::MakeSRGB();
+    }
+
+    SkAlphaType workingAT;
+    sk_sp<SkColorSpace> workingCS = filter->workingFormat(dstCS, &workingAT);
+
+    GrColorInfo dst = {dstColorInfo.colorType(), dstColorInfo.alphaType(), dstCS},
+                working = {dstColorInfo.colorType(), workingAT, workingCS};
+
+    auto [ok, fp] = Make(context,
+                         filter->child().get(),
+                         GrColorSpaceXformEffect::Make(std::move(inputFP), dst, working),
+                         working,
+                         props);
+
+    return ok ? GrFPSuccess(GrColorSpaceXformEffect::Make(std::move(fp), working, dst))
+              : GrFPFailure(std::move(fp));
+}
+
+GrFPResult Make(GrRecordingContext* ctx,
+                const SkColorFilter* cf,
+                std::unique_ptr<GrFragmentProcessor> inputFP,
+                const GrColorInfo& dstColorInfo,
+                const SkSurfaceProps& props) {
+    if (!cf) {
+        return GrFPFailure(nullptr);
+    }
+    auto cfb = as_CFB(cf);
+    switch (cfb->type()) {
+        case SkColorFilterBase::Type::kNoop:
+            return GrFPFailure(nullptr);
+#define M(type)                                                                   \
+    case SkColorFilterBase::Type::k##type:                                        \
+        return make_colorfilter_fp(ctx,                                           \
+                                   static_cast<const Sk##type##ColorFilter*>(cf), \
+                                   std::move(inputFP),                            \
+                                   dstColorInfo,                                  \
+                                   props);
+            SK_ALL_COLOR_FILTERS(M)
+#undef M
+    }
+    SkUNREACHABLE;
+}
+
 bool IsSupported(const SkMaskFilter* maskfilter) {
     if (!maskfilter) {
         return false;
diff --git a/src/gpu/ganesh/GrFragmentProcessors.h b/src/gpu/ganesh/GrFragmentProcessors.h
index 0dbc37b..460c90e 100644
--- a/src/gpu/ganesh/GrFragmentProcessors.h
+++ b/src/gpu/ganesh/GrFragmentProcessors.h
@@ -15,20 +15,20 @@
 #include <tuple>
 #include <memory>
 
+class GrColorInfo;
 class GrFragmentProcessor;
+class GrRecordingContext;
 class SkBlenderBase;
+class SkColorFilter;
 class SkData;
 class SkMaskFilter;
 class SkMatrix;
+class SkSurfaceProps;
 struct GrFPArgs;
 
 using GrFPResult = std::tuple<bool, std::unique_ptr<GrFragmentProcessor>>;
 
 namespace GrFragmentProcessors {
-std::unique_ptr<GrFragmentProcessor> Make(const SkMaskFilter*,
-                                          const GrFPArgs&,
-                                          const SkMatrix& ctm);
-
 /**
  * Returns a GrFragmentProcessor that implements this blend for the Ganesh GPU backend.
  * The GrFragmentProcessor expects premultiplied inputs and returns a premultiplied output.
@@ -38,6 +38,24 @@
                                           std::unique_ptr<GrFragmentProcessor> dstFP,
                                           const GrFPArgs& fpArgs);
 
+/**
+ *  Returns a GrFragmentProcessor that implements the color filter in GPU shader code.
+ *
+ *  The fragment processor receives a input FP that generates a premultiplied input color, and
+ *  produces a premultiplied output color.
+ *
+ *  A GrFPFailure indicates that the color filter isn't implemented for the GPU backend.
+ */
+GrFPResult Make(GrRecordingContext*,
+                const SkColorFilter*,
+                std::unique_ptr<GrFragmentProcessor> inputFP,
+                const GrColorInfo& dstColorInfo,
+                const SkSurfaceProps&);
+
+std::unique_ptr<GrFragmentProcessor> Make(const SkMaskFilter*,
+                                          const GrFPArgs&,
+                                          const SkMatrix& ctm);
+
 bool IsSupported(const SkMaskFilter*);
 
 // TODO(kjlubick, brianosman) remove this after all related effects have been migrated
diff --git a/src/gpu/ganesh/SkGr.cpp b/src/gpu/ganesh/SkGr.cpp
index 3199e1b..f4812ce 100644
--- a/src/gpu/ganesh/SkGr.cpp
+++ b/src/gpu/ganesh/SkGr.cpp
@@ -27,7 +27,6 @@
 #include "include/private/gpu/ganesh/GrTypesPriv.h"
 #include "src/core/SkBlendModePriv.h"
 #include "src/core/SkBlenderBase.h"
-#include "src/core/SkColorFilterBase.h"
 #include "src/core/SkMessageBus.h"
 #include "src/core/SkPaintPriv.h"
 #include "src/core/SkRuntimeEffectPriv.h"
@@ -475,9 +474,8 @@
             SkColorSpace* dstCS = dstColorInfo.colorSpace();
             grPaint->setColor4f(colorFilter->filterColor4f(origColor, dstCS, dstCS).premul());
         } else {
-            auto [success, fp] = as_CFB(colorFilter)->asFragmentProcessor(std::move(paintFP),
-                                                                          context, dstColorInfo,
-                                                                          surfaceProps);
+            auto [success, fp] = GrFragmentProcessors::Make(
+                    context, colorFilter, std::move(paintFP), dstColorInfo, surfaceProps);
             if (!success) {
                 return false;
             }
diff --git a/src/gpu/ganesh/effects/BUILD.bazel b/src/gpu/ganesh/effects/BUILD.bazel
index 36d36d6..c00129c 100644
--- a/src/gpu/ganesh/effects/BUILD.bazel
+++ b/src/gpu/ganesh/effects/BUILD.bazel
@@ -14,6 +14,8 @@
     "GrBitmapTextGeoProc.h",
     "GrBlendFragmentProcessor.cpp",
     "GrBlendFragmentProcessor.h",
+    "GrColorTableEffect.cpp",
+    "GrColorTableEffect.h",
     "GrConvexPolyEffect.cpp",
     "GrConvexPolyEffect.h",
     "GrCoverageSetOpXP.cpp",
diff --git a/src/gpu/ganesh/effects/GrColorTableEffect.cpp b/src/gpu/ganesh/effects/GrColorTableEffect.cpp
new file mode 100644
index 0000000..095b29e
--- /dev/null
+++ b/src/gpu/ganesh/effects/GrColorTableEffect.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/ganesh/effects/GrColorTableEffect.h"
+
+#include "include/core/SkAlphaType.h"
+#include "include/core/SkBitmap.h"
+#include "include/core/SkColorFilter.h"
+#include "include/core/SkColorSpace.h"
+#include "include/core/SkRefCnt.h"
+#include "include/core/SkString.h"
+#include "include/core/SkSurfaceProps.h"
+#include "include/gpu/GpuTypes.h"
+#include "include/gpu/GrTypes.h"
+#include "include/private/SkSLSampleUsage.h"
+#include "include/private/base/SkAssert.h"
+#include "include/private/base/SkTo.h"
+#include "include/private/gpu/ganesh/GrTypesPriv.h"
+#include "src/base/SkRandom.h"
+#include "src/gpu/ganesh/GrColorInfo.h"
+#include "src/gpu/ganesh/GrFragmentProcessor.h"
+#include "src/gpu/ganesh/GrFragmentProcessors.h"
+#include "src/gpu/ganesh/GrProcessorUnitTest.h"
+#include "src/gpu/ganesh/GrSurfaceProxyView.h"
+#include "src/gpu/ganesh/GrTestUtils.h"
+#include "src/gpu/ganesh/SkGr.h"
+#include "src/gpu/ganesh/effects/GrTextureEffect.h"
+#include "src/gpu/ganesh/glsl/GrGLSLFragmentShaderBuilder.h"
+
+#include <cstdint>
+#include <tuple>
+#include <utility>
+
+class GrRecordingContext;
+
+ColorTableEffect::ColorTableEffect(std::unique_ptr<GrFragmentProcessor> inputFP,
+                                   GrSurfaceProxyView view)
+        // Not bothering with table-specific optimizations.
+        : GrFragmentProcessor(kColorTableEffect_ClassID, kNone_OptimizationFlags) {
+    this->registerChild(GrTextureEffect::Make(std::move(view), kUnknown_SkAlphaType),
+                        SkSL::SampleUsage::Explicit());
+    this->registerChild(std::move(inputFP));
+}
+
+ColorTableEffect::ColorTableEffect(const ColorTableEffect& that) : GrFragmentProcessor(that) {}
+
+std::unique_ptr<GrFragmentProcessor::ProgramImpl> ColorTableEffect::onMakeProgramImpl() const {
+    class Impl : public ProgramImpl {
+    public:
+        void emitCode(EmitArgs& args) override {
+            GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder;
+            SkString inputColor = this->invokeChild(kInputFPIndex, args);
+            SkString a = this->invokeChild(kTexEffectFPIndex, args, "half2(coord.a, 0.5)");
+            SkString r = this->invokeChild(kTexEffectFPIndex, args, "half2(coord.r, 1.5)");
+            SkString g = this->invokeChild(kTexEffectFPIndex, args, "half2(coord.g, 2.5)");
+            SkString b = this->invokeChild(kTexEffectFPIndex, args, "half2(coord.b, 3.5)");
+            fragBuilder->codeAppendf(
+                    "half4 coord = 255 * unpremul(%s) + 0.5;\n"
+                    "half4 color = half4(%s.a, %s.a, %s.a, 1);\n"
+                    "return color * %s.a;\n",
+                    inputColor.c_str(),
+                    r.c_str(),
+                    g.c_str(),
+                    b.c_str(),
+                    a.c_str());
+        }
+    };
+
+    return std::make_unique<Impl>();
+}
+
+std::unique_ptr<GrFragmentProcessor> ColorTableEffect::Make(
+        std::unique_ptr<GrFragmentProcessor> inputFP,
+        GrRecordingContext* context,
+        const SkBitmap& bitmap) {
+    SkASSERT(kPremul_SkAlphaType == bitmap.alphaType());
+    SkASSERT(bitmap.isImmutable());
+
+    auto view = std::get<0>(GrMakeCachedBitmapProxyView(context,
+                                                        bitmap,
+                                                        /*label=*/"MakeColorTableEffect",
+                                                        GrMipmapped::kNo));
+    if (!view) {
+        return nullptr;
+    }
+
+    return std::unique_ptr<GrFragmentProcessor>(
+            new ColorTableEffect(std::move(inputFP), std::move(view)));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+GR_DEFINE_FRAGMENT_PROCESSOR_TEST(ColorTableEffect)
+
+#if GR_TEST_UTILS
+std::unique_ptr<GrFragmentProcessor> ColorTableEffect::TestCreate(GrProcessorTestData* d) {
+    int flags = 0;
+    uint8_t luts[256][4];
+    do {
+        for (int i = 0; i < 4; ++i) {
+            flags |= d->fRandom->nextBool() ? (1 << i) : 0;
+        }
+    } while (!flags);
+    for (int i = 0; i < 4; ++i) {
+        if (flags & (1 << i)) {
+            for (int j = 0; j < 256; ++j) {
+                luts[j][i] = SkToU8(d->fRandom->nextBits(8));
+            }
+        }
+    }
+    auto filter(SkColorFilters::TableARGB((flags & (1 << 0)) ? luts[0] : nullptr,
+                                          (flags & (1 << 1)) ? luts[1] : nullptr,
+                                          (flags & (1 << 2)) ? luts[2] : nullptr,
+                                          (flags & (1 << 3)) ? luts[3] : nullptr));
+    sk_sp<SkColorSpace> colorSpace = GrTest::TestColorSpace(d->fRandom);
+    SkSurfaceProps props;  // default props for testing
+    auto [success, fp] = GrFragmentProcessors::Make(
+            d->context(),
+            filter.get(),
+            d->inputFP(),
+            GrColorInfo(GrColorType::kRGBA_8888, kUnknown_SkAlphaType, std::move(colorSpace)),
+            props);
+    SkASSERT(success);
+    return std::move(fp);
+}
+#endif
diff --git a/src/gpu/ganesh/effects/GrColorTableEffect.h b/src/gpu/ganesh/effects/GrColorTableEffect.h
new file mode 100644
index 0000000..823e68c
--- /dev/null
+++ b/src/gpu/ganesh/effects/GrColorTableEffect.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef GrColorTableEffect_DEFINED
+#define GrColorTableEffect_DEFINED
+
+#include "src/gpu/ganesh/GrFragmentProcessor.h"
+#include "src/gpu/ganesh/GrProcessorUnitTest.h"
+
+#include <memory>
+
+class GrRecordingContext;
+class GrSurfaceProxyView;
+class SkBitmap;
+struct GrShaderCaps;
+
+namespace skgpu {
+class KeyBuilder;
+}
+
+class ColorTableEffect : public GrFragmentProcessor {
+public:
+    static std::unique_ptr<GrFragmentProcessor> Make(std::unique_ptr<GrFragmentProcessor> inputFP,
+                                                     GrRecordingContext* context,
+                                                     const SkBitmap& bitmap);
+
+    ~ColorTableEffect() override {}
+
+    const char* name() const override { return "ColorTableEffect"; }
+
+    std::unique_ptr<GrFragmentProcessor> clone() const override {
+        return std::unique_ptr<GrFragmentProcessor>(new ColorTableEffect(*this));
+    }
+
+    inline static constexpr int kTexEffectFPIndex = 0;
+    inline static constexpr int kInputFPIndex = 1;
+
+private:
+    std::unique_ptr<ProgramImpl> onMakeProgramImpl() const override;
+
+    void onAddToKey(const GrShaderCaps&, skgpu::KeyBuilder*) const override {}
+
+    bool onIsEqual(const GrFragmentProcessor&) const override { return true; }
+
+    ColorTableEffect(std::unique_ptr<GrFragmentProcessor> inputFP, GrSurfaceProxyView view);
+
+    explicit ColorTableEffect(const ColorTableEffect& that);
+
+    GR_DECLARE_FRAGMENT_PROCESSOR_TEST
+};
+
+#endif
diff --git a/src/gpu/ganesh/effects/GrSkSLFP.cpp b/src/gpu/ganesh/effects/GrSkSLFP.cpp
index c060625..cc909d5 100644
--- a/src/gpu/ganesh/effects/GrSkSLFP.cpp
+++ b/src/gpu/ganesh/effects/GrSkSLFP.cpp
@@ -21,7 +21,6 @@
 #include "include/private/gpu/ganesh/GrTypesPriv.h"
 #include "src/base/SkArenaAlloc.h"
 #include "src/base/SkRandom.h"
-#include "src/core/SkColorFilterBase.h"
 #include "src/core/SkColorSpacePriv.h"
 #include "src/core/SkRasterPipeline.h"
 #include "src/core/SkRasterPipelineOpContexts.h"
@@ -31,6 +30,7 @@
 #include "src/gpu/KeyBuilder.h"
 #include "src/gpu/ganesh/GrColorInfo.h"
 #include "src/gpu/ganesh/GrColorSpaceXform.h"
+#include "src/gpu/ganesh/GrFragmentProcessors.h"
 #include "src/gpu/ganesh/GrShaderVar.h"
 #include "src/gpu/ganesh/glsl/GrGLSLFragmentShaderBuilder.h"
 #include "src/gpu/ganesh/glsl/GrGLSLUniformHandler.h"
@@ -524,8 +524,8 @@
     }
     auto filter = SkOverdrawColorFilter::MakeWithSkColors(colors);
     SkSurfaceProps props; // default props for testing
-    auto [success, fp] = as_CFB(filter)->asFragmentProcessor(/*inputFP=*/nullptr, d->context(),
-                                                             GrColorInfo{}, props);
+    auto [success, fp] = GrFragmentProcessors::Make(
+            d->context(), filter.get(), /*inputFP=*/nullptr, GrColorInfo{}, props);
     SkASSERT(success);
     return std::move(fp);
 }
diff --git a/src/ports/SkGlobalInitialization_default.cpp b/src/ports/SkGlobalInitialization_default.cpp
index 63c1d2b..5aec459 100644
--- a/src/ports/SkGlobalInitialization_default.cpp
+++ b/src/ports/SkGlobalInitialization_default.cpp
@@ -78,7 +78,7 @@
         SkRegisterMatrixColorFilterFlattenable();
         SkRegisterComposeColorFilterFlattenable();
         SkRegisterModeColorFilterFlattenable();
-        SkRegisterColorSpaceXformColorFilterFlattenable();
+        SkRegisterSkColorSpaceXformColorFilterFlattenable();
         SkRegisterWorkingFormatColorFilterFlattenable();
         SkRegisterTableColorFilterFlattenable();
 
diff --git a/src/shaders/SkColorFilterShader.cpp b/src/shaders/SkColorFilterShader.cpp
index eee266c..cd8ad50 100644
--- a/src/shaders/SkColorFilterShader.cpp
+++ b/src/shaders/SkColorFilterShader.cpp
@@ -18,6 +18,7 @@
 #if defined(SK_GANESH)
 #include "src/gpu/ganesh/GrFPArgs.h"
 #include "src/gpu/ganesh/GrFragmentProcessor.h"
+#include "src/gpu/ganesh/GrFragmentProcessors.h"
 #endif
 
 #if defined(SK_GRAPHITE)
@@ -108,8 +109,11 @@
     // TODO I guess, but it shouldn't come up as used today.
     SkASSERT(fAlpha == 1.0f);
 
-    auto [success, fp] = fFilter->asFragmentProcessor(std::move(shaderFP), args.fContext,
-                                                      *args.fDstColorInfo, args.fSurfaceProps);
+    auto [success, fp] = GrFragmentProcessors::Make(args.fContext,
+                                                    fFilter.get(),
+                                                    std::move(shaderFP),
+                                                    *args.fDstColorInfo,
+                                                    args.fSurfaceProps);
     // If the filter FP could not be created, we still want to return the shader FP, so checking
     // success can be omitted here.
     return std::move(fp);
diff --git a/src/utils/BUILD.bazel b/src/utils/BUILD.bazel
index 6c6dc24..f63359a 100644
--- a/src/utils/BUILD.bazel
+++ b/src/utils/BUILD.bazel
@@ -38,6 +38,7 @@
     "SkFloatToDecimal.h",
     "SkFloatUtils.h",
     "SkGaussianColorFilter.cpp",
+    "SkGaussianColorFilter.h",
     "SkMatrix22.cpp",
     "SkMatrix22.h",
     "SkMultiPictureDocument.cpp",
diff --git a/src/utils/SkGaussianColorFilter.cpp b/src/utils/SkGaussianColorFilter.cpp
index 728ddfd..c80e599 100644
--- a/src/utils/SkGaussianColorFilter.cpp
+++ b/src/utils/SkGaussianColorFilter.cpp
@@ -5,6 +5,8 @@
  * found in the LICENSE file.
  */
 
+#include "src/utils/SkGaussianColorFilter.h"
+
 #include "include/core/SkColorFilter.h"
 #include "include/core/SkFlattenable.h"
 #include "include/core/SkRefCnt.h"
@@ -15,21 +17,6 @@
 #include "src/core/SkRasterPipeline.h"
 #include "src/core/SkRasterPipelineOpList.h"
 
-#if defined(SK_GANESH)
-#include "src/gpu/ganesh/GrFragmentProcessor.h"
-// This shouldn't be needed but IWYU needs both (identical) defs of GrFPResult.
-#include "src/shaders/SkShaderBase.h"
-#include <memory>
-#include <utility>
-
-class GrColorInfo;
-class GrRecordingContext;
-class SkSurfaceProps;
-#endif
-
-class SkReadBuffer;
-class SkWriteBuffer;
-
 #if defined(SK_GRAPHITE)
 #include "src/gpu/graphite/KeyContext.h"
 #include "src/gpu/graphite/KeyHelpers.h"
@@ -46,82 +33,36 @@
 class SkColorInfo;
 #endif
 
-/**
- * Remaps the input color's alpha to a Gaussian ramp and then outputs premul white using the
- * remapped alpha.
- */
-class SkGaussianColorFilter final : public SkColorFilterBase {
-public:
-    SkGaussianColorFilter() : SkColorFilterBase() {}
+SkGaussianColorFilter::SkGaussianColorFilter() : SkColorFilterBase() {}
 
-    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override {
-        rec.fPipeline->append(SkRasterPipelineOp::gauss_a_to_rgba);
-        return true;
-    }
-
-#if defined(SK_GANESH)
-    GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                   GrRecordingContext*,
-                                   const GrColorInfo&,
-                                   const SkSurfaceProps&) const override;
-#endif
-
-#if defined(SK_GRAPHITE)
-    void addToKey(const skgpu::graphite::KeyContext&,
-                  skgpu::graphite::PaintParamsKeyBuilder*,
-                  skgpu::graphite::PipelineDataGatherer*) const override;
-#endif
-
-protected:
-    void flatten(SkWriteBuffer&) const override {}
+bool SkGaussianColorFilter::appendStages(const SkStageRec& rec, bool shaderIsOpaque) const {
+    rec.fPipeline->append(SkRasterPipelineOp::gauss_a_to_rgba);
+    return true;
+}
 
 #if defined(SK_ENABLE_SKVM)
-    skvm::Color onProgram(skvm::Builder* p, skvm::Color c, const SkColorInfo& dst, skvm::Uniforms*,
-                          SkArenaAlloc*) const override {
-        // x = 1 - x;
-        // exp(-x * x * 4) - 0.018f;
-        // ... now approximate with quartic
-        //
-        skvm::F32 x = p->splat(-2.26661229133605957031f);
-        x = c.a * x + 2.89795351028442382812f;
-        x = c.a * x + 0.21345567703247070312f;
-        x = c.a * x + 0.15489584207534790039f;
-        x = c.a * x + 0.00030726194381713867f;
-        return {x, x, x, x};
-    }
+skvm::Color SkGaussianColorFilter::onProgram(skvm::Builder* p,
+                                             skvm::Color c,
+                                             const SkColorInfo& dst,
+                                             skvm::Uniforms*,
+                                             SkArenaAlloc*) const {
+    // x = 1 - x;
+    // exp(-x * x * 4) - 0.018f;
+    // ... now approximate with quartic
+    //
+    skvm::F32 x = p->splat(-2.26661229133605957031f);
+    x = c.a * x + 2.89795351028442382812f;
+    x = c.a * x + 0.21345567703247070312f;
+    x = c.a * x + 0.15489584207534790039f;
+    x = c.a * x + 0.00030726194381713867f;
+    return {x, x, x, x};
+}
 #endif
 
-private:
-    SK_FLATTENABLE_HOOKS(SkGaussianColorFilter)
-};
-
 sk_sp<SkFlattenable> SkGaussianColorFilter::CreateProc(SkReadBuffer&) {
     return SkColorFilterPriv::MakeGaussian();
 }
 
-#if defined(SK_GANESH)
-
-#include "include/effects/SkRuntimeEffect.h"
-#include "src/core/SkRuntimeEffectPriv.h"
-#include "src/gpu/ganesh/effects/GrSkSLFP.h"
-
-GrFPResult SkGaussianColorFilter::asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
-                                                      GrRecordingContext*,
-                                                      const GrColorInfo&,
-                                                      const SkSurfaceProps&) const {
-    static const SkRuntimeEffect* effect = SkMakeRuntimeEffect(SkRuntimeEffect::MakeForColorFilter,
-            "half4 main(half4 inColor) {"
-                "half factor = 1 - inColor.a;"
-                "factor = exp(-factor * factor * 4) - 0.018;"
-                "return half4(factor);"
-            "}"
-        );
-    SkASSERT(SkRuntimeEffectPriv::SupportsConstantOutputForConstantInput(effect));
-    return GrFPSuccess(GrSkSLFP::Make(effect, "gaussian_fp", std::move(inputFP),
-                                      GrSkSLFP::OptFlags::kNone));
-}
-#endif
-
 #if defined(SK_GRAPHITE)
 
 void SkGaussianColorFilter::addToKey(const skgpu::graphite::KeyContext& keyContext,
diff --git a/src/utils/SkGaussianColorFilter.h b/src/utils/SkGaussianColorFilter.h
new file mode 100644
index 0000000..2511763
--- /dev/null
+++ b/src/utils/SkGaussianColorFilter.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef SkGaussianColorFilter_DEFINED
+#define SkGaussianColorFilter_DEFINED
+
+#include "include/core/SkFlattenable.h"
+#include "include/core/SkTypes.h"
+#include "src/core/SkColorFilterBase.h"
+
+class SkReadBuffer;
+class SkWriteBuffer;
+struct SkStageRec;
+
+#if defined(SK_GRAPHITE)
+#include "src/gpu/graphite/KeyContext.h"
+#include "src/gpu/graphite/KeyHelpers.h"
+#include "src/gpu/graphite/PaintParamsKey.h"
+
+namespace skgpu::graphite {
+class PipelineDataGatherer;
+}
+#endif
+
+#if defined(SK_ENABLE_SKVM)
+#include "src/core/SkVM.h"
+class SkArenaAlloc;
+class SkColorInfo;
+#endif
+
+/**
+ * Remaps the input color's alpha to a Gaussian ramp and then outputs premul white using the
+ * remapped alpha.
+ */
+class SkGaussianColorFilter final : public SkColorFilterBase {
+public:
+    SkGaussianColorFilter();
+
+    bool appendStages(const SkStageRec& rec, bool shaderIsOpaque) const override;
+
+    SkColorFilterBase::Type type() const override { return SkColorFilterBase::Type::kGaussian; }
+
+#if defined(SK_GRAPHITE)
+    void addToKey(const skgpu::graphite::KeyContext&,
+                  skgpu::graphite::PaintParamsKeyBuilder*,
+                  skgpu::graphite::PipelineDataGatherer*) const override;
+#endif
+
+protected:
+    void flatten(SkWriteBuffer&) const override {}
+
+#if defined(SK_ENABLE_SKVM)
+    skvm::Color onProgram(skvm::Builder* p,
+                          skvm::Color c,
+                          const SkColorInfo& dst,
+                          skvm::Uniforms*,
+                          SkArenaAlloc*) const override;
+#endif
+
+private:
+    SK_FLATTENABLE_HOOKS(SkGaussianColorFilter)
+};
+
+#endif
diff --git a/tests/ColorFilterTest.cpp b/tests/ColorFilterTest.cpp
index 420bc14..d1710bf 100644
--- a/tests/ColorFilterTest.cpp
+++ b/tests/ColorFilterTest.cpp
@@ -163,6 +163,8 @@
     }
 #endif
 
+    SkColorFilterBase::Type type() const override { return SkColorFilterBase::Type::kNoop; }
+
     bool appendStages(const SkStageRec&, bool) const override { return false; }
 
     // Only created here, should never be flattened / unflattened.
diff --git a/toolchain/linux_trampolines/clang_trampoline_linux.sh b/toolchain/linux_trampolines/clang_trampoline_linux.sh
index 2207d9c..778a26e 100755
--- a/toolchain/linux_trampolines/clang_trampoline_linux.sh
+++ b/toolchain/linux_trampolines/clang_trampoline_linux.sh
@@ -49,8 +49,12 @@
   "src/core/SkCanvas.cpp"
   "src/core/SkCanvas_Raster.cpp"
   "src/core/SkColor.cpp"
+  "src/core/SkColorFilter.cpp"
   "src/core/SkColorSpace.cpp"
+  "src/core/SkColorSpaceXformColorFilter.cpp"
+  "src/core/SkComposeColorFilter.cpp"
   "src/core/SkCompressedDataUtils.cpp"
+  "src/core/SkConvertPixels.cpp"
   "src/core/SkCubicClipper.cpp"
   "src/core/SkCubicMap.cpp"
   "src/core/SkData.cpp"
@@ -63,12 +67,13 @@
   "src/core/SkGlyph.cpp"
   "src/core/SkGlyphRunPainter.cpp"
   "src/core/SkICC.cpp"
-  "src/core/SkImageInfo.cpp"
   "src/core/SkImageGenerator.cpp"
+  "src/core/SkImageInfo.cpp"
   "src/core/SkLineClipper.cpp"
   "src/core/SkMD5.cpp"
   "src/core/SkMaskFilter.cpp"
   "src/core/SkMatrix.cpp"
+  "src/core/SkMatrixColorFilter.cpp"
   "src/core/SkMipmapAccessor.cpp"
   "src/core/SkMipmapBuilder.cpp"
   "src/core/SkPaint.cpp"
@@ -78,9 +83,8 @@
   "src/core/SkPathUtils.cpp"
   "src/core/SkPictureData.cpp"
   "src/core/SkPicturePlayback.cpp"
-  "src/core/SkConvertPixels.cpp"
-  "src/core/SkPixmap.cpp"
   "src/core/SkPixelRef.cpp"
+  "src/core/SkPixmap.cpp"
   "src/core/SkPixmapDraw.cpp"
   "src/core/SkPoint.cpp"
   "src/core/SkRRect.cpp"
@@ -91,8 +95,9 @@
   "src/core/SkRuntime"
   "src/core/SkScalar.cpp"
   "src/core/SkStream.cpp"
-  "src/core/SkString.cpp"
   "src/core/SkStrike"
+  "src/core/SkString.cpp"
+  "src/core/SkWorkingFormatColorFilter.cpp"
   "src/core/SkWriteBuffer.cpp"
   "src/core/SkWritePixelsRec.cpp"
   "src/core/SkYUVAInfo.cpp"