rename the VM blitter to SkVMBlitter

Change-Id: I71ae924d32dc2268cc4a3db1920c08287533f9ae
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/430697
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Herb Derby <herb@google.com>
diff --git a/gn/core.gni b/gn/core.gni
index df6d672..55b5cea 100644
--- a/gn/core.gni
+++ b/gn/core.gni
@@ -406,6 +406,7 @@
   "$_src/core/SkVM.cpp",
   "$_src/core/SkVM.h",
   "$_src/core/SkVMBlitter.cpp",
+  "$_src/core/SkVMBlitter.h",
   "$_src/core/SkVM_fwd.h",
   "$_src/core/SkValidationUtils.h",
   "$_src/core/SkVertState.cpp",
diff --git a/src/core/SkBlitter.cpp b/src/core/SkBlitter.cpp
index 2ce47e4..f77f9cf 100644
--- a/src/core/SkBlitter.cpp
+++ b/src/core/SkBlitter.cpp
@@ -22,6 +22,7 @@
 #include "src/core/SkRegionPriv.h"
 #include "src/core/SkTLazy.h"
 #include "src/core/SkUtils.h"
+#include "src/core/SkVMBlitter.h"
 #include "src/core/SkWriteBuffer.h"
 #include "src/core/SkXfermodeInterpretation.h"
 #include "src/shaders/SkShaderBase.h"
@@ -748,8 +749,8 @@
     }
 
     if (gUseSkVMBlitter) {
-        if (auto blitter = SkCreateSkVMBlitter(device, *paint, matrixProvider,
-                                               alloc, clipShader)) {
+        if (auto blitter = SkVMBlitter::Make(device, *paint, matrixProvider,
+                                             alloc, clipShader)) {
             return blitter;
         }
     }
@@ -761,8 +762,8 @@
                                                          alloc, clipShader)) {
             return blitter;
         }
-        if (auto blitter = SkCreateSkVMBlitter(device, *paint, matrixProvider,
-                                               alloc, clipShader)) {
+        if (auto blitter = SkVMBlitter::Make(device, *paint, matrixProvider,
+                                             alloc, clipShader)) {
             return blitter;
         }
         return alloc->make<SkNullBlitter>();
diff --git a/src/core/SkBlitter_Sprite.cpp b/src/core/SkBlitter_Sprite.cpp
index 3f35d42..8c1b2ef 100644
--- a/src/core/SkBlitter_Sprite.cpp
+++ b/src/core/SkBlitter_Sprite.cpp
@@ -13,6 +13,7 @@
 #include "src/core/SkOpts.h"
 #include "src/core/SkRasterPipeline.h"
 #include "src/core/SkSpriteBlitter.h"
+#include "src/core/SkVMBlitter.h"
 
 extern bool gUseSkVMBlitter;
 
@@ -187,7 +188,7 @@
     SkASSERT(alloc != nullptr);
 
     if (gUseSkVMBlitter) {
-        return SkCreateSkVMSpriteBlitter(dst, paint, source,left,top, alloc, std::move(clipShader));
+        return SkVMBlitter::Make(dst, paint, source,left,top, alloc, std::move(clipShader));
     }
 
     // TODO: in principle SkRasterPipelineSpriteBlitter could be made to handle this.
@@ -225,5 +226,5 @@
         return blitter;
     }
 
-    return SkCreateSkVMSpriteBlitter(dst, paint, source,left,top, alloc, std::move(clipShader));
+    return SkVMBlitter::Make(dst, paint, source,left,top, alloc, std::move(clipShader));
 }
diff --git a/src/core/SkCoreBlitters.h b/src/core/SkCoreBlitters.h
index 51e73a8..92e4924 100644
--- a/src/core/SkCoreBlitters.h
+++ b/src/core/SkCoreBlitters.h
@@ -173,17 +173,4 @@
                                          bool shader_is_opaque,
                                          SkArenaAlloc*, sk_sp<SkShader> clipShader);
 
-SkBlitter* SkCreateSkVMBlitter(const SkPixmap& dst,
-                               const SkPaint&,
-                               const SkMatrixProvider&,
-                               SkArenaAlloc*,
-                               sk_sp<SkShader> clipShader);
-
-SkBlitter* SkCreateSkVMSpriteBlitter(const SkPixmap& dst,
-                                     const SkPaint&,
-                                     const SkPixmap& sprite,
-                                     int left, int top,
-                                     SkArenaAlloc*,
-                                     sk_sp<SkShader> clipShader);
-
 #endif
diff --git a/src/core/SkDraw_vertices.cpp b/src/core/SkDraw_vertices.cpp
index 14a5e88..d2d2191 100644
--- a/src/core/SkDraw_vertices.cpp
+++ b/src/core/SkDraw_vertices.cpp
@@ -17,6 +17,7 @@
 #include "src/core/SkRasterPipeline.h"
 #include "src/core/SkScan.h"
 #include "src/core/SkVM.h"
+#include "src/core/SkVMBlitter.h"
 #include "src/core/SkVertState.h"
 #include "src/core/SkVerticesPriv.h"
 #include "src/shaders/SkComposeShader.h"
@@ -308,7 +309,7 @@
     // No colors are changing and no texture coordinates are changing, so no updates between
     // triangles are needed. Use SkVM to blit the triangles.
     if (!colors && (!texCoords || texCoords == positions)) {
-        if (auto blitter = SkCreateSkVMBlitter(
+        if (auto blitter = SkVMBlitter::Make(
                 fDst, paint, *fMatrixProvider, outerAlloc, this->fRC->clipShader())) {
             while (vertProc(&state)) {
                 fill_triangle(state, blitter, *fRC, dev2, dev3);
diff --git a/src/core/SkVMBlitter.cpp b/src/core/SkVMBlitter.cpp
index 4821358..c388dbe 100644
--- a/src/core/SkVMBlitter.cpp
+++ b/src/core/SkVMBlitter.cpp
@@ -19,6 +19,7 @@
 #include "src/core/SkOpts.h"
 #include "src/core/SkPaintPriv.h"
 #include "src/core/SkVM.h"
+#include "src/core/SkVMBlitter.h"
 #include "src/shaders/SkColorFilterShader.h"
 
 #include <cinttypes>
@@ -34,83 +35,6 @@
     static_assert(SkIsAlign4(sizeof(BlitterUniforms)), "");
     static constexpr int kBlitterUniformsCount = sizeof(BlitterUniforms) / 4;
 
-    enum class Coverage { Full, UniformF, MaskA8, MaskLCD16, Mask3D };
-
-    struct Params {
-        sk_sp<SkShader>         shader;
-        sk_sp<SkShader>         clip;
-        sk_sp<SkBlender>        blender;    // never null
-        SkColorInfo             dst;
-        Coverage                coverage;
-        SkColor4f               paint;
-        const SkMatrixProvider& matrices;
-
-        Params withCoverage(Coverage c) const {
-            Params p = *this;
-            p.coverage = c;
-            return p;
-        }
-    };
-
-    SK_BEGIN_REQUIRE_DENSE;
-    struct Key {
-        uint64_t shader,
-                 clip,
-                 blender,
-                 colorSpace;
-        uint8_t  colorType,
-                 alphaType,
-                 coverage;
-        uint8_t  padding8{0};
-        uint32_t padding{0};
-        // Params::{paint,quality,matrices} are only passed to {shader,clip}->program(),
-        // not used here by the blitter itself.  No need to include them in the key;
-        // they'll be folded into the shader key if used.
-
-        bool operator==(const Key& that) const {
-            return this->shader      == that.shader
-                && this->clip        == that.clip
-                && this->blender     == that.blender
-                && this->colorSpace  == that.colorSpace
-                && this->colorType   == that.colorType
-                && this->alphaType   == that.alphaType
-                && this->coverage    == that.coverage;
-        }
-
-        Key withCoverage(Coverage c) const {
-            Key k = *this;
-            k.coverage = SkToU8(c);
-            return k;
-        }
-    };
-    SK_END_REQUIRE_DENSE;
-
-    static SkString debug_name(const Key& key) {
-        return SkStringPrintf("Shader-%" PRIx64 "_Clip-%" PRIx64 "_Blender-%" PRIx64
-                              "_CS-%" PRIx64 "_CT-%d_AT-%d_Cov-%d",
-                              key.shader,
-                              key.clip,
-                              key.blender,
-                              key.colorSpace,
-                              key.colorType,
-                              key.alphaType,
-                              key.coverage);
-    }
-
-    static SkLRUCache<Key, skvm::Program>* try_acquire_program_cache() {
-    #if 1 && defined(SKVM_JIT)
-        thread_local static SkLRUCache<Key, skvm::Program> cache{64};
-        return &cache;
-    #else
-        // iOS in particular does not support thread_local until iOS 9.0.
-        // On the other hand, we'll never be able to JIT there anyway.
-        // It's probably fine to not cache any interpreted programs, anywhere.
-        return nullptr;
-    #endif
-    }
-
-    static void release_program_cache() { }
-
     static skvm::Coord device_coord(skvm::Builder* p, skvm::Uniforms* uniforms) {
         skvm::I32 dx = p->uniform32(uniforms->base, offsetof(BlitterUniforms, right))
                      - p->index(),
@@ -121,267 +45,6 @@
         };
     }
 
-    static skvm::Color dst_color(skvm::Builder* p, const Params& params) {
-        skvm::PixelFormat dstFormat = skvm::SkColorType_to_PixelFormat(params.dst.colorType());
-        skvm::Ptr dst_ptr = p->arg(SkColorTypeBytesPerPixel(params.dst.colorType()));
-        return p->load(dstFormat, dst_ptr);
-    }
-
-    // If build_program() can't build this program, cache_key() sets *ok to false.
-    static Key cache_key(const Params& params,
-                         skvm::Uniforms* uniforms, SkArenaAlloc* alloc, bool* ok) {
-        // Take care to match build_program()'s reuse of the paint color uniforms.
-        skvm::Uniform r = uniforms->pushF(params.paint.fR),
-                      g = uniforms->pushF(params.paint.fG),
-                      b = uniforms->pushF(params.paint.fB),
-                      a = uniforms->pushF(params.paint.fA);
-
-        auto hash_shader = [&](skvm::Builder& p, const sk_sp<SkShader>& shader,
-                               skvm::Color* outColor) {
-            const SkShaderBase* sb = as_SB(shader);
-
-            skvm::Coord device = device_coord(&p, uniforms);
-            skvm::Color paint = {
-                p.uniformF(r),
-                p.uniformF(g),
-                p.uniformF(b),
-                p.uniformF(a),
-            };
-
-            uint64_t hash = 0;
-            *outColor = sb->program(&p, device, /*local=*/device, paint, params.matrices,
-                                    /*localM=*/nullptr, params.dst, uniforms, alloc);
-            if (*outColor) {
-                hash = p.hash();
-                // p.hash() folds in all instructions to produce r,g,b,a but does not know
-                // precisely which value we'll treat as which channel.  Imagine the shader
-                // called std::swap(*r,*b)... it draws differently, but p.hash() is unchanged.
-                // We'll fold the hash of their IDs in order to disambiguate.
-                const skvm::Val outputs[] = {
-                    outColor->r.id,
-                    outColor->g.id,
-                    outColor->b.id,
-                    outColor->a.id
-                };
-                hash ^= SkOpts::hash(outputs, sizeof(outputs));
-            } else {
-                *ok = false;
-            }
-            return hash;
-        };
-
-        // Use this builder for shader, clip and blender, so that color objects that pass
-        // from one to the other all 'make sense' -- i.e. have the same builder and/or have
-        // meaningful values for the hash.
-        //
-        // Question: better if we just pass in mock uniform colors, so we don't need to
-        //           explicitly use the output color from one stage as input to another?
-        //
-        skvm::Builder p;
-
-        // Calculate a hash for the color shader.
-        SkASSERT(params.shader);
-        skvm::Color src;
-        uint64_t shaderHash = hash_shader(p, params.shader, &src);
-
-        // Calculate a hash for the clip shader, if one exists.
-        uint64_t clipHash = 0;
-        if (params.clip) {
-            skvm::Color cov;
-            clipHash = hash_shader(p, params.clip, &cov);
-            if (clipHash == 0) {
-                clipHash = 1;
-            }
-        }
-
-        // Calculate a hash for the blender.
-        uint64_t blendHash = 0;
-        if (auto bm = as_BB(params.blender)->asBlendMode()) {
-            blendHash = static_cast<uint8_t>(bm.value());
-        } else if (*ok) {
-            const SkBlenderBase* blender = as_BB(params.blender);
-
-            skvm::Color dst = dst_color(&p, params);
-            skvm::Color outColor = blender->program(&p, src, dst, params.dst, uniforms, alloc);
-            if (outColor) {
-                blendHash = p.hash();
-                // Like in `hash_shader` above, we must fold the color component IDs into our hash.
-                const skvm::Val outputs[] = {
-                    outColor.r.id,
-                    outColor.g.id,
-                    outColor.b.id,
-                    outColor.a.id
-                };
-                blendHash ^= SkOpts::hash(outputs, sizeof(outputs));
-            } else {
-                *ok = false;
-            }
-            if (blendHash == 0) {
-                blendHash = 1;
-            }
-        }
-
-        return {
-            shaderHash,
-              clipHash,
-             blendHash,
-            params.dst.colorSpace() ? params.dst.colorSpace()->hash() : 0,
-            SkToU8(params.dst.colorType()),
-            SkToU8(params.dst.alphaType()),
-            SkToU8(params.coverage),
-        };
-    }
-
-    static void build_program(skvm::Builder* p, const Params& params,
-                              skvm::Uniforms* uniforms, SkArenaAlloc* alloc) {
-        // First two arguments are always uniforms and the destination buffer.
-        uniforms->base    = p->uniform();
-        skvm::Ptr dst_ptr = p->arg(SkColorTypeBytesPerPixel(params.dst.colorType()));
-        // A SpriteShader (in this file) may next use one argument as its varying source.
-        // Subsequent arguments depend on params.coverage:
-        //    - Full:      (no more arguments)
-        //    - Mask3D:    mul varying, add varying, 8-bit coverage varying
-        //    - MaskA8:    8-bit coverage varying
-        //    - MaskLCD16: 565 coverage varying
-        //    - UniformF:  float coverage uniform
-
-        skvm::Coord device = device_coord(p, uniforms);
-        skvm::Color paint = p->uniformColor(params.paint, uniforms);
-
-        // See note about arguments above: a SpriteShader will call p->arg() once during program().
-        skvm::Color src = as_SB(params.shader)->program(p, device, /*local=*/device, paint,
-                                                        params.matrices, /*localM=*/nullptr,
-                                                        params.dst, uniforms, alloc);
-        SkASSERT(src);
-        if (params.coverage == Coverage::Mask3D) {
-            skvm::F32 M = from_unorm(8, p->load8(p->varying<uint8_t>())),
-                      A = from_unorm(8, p->load8(p->varying<uint8_t>()));
-
-            src.r = min(src.r * M + A, src.a);
-            src.g = min(src.g * M + A, src.a);
-            src.b = min(src.b * M + A, src.a);
-        }
-
-        // If we can determine this we can skip a fair bit of clamping!
-        bool src_in_gamut = false;
-
-        // Normalized premul formats can surprisingly represent some out-of-gamut
-        // values (e.g. r=0xff, a=0xee fits in unorm8 but r = 1.07), but most code
-        // working with normalized premul colors is not prepared to handle r,g,b > a.
-        // So we clamp the shader to gamut here before blending and coverage.
-        //
-        // In addition, GL clamps all its color channels to limits of the format just
-        // before the blend step (~here).  To match that auto-clamp, we clamp alpha to
-        // [0,1] too, just in case someone gave us a crazy alpha.
-        if (!src_in_gamut
-                && params.dst.alphaType() == kPremul_SkAlphaType
-                && SkColorTypeIsNormalized(params.dst.colorType())) {
-            src.a = clamp(src.a, 0.0f,  1.0f);
-            src.r = clamp(src.r, 0.0f, src.a);
-            src.g = clamp(src.g, 0.0f, src.a);
-            src.b = clamp(src.b, 0.0f, src.a);
-            src_in_gamut = true;
-        }
-
-        // Load the destination color.
-        skvm::PixelFormat dstFormat = skvm::SkColorType_to_PixelFormat(params.dst.colorType());
-        skvm::Color dst = p->load(dstFormat, dst_ptr);
-        if (params.dst.isOpaque()) {
-            // When a destination is known opaque, we may assume it both starts and stays fully
-            // opaque, ignoring any math that disagrees.  This sometimes trims a little work.
-            dst.a = p->splat(1.0f);
-        } else if (params.dst.alphaType() == kUnpremul_SkAlphaType) {
-            // All our blending works in terms of premul.
-            dst = premul(dst);
-        }
-
-        // Load coverage.
-        skvm::Color cov;
-        switch (params.coverage) {
-            case Coverage::Full:
-                cov.r = cov.g = cov.b = cov.a = p->splat(1.0f);
-                break;
-
-            case Coverage::UniformF:
-                cov.r = cov.g = cov.b = cov.a = p->uniformF(p->uniform(), 0);
-                break;
-
-            case Coverage::Mask3D:
-            case Coverage::MaskA8:
-                cov.r = cov.g = cov.b = cov.a = from_unorm(8, p->load8(p->varying<uint8_t>()));
-                break;
-
-            case Coverage::MaskLCD16: {
-                skvm::PixelFormat fmt = skvm::SkColorType_to_PixelFormat(kRGB_565_SkColorType);
-                cov = p->load(fmt, p->varying<uint16_t>());
-                cov.a = select(src.a < dst.a, min(cov.r, min(cov.g, cov.b))
-                                            , max(cov.r, max(cov.g, cov.b)));
-            } break;
-        }
-        if (params.clip) {
-            skvm::Color clip = as_SB(params.clip)->program(p, device, /*local=*/device, paint,
-                                                           params.matrices, /*localM=*/nullptr,
-                                                           params.dst, uniforms, alloc);
-            SkAssertResult(clip);
-            cov.r *= clip.a;  // We use the alpha channel of clip for all four.
-            cov.g *= clip.a;
-            cov.b *= clip.a;
-            cov.a *= clip.a;
-        }
-
-        const SkBlenderBase* blender = as_BB(params.blender);
-        const auto as_blendmode = blender->asBlendMode();
-
-        // The math for some blend modes lets us fold coverage into src before the blend, which is
-        // simpler than the canonical post-blend lerp().
-        bool applyPostBlendCoverage = true;
-        if (as_blendmode &&
-            SkBlendMode_ShouldPreScaleCoverage(as_blendmode.value(),
-                                               params.coverage == Coverage::MaskLCD16)) {
-            applyPostBlendCoverage = false;
-            src.r *= cov.r;
-            src.g *= cov.g;
-            src.b *= cov.b;
-            src.a *= cov.a;
-        }
-
-        // Apply our blend function to the computed color.
-        src = blender->program(p, src, dst, params.dst, uniforms, alloc);
-
-        if (applyPostBlendCoverage) {
-            src.r = lerp(dst.r, src.r, cov.r);
-            src.g = lerp(dst.g, src.g, cov.g);
-            src.b = lerp(dst.b, src.b, cov.b);
-            src.a = lerp(dst.a, src.a, cov.a);
-        }
-
-        if (params.dst.isOpaque()) {
-            // (See the note above when loading the destination color.)
-            src.a = p->splat(1.0f);
-        } else if (params.dst.alphaType() == kUnpremul_SkAlphaType) {
-            src = unpremul(src);
-        }
-
-        // Clamp to fit destination color format if needed.
-        if (as_blendmode && src_in_gamut) {
-            // An in-gamut src blended with an in-gamut dst should stay in gamut.
-            // Being in-gamut implies all channels are in [0,1], so no need to clamp.
-            // We allow one ulp error above 1.0f, and about that much (~1.2e-7) below 0.
-            skvm::F32 lo = pun_to_F32(p->splat(0xb400'0000)),
-                      hi = pun_to_F32(p->splat(0x3f80'0001));
-            assert_true(src.r == clamp(src.r, lo, hi), src.r);
-            assert_true(src.g == clamp(src.g, lo, hi), src.g);
-            assert_true(src.b == clamp(src.b, lo, hi), src.b);
-            assert_true(src.a == clamp(src.a, lo, hi), src.a);
-        } else if (SkColorTypeIsNormalized(params.dst.colorType())) {
-            src = clamp01(src);
-        }
-
-        // Write it out!
-        store(dstFormat, dst_ptr, src);
-    }
-
-
     struct NoopColorFilter : public SkColorFilterBase {
         skvm::Color onProgram(skvm::Builder*, skvm::Color c,
                               const SkColorInfo&, skvm::Uniforms*, SkArenaAlloc*) const override {
@@ -528,302 +191,592 @@
             return premul(paint);
         }
     };
-
-    static Params effective_params(const SkPixmap& device,
-                                   const SkPixmap* sprite,
-                                   SkPaint paint,
-                                   const SkMatrixProvider& matrices,
-                                   sk_sp<SkShader> clip) {
-        // Sprites take priority over any shader.  (There's rarely one set, and it's meaningless.)
-        if (sprite) {
-            paint.setShader(sk_make_sp<SpriteShader>(*sprite));
-        }
-
-        // Normal blitters will have already folded color filters into their shader,
-        // but we may still need to do that here for SpriteShaders.
-        if (paint.getColorFilter()) {
-            SkPaintPriv::RemoveColorFilter(&paint, device.colorSpace());
-        }
-        SkASSERT(!paint.getColorFilter());
-
-        // If there's no explicit shader, the paint color is the shader,
-        // but if there is a shader, it's modulated by the paint alpha.
-        sk_sp<SkShader> shader = paint.refShader();
-        if (!shader) {
-            shader = sk_make_sp<PaintColorShader>(paint.getColor4f().isOpaque());
-        } else if (paint.getAlphaf() < 1.0f) {
-            shader = sk_make_sp<SkColorFilterShader>(std::move(shader),
-                                                     paint.getAlphaf(),
-                                                     sk_make_sp<NoopColorFilter>());
-        }
-
-        // Add dither to the end of the shader pipeline if requested and needed.
-        if (paint.isDither() && !as_SB(shader)->isConstant()) {
-            shader = sk_make_sp<DitherShader>(std::move(shader));
-        }
-
-        // Add the blender.
-        sk_sp<SkBlender> blender = paint.refBlender();
-        if (!blender) {
-            blender = SkBlender::Mode(SkBlendMode::kSrcOver);
-        }
-
-        // The most common blend mode is SrcOver, and it can be strength-reduced
-        // _greatly_ to Src mode when the shader is opaque.
-        //
-        // In general all the information we use to make decisions here need to
-        // be reflected in Params and Key to make program caching sound, and it
-        // might appear that shader->isOpaque() is a property of the shader's
-        // uniforms than its fundamental program structure and so unsafe to use.
-        //
-        // Opacity is such a powerful property that SkShaderBase::program()
-        // forces opacity for any shader subclass that claims isOpaque(), so
-        // the opaque bit is strongly guaranteed to be part of the program and
-        // not just a property of the uniforms.  The shader program hash includes
-        // this information, making it safe to use anywhere in the blitter codegen.
-        if (as_BB(blender)->asBlendMode() == SkBlendMode::kSrcOver && shader->isOpaque()) {
-            blender = SkBlender::Mode(SkBlendMode::kSrc);
-        }
-
-        SkColor4f paintColor = paint.getColor4f();
-        SkColorSpaceXformSteps{sk_srgb_singleton(), kUnpremul_SkAlphaType,
-                               device.colorSpace(), kUnpremul_SkAlphaType}
-            .apply(paintColor.vec());
-
-        return {
-            std::move(shader),
-            std::move(clip),
-            std::move(blender),
-            { device.colorType(), device.alphaType(), device.refColorSpace() },
-            Coverage::Full,  // Placeholder... withCoverage() will change as needed.
-            paintColor,
-            matrices,
-        };
-    }
-
-    class Blitter final : public SkBlitter {
-    public:
-        Blitter(const SkPixmap&         device,
-                const SkPaint&          paint,
-                const SkPixmap*         sprite,
-                SkIPoint                spriteOffset,
-                const SkMatrixProvider& matrices,
-                sk_sp<SkShader>         clip,
-                bool* ok)
-            : fDevice(device)
-            , fSprite(sprite ? *sprite : SkPixmap{})
-            , fSpriteOffset(spriteOffset)
-            , fUniforms(skvm::Ptr{0}, kBlitterUniformsCount)
-            , fParams(effective_params(device, sprite, paint, matrices, std::move(clip)))
-            , fKey(cache_key(fParams, &fUniforms, &fAlloc, ok))
-        {}
-
-        ~Blitter() override {
-            if (SkLRUCache<Key, skvm::Program>* cache = try_acquire_program_cache()) {
-                auto cache_program = [&](skvm::Program&& program, Coverage coverage) {
-                    if (!program.empty()) {
-                        cache->insert_or_update(fKey.withCoverage(coverage), std::move(program));
-                    }
-                };
-                cache_program(std::move(fBlitH),         Coverage::Full);
-                cache_program(std::move(fBlitAntiH),     Coverage::UniformF);
-                cache_program(std::move(fBlitMaskA8),    Coverage::MaskA8);
-                cache_program(std::move(fBlitMask3D),    Coverage::Mask3D);
-                cache_program(std::move(fBlitMaskLCD16), Coverage::MaskLCD16);
-
-                release_program_cache();
-            }
-        }
-
-    private:
-        SkPixmap        fDevice;
-        const SkPixmap  fSprite;                  // See isSprite().
-        const SkIPoint  fSpriteOffset;
-        skvm::Uniforms  fUniforms;                // Most data is copied directly into fUniforms,
-        SkArenaAlloc    fAlloc{2*sizeof(void*)};  // but a few effects need to ref large content.
-        const Params    fParams;
-        const Key       fKey;
-        skvm::Program   fBlitH,
-                        fBlitAntiH,
-                        fBlitMaskA8,
-                        fBlitMask3D,
-                        fBlitMaskLCD16;
-
-        skvm::Program buildProgram(Coverage coverage) {
-            Key key = fKey.withCoverage(coverage);
-            {
-                skvm::Program p;
-                if (SkLRUCache<Key, skvm::Program>* cache = try_acquire_program_cache()) {
-                    if (skvm::Program* found = cache->find(key)) {
-                        p = std::move(*found);
-                    }
-                    release_program_cache();
-                }
-                if (!p.empty()) {
-                    return p;
-                }
-            }
-            // We don't really _need_ to rebuild fUniforms here.
-            // It's just more natural to have effects unconditionally emit them,
-            // and more natural to rebuild fUniforms than to emit them into a temporary buffer.
-            // fUniforms should reuse the exact same memory, so this is very cheap.
-            SkDEBUGCODE(size_t prev = fUniforms.buf.size();)
-            fUniforms.buf.resize(kBlitterUniformsCount);
-            skvm::Builder builder;
-            build_program(&builder, fParams.withCoverage(coverage), &fUniforms, &fAlloc);
-            SkASSERTF(fUniforms.buf.size() == prev,
-                      "%zu, prev was %zu", fUniforms.buf.size(), prev);
-
-            skvm::Program program = builder.done(debug_name(key).c_str());
-            if (false) {
-                static std::atomic<int> missed{0},
-                                         total{0};
-                if (!program.hasJIT()) {
-                    SkDebugf("\ncouldn't JIT %s\n", debug_name(key).c_str());
-                    builder.dump();
-                    program.dump();
-
-                    missed++;
-                }
-                if (0 == total++) {
-                    atexit([]{ SkDebugf("SkVMBlitter compiled %d programs, %d without JIT.\n",
-                                        total.load(), missed.load()); });
-                }
-            }
-            return program;
-        }
-
-        void updateUniforms(int right, int y) {
-            BlitterUniforms uniforms{right, y};
-            memcpy(fUniforms.buf.data(), &uniforms, sizeof(BlitterUniforms));
-        }
-
-        const void* isSprite(int x, int y) const {
-            if (fSprite.colorType() != kUnknown_SkColorType) {
-                return fSprite.addr(x - fSpriteOffset.x(),
-                                    y - fSpriteOffset.y());
-            }
-            return nullptr;
-        }
-
-        void blitH(int x, int y, int w) override {
-            if (fBlitH.empty()) {
-                fBlitH = this->buildProgram(Coverage::Full);
-            }
-            this->updateUniforms(x+w, y);
-            if (const void* sprite = this->isSprite(x,y)) {
-                fBlitH.eval(w, fUniforms.buf.data(), fDevice.addr(x,y), sprite);
-            } else {
-                fBlitH.eval(w, fUniforms.buf.data(), fDevice.addr(x,y));
-            }
-        }
-
-        void blitAntiH(int x, int y, const SkAlpha cov[], const int16_t runs[]) override {
-            if (fBlitAntiH.empty()) {
-                fBlitAntiH = this->buildProgram(Coverage::UniformF);
-            }
-            for (int16_t run = *runs; run > 0; run = *runs) {
-                this->updateUniforms(x+run, y);
-                const float covF = *cov * (1/255.0f);
-                if (const void* sprite = this->isSprite(x,y)) {
-                    fBlitAntiH.eval(run, fUniforms.buf.data(), fDevice.addr(x,y), sprite, &covF);
-                } else {
-                    fBlitAntiH.eval(run, fUniforms.buf.data(), fDevice.addr(x,y), &covF);
-                }
-                x    += run;
-                runs += run;
-                cov  += run;
-            }
-        }
-
-        void blitMask(const SkMask& mask, const SkIRect& clip) override {
-            if (mask.fFormat == SkMask::kBW_Format) {
-                return SkBlitter::blitMask(mask, clip);
-            }
-
-            const skvm::Program* program = nullptr;
-            switch (mask.fFormat) {
-                default: SkUNREACHABLE;     // ARGB and SDF masks shouldn't make it here.
-
-                case SkMask::k3D_Format:
-                    if (fBlitMask3D.empty()) {
-                        fBlitMask3D = this->buildProgram(Coverage::Mask3D);
-                    }
-                    program = &fBlitMask3D;
-                    break;
-
-                case SkMask::kA8_Format:
-                    if (fBlitMaskA8.empty()) {
-                        fBlitMaskA8 = this->buildProgram(Coverage::MaskA8);
-                    }
-                    program = &fBlitMaskA8;
-                    break;
-
-                case SkMask::kLCD16_Format:
-                    if (fBlitMaskLCD16.empty()) {
-                        fBlitMaskLCD16 = this->buildProgram(Coverage::MaskLCD16);
-                    }
-                    program = &fBlitMaskLCD16;
-                    break;
-            }
-
-            SkASSERT(program);
-            if (program) {
-                for (int y = clip.top(); y < clip.bottom(); y++) {
-                    int x = clip.left(),
-                        w = clip.width();
-                    void* dptr =        fDevice.writable_addr(x,y);
-                    auto  mptr = (const uint8_t*)mask.getAddr(x,y);
-                    this->updateUniforms(x+w,y);
-
-                    if (program == &fBlitMask3D) {
-                        size_t plane = mask.computeImageSize();
-                        if (const void* sprite = this->isSprite(x,y)) {
-                            program->eval(w, fUniforms.buf.data(), dptr, sprite, mptr + 1*plane
-                                                                               , mptr + 2*plane
-                                                                               , mptr + 0*plane);
-                        } else {
-                            program->eval(w, fUniforms.buf.data(), dptr, mptr + 1*plane
-                                                                       , mptr + 2*plane
-                                                                       , mptr + 0*plane);
-                        }
-                    } else {
-                        if (const void* sprite = this->isSprite(x,y)) {
-                            program->eval(w, fUniforms.buf.data(), dptr, sprite, mptr);
-                        } else {
-                            program->eval(w, fUniforms.buf.data(), dptr, mptr);
-                        }
-                    }
-                }
-            }
-        }
-    };
-
 }  // namespace
 
-SkBlitter* SkCreateSkVMBlitter(const SkPixmap& device,
+bool SkVMBlitter::Key::operator==(const Key& that) const {
+    return this->shader      == that.shader
+        && this->clip        == that.clip
+        && this->blender     == that.blender
+        && this->colorSpace  == that.colorSpace
+        && this->colorType   == that.colorType
+        && this->alphaType   == that.alphaType
+        && this->coverage    == that.coverage;
+}
+
+SkVMBlitter::Key SkVMBlitter::Key::withCoverage(Coverage c) const {
+    Key k = *this;
+    k.coverage = SkToU8(c);
+    return k;
+}
+
+SkVMBlitter::Params SkVMBlitter::Params::withCoverage(Coverage c) const {
+    Params p = *this;
+    p.coverage = c;
+    return p;
+}
+
+SkVMBlitter::Params SkVMBlitter::EffectiveParams(const SkPixmap& device,
+                                                 const SkPixmap* sprite,
+                                                 SkPaint paint,
+                                                 const SkMatrixProvider& matrices,
+                                                 sk_sp<SkShader> clip) {
+    // Sprites take priority over any shader.  (There's rarely one set, and it's meaningless.)
+    if (sprite) {
+        paint.setShader(sk_make_sp<SpriteShader>(*sprite));
+    }
+
+    // Normal blitters will have already folded color filters into their shader,
+    // but we may still need to do that here for SpriteShaders.
+    if (paint.getColorFilter()) {
+        SkPaintPriv::RemoveColorFilter(&paint, device.colorSpace());
+    }
+    SkASSERT(!paint.getColorFilter());
+
+    // If there's no explicit shader, the paint color is the shader,
+    // but if there is a shader, it's modulated by the paint alpha.
+    sk_sp<SkShader> shader = paint.refShader();
+    if (!shader) {
+        shader = sk_make_sp<PaintColorShader>(paint.getColor4f().isOpaque());
+    } else if (paint.getAlphaf() < 1.0f) {
+        shader = sk_make_sp<SkColorFilterShader>(std::move(shader),
+                                                 paint.getAlphaf(),
+                                                 sk_make_sp<NoopColorFilter>());
+    }
+
+    // Add dither to the end of the shader pipeline if requested and needed.
+    if (paint.isDither() && !as_SB(shader)->isConstant()) {
+        shader = sk_make_sp<DitherShader>(std::move(shader));
+    }
+
+    // Add the blender.
+    sk_sp<SkBlender> blender = paint.refBlender();
+    if (!blender) {
+        blender = SkBlender::Mode(SkBlendMode::kSrcOver);
+    }
+
+    // The most common blend mode is SrcOver, and it can be strength-reduced
+    // _greatly_ to Src mode when the shader is opaque.
+    //
+    // In general all the information we use to make decisions here need to
+    // be reflected in Params and Key to make program caching sound, and it
+    // might appear that shader->isOpaque() is a property of the shader's
+    // uniforms than its fundamental program structure and so unsafe to use.
+    //
+    // Opacity is such a powerful property that SkShaderBase::program()
+    // forces opacity for any shader subclass that claims isOpaque(), so
+    // the opaque bit is strongly guaranteed to be part of the program and
+    // not just a property of the uniforms.  The shader program hash includes
+    // this information, making it safe to use anywhere in the blitter codegen.
+    if (as_BB(blender)->asBlendMode() == SkBlendMode::kSrcOver && shader->isOpaque()) {
+        blender = SkBlender::Mode(SkBlendMode::kSrc);
+    }
+
+    SkColor4f paintColor = paint.getColor4f();
+    SkColorSpaceXformSteps{sk_srgb_singleton(), kUnpremul_SkAlphaType,
+                           device.colorSpace(), kUnpremul_SkAlphaType}
+            .apply(paintColor.vec());
+
+    return {
+        std::move(shader),
+        std::move(clip),
+        std::move(blender),
+        { device.colorType(), device.alphaType(), device.refColorSpace() },
+        Coverage::Full,  // Placeholder... withCoverage() will change as needed.
+        paintColor,
+        matrices,
+    };
+}
+
+skvm::Color SkVMBlitter::DstColor(skvm::Builder* p, const Params& params) {
+    skvm::PixelFormat dstFormat = skvm::SkColorType_to_PixelFormat(params.dst.colorType());
+    skvm::Ptr dst_ptr = p->arg(SkColorTypeBytesPerPixel(params.dst.colorType()));
+    return p->load(dstFormat, dst_ptr);
+}
+
+void SkVMBlitter::BuildProgram(skvm::Builder* p, const Params& params,
+                               skvm::Uniforms* uniforms, SkArenaAlloc* alloc) {
+    // First two arguments are always uniforms and the destination buffer.
+    uniforms->base    = p->uniform();
+    skvm::Ptr dst_ptr = p->arg(SkColorTypeBytesPerPixel(params.dst.colorType()));
+    // A SpriteShader (in this file) may next use one argument as its varying source.
+    // Subsequent arguments depend on params.coverage:
+    //    - Full:      (no more arguments)
+    //    - Mask3D:    mul varying, add varying, 8-bit coverage varying
+    //    - MaskA8:    8-bit coverage varying
+    //    - MaskLCD16: 565 coverage varying
+    //    - UniformF:  float coverage uniform
+
+    skvm::Coord device = device_coord(p, uniforms);
+    skvm::Color paint = p->uniformColor(params.paint, uniforms);
+
+    // See note about arguments above: a SpriteShader will call p->arg() once during program().
+    skvm::Color src = as_SB(params.shader)->program(p, device, /*local=*/device, paint,
+                                                    params.matrices, /*localM=*/nullptr,
+                                                    params.dst, uniforms, alloc);
+    SkASSERT(src);
+    if (params.coverage == Coverage::Mask3D) {
+        skvm::F32 M = from_unorm(8, p->load8(p->varying<uint8_t>())),
+                  A = from_unorm(8, p->load8(p->varying<uint8_t>()));
+
+        src.r = min(src.r * M + A, src.a);
+        src.g = min(src.g * M + A, src.a);
+        src.b = min(src.b * M + A, src.a);
+    }
+
+    // If we can determine this we can skip a fair bit of clamping!
+    bool src_in_gamut = false;
+
+    // Normalized premul formats can surprisingly represent some out-of-gamut
+    // values (e.g. r=0xff, a=0xee fits in unorm8 but r = 1.07), but most code
+    // working with normalized premul colors is not prepared to handle r,g,b > a.
+    // So we clamp the shader to gamut here before blending and coverage.
+    //
+    // In addition, GL clamps all its color channels to limits of the format just
+    // before the blend step (~here).  To match that auto-clamp, we clamp alpha to
+    // [0,1] too, just in case someone gave us an out of range alpha.
+    if (!src_in_gamut
+            && params.dst.alphaType() == kPremul_SkAlphaType
+            && SkColorTypeIsNormalized(params.dst.colorType())) {
+        src.a = clamp(src.a, 0.0f,  1.0f);
+        src.r = clamp(src.r, 0.0f, src.a);
+        src.g = clamp(src.g, 0.0f, src.a);
+        src.b = clamp(src.b, 0.0f, src.a);
+        src_in_gamut = true;
+    }
+
+    // Load the destination color.
+    skvm::PixelFormat dstFormat = skvm::SkColorType_to_PixelFormat(params.dst.colorType());
+    skvm::Color dst = p->load(dstFormat, dst_ptr);
+    if (params.dst.isOpaque()) {
+        // When a destination is known opaque, we may assume it both starts and stays fully
+        // opaque, ignoring any math that disagrees.  This sometimes trims a little work.
+        dst.a = p->splat(1.0f);
+    } else if (params.dst.alphaType() == kUnpremul_SkAlphaType) {
+        // All our blending works in terms of premul.
+        dst = premul(dst);
+    }
+
+    // Load coverage.
+    skvm::Color cov;
+    switch (params.coverage) {
+        case Coverage::Full:
+            cov.r = cov.g = cov.b = cov.a = p->splat(1.0f);
+            break;
+
+        case Coverage::UniformF:
+            cov.r = cov.g = cov.b = cov.a = p->uniformF(p->uniform(), 0);
+            break;
+
+        case Coverage::Mask3D:
+        case Coverage::MaskA8:
+            cov.r = cov.g = cov.b = cov.a = from_unorm(8, p->load8(p->varying<uint8_t>()));
+            break;
+
+        case Coverage::MaskLCD16: {
+            skvm::PixelFormat fmt = skvm::SkColorType_to_PixelFormat(kRGB_565_SkColorType);
+            cov = p->load(fmt, p->varying<uint16_t>());
+            cov.a = select(src.a < dst.a, min(cov.r, min(cov.g, cov.b)),
+                           max(cov.r, max(cov.g, cov.b)));
+        } break;
+    }
+    if (params.clip) {
+        skvm::Color clip = as_SB(params.clip)->program(p, device, /*local=*/device, paint,
+                                                       params.matrices, /*localM=*/nullptr,
+                                                       params.dst, uniforms, alloc);
+        SkAssertResult(clip);
+        cov.r *= clip.a;  // We use the alpha channel of clip for all four.
+        cov.g *= clip.a;
+        cov.b *= clip.a;
+        cov.a *= clip.a;
+    }
+
+    const SkBlenderBase* blender = as_BB(params.blender);
+    const auto as_blendmode = blender->asBlendMode();
+
+    // The math for some blend modes lets us fold coverage into src before the blend, which is
+    // simpler than the canonical post-blend lerp().
+    bool applyPostBlendCoverage = true;
+    if (as_blendmode &&
+        SkBlendMode_ShouldPreScaleCoverage(as_blendmode.value(),
+                                           params.coverage == Coverage::MaskLCD16)) {
+        applyPostBlendCoverage = false;
+        src.r *= cov.r;
+        src.g *= cov.g;
+        src.b *= cov.b;
+        src.a *= cov.a;
+    }
+
+    // Apply our blend function to the computed color.
+    src = blender->program(p, src, dst, params.dst, uniforms, alloc);
+
+    if (applyPostBlendCoverage) {
+        src.r = lerp(dst.r, src.r, cov.r);
+        src.g = lerp(dst.g, src.g, cov.g);
+        src.b = lerp(dst.b, src.b, cov.b);
+        src.a = lerp(dst.a, src.a, cov.a);
+    }
+
+    if (params.dst.isOpaque()) {
+        // (See the note above when loading the destination color.)
+        src.a = p->splat(1.0f);
+    } else if (params.dst.alphaType() == kUnpremul_SkAlphaType) {
+        src = unpremul(src);
+    }
+
+    // Clamp to fit destination color format if needed.
+    if (as_blendmode && src_in_gamut) {
+        // An in-gamut src blended with an in-gamut dst should stay in gamut.
+        // Being in-gamut implies all channels are in [0,1], so no need to clamp.
+        // We allow one ulp error above 1.0f, and about that much (~1.2e-7) below 0.
+        skvm::F32 lo = pun_to_F32(p->splat(0xb400'0000)),
+                  hi = pun_to_F32(p->splat(0x3f80'0001));
+        assert_true(src.r == clamp(src.r, lo, hi), src.r);
+        assert_true(src.g == clamp(src.g, lo, hi), src.g);
+        assert_true(src.b == clamp(src.b, lo, hi), src.b);
+        assert_true(src.a == clamp(src.a, lo, hi), src.a);
+    } else if (SkColorTypeIsNormalized(params.dst.colorType())) {
+        src = clamp01(src);
+    }
+
+    // Write it out!
+    store(dstFormat, dst_ptr, src);
+}
+
+// If BuildProgram() can't build this program, CacheKey() sets *ok to false.
+SkVMBlitter::Key SkVMBlitter::CacheKey(
+        const Params& params, skvm::Uniforms* uniforms, SkArenaAlloc* alloc, bool* ok) {
+    // Take care to match buildProgram()'s reuse of the paint color uniforms.
+    skvm::Uniform r = uniforms->pushF(params.paint.fR),
+                  g = uniforms->pushF(params.paint.fG),
+                  b = uniforms->pushF(params.paint.fB),
+                  a = uniforms->pushF(params.paint.fA);
+
+    auto hash_shader = [&](skvm::Builder& p, const sk_sp<SkShader>& shader,
+                           skvm::Color* outColor) {
+        const SkShaderBase* sb = as_SB(shader);
+
+        skvm::Coord device = device_coord(&p, uniforms);
+        skvm::Color paint = {
+            p.uniformF(r),
+            p.uniformF(g),
+            p.uniformF(b),
+            p.uniformF(a),
+        };
+
+        uint64_t hash = 0;
+        *outColor = sb->program(&p, device, /*local=*/device, paint, params.matrices,
+                /*localM=*/nullptr, params.dst, uniforms, alloc);
+        if (*outColor) {
+            hash = p.hash();
+            // p.hash() folds in all instructions to produce r,g,b,a but does not know
+            // precisely which value we'll treat as which channel.  Imagine the shader
+            // called std::swap(*r,*b)... it draws differently, but p.hash() is unchanged.
+            // We'll fold the hash of their IDs in order to disambiguate.
+            const skvm::Val outputs[] = {
+                outColor->r.id,
+                outColor->g.id,
+                outColor->b.id,
+                outColor->a.id
+            };
+            hash ^= SkOpts::hash(outputs, sizeof(outputs));
+        } else {
+            *ok = false;
+        }
+        return hash;
+    };
+
+    // Use this builder for shader, clip and blender, so that color objects that pass
+    // from one to the other all 'make sense' -- i.e. have the same builder and/or have
+    // meaningful values for the hash.
+    //
+    // Question: better if we just pass in mock uniform colors, so we don't need to
+    //           explicitly use the output color from one stage as input to another?
+    //
+    skvm::Builder p;
+
+    // Calculate a hash for the color shader.
+    SkASSERT(params.shader);
+    skvm::Color src;
+    uint64_t shaderHash = hash_shader(p, params.shader, &src);
+
+    // Calculate a hash for the clip shader, if one exists.
+    uint64_t clipHash = 0;
+    if (params.clip) {
+        skvm::Color cov;
+        clipHash = hash_shader(p, params.clip, &cov);
+        if (clipHash == 0) {
+            clipHash = 1;
+        }
+    }
+
+    // Calculate a hash for the blender.
+    uint64_t blendHash = 0;
+    if (auto bm = as_BB(params.blender)->asBlendMode()) {
+        blendHash = static_cast<uint8_t>(bm.value());
+    } else if (*ok) {
+        const SkBlenderBase* blender = as_BB(params.blender);
+
+        skvm::Color dst = DstColor(&p, params);
+        skvm::Color outColor = blender->program(&p, src, dst, params.dst, uniforms, alloc);
+        if (outColor) {
+            blendHash = p.hash();
+            // Like in `hash_shader` above, we must fold the color component IDs into our hash.
+            const skvm::Val outputs[] = {
+                outColor.r.id,
+                outColor.g.id,
+                outColor.b.id,
+                outColor.a.id
+            };
+            blendHash ^= SkOpts::hash(outputs, sizeof(outputs));
+        } else {
+            *ok = false;
+        }
+        if (blendHash == 0) {
+            blendHash = 1;
+        }
+    }
+
+    return {
+        shaderHash,
+        clipHash,
+        blendHash,
+        params.dst.colorSpace() ? params.dst.colorSpace()->hash() : 0,
+        SkToU8(params.dst.colorType()),
+        SkToU8(params.dst.alphaType()),
+        SkToU8(params.coverage),
+    };
+}
+
+SkVMBlitter::SkVMBlitter(const SkPixmap& device,
+                         const SkPaint& paint,
+                         const SkPixmap* sprite,
+                         SkIPoint spriteOffset,
+                         const SkMatrixProvider& matrices,
+                         sk_sp<SkShader> clip,
+                         bool* ok)
+        : fDevice(device), fSprite(sprite ? *sprite : SkPixmap{})
+        , fSpriteOffset(spriteOffset)
+        , fUniforms(skvm::Ptr{0}
+        , kBlitterUniformsCount)
+        , fParams(EffectiveParams(device, sprite, paint, matrices, std::move(clip)))
+        , fKey(CacheKey(fParams, &fUniforms, &fAlloc, ok)) {}
+
+SkVMBlitter::~SkVMBlitter() {
+    if (SkLRUCache<Key, skvm::Program>* cache = TryAcquireProgramCache()) {
+        auto cache_program = [&](skvm::Program&& program, Coverage coverage) {
+            if (!program.empty()) {
+                cache->insert_or_update(fKey.withCoverage(coverage), std::move(program));
+            }
+        };
+        cache_program(std::move(fBlitH),         Coverage::Full);
+        cache_program(std::move(fBlitAntiH),     Coverage::UniformF);
+        cache_program(std::move(fBlitMaskA8),    Coverage::MaskA8);
+        cache_program(std::move(fBlitMask3D),    Coverage::Mask3D);
+        cache_program(std::move(fBlitMaskLCD16), Coverage::MaskLCD16);
+
+        ReleaseProgramCache();
+    }
+}
+
+SkLRUCache<SkVMBlitter::Key, skvm::Program>* SkVMBlitter::TryAcquireProgramCache() {
+#if 1 && defined(SKVM_JIT)
+    thread_local static SkLRUCache<Key, skvm::Program> cache{64};
+    return &cache;
+#else
+    // iOS in particular does not support thread_local until iOS 9.0.
+        // On the other hand, we'll never be able to JIT there anyway.
+        // It's probably fine to not cache any interpreted programs, anywhere.
+        return nullptr;
+#endif
+}
+
+SkString SkVMBlitter::DebugName(const Key& key) {
+    return SkStringPrintf("Shader-%" PRIx64 "_Clip-%" PRIx64 "_Blender-%" PRIx64
+                          "_CS-%" PRIx64 "_CT-%d_AT-%d_Cov-%d",
+                          key.shader,
+                          key.clip,
+                          key.blender,
+                          key.colorSpace,
+                          key.colorType,
+                          key.alphaType,
+                          key.coverage);
+}
+
+void SkVMBlitter::ReleaseProgramCache() {}
+
+skvm::Program SkVMBlitter::buildProgram(Coverage coverage) {
+    Key key = fKey.withCoverage(coverage);
+    {
+        skvm::Program p;
+        if (SkLRUCache<Key, skvm::Program>* cache = TryAcquireProgramCache()) {
+            if (skvm::Program* found = cache->find(key)) {
+                p = std::move(*found);
+            }
+            ReleaseProgramCache();
+        }
+        if (!p.empty()) {
+            return p;
+        }
+    }
+    // We don't really _need_ to rebuild fUniforms here.
+    // It's just more natural to have effects unconditionally emit them,
+    // and more natural to rebuild fUniforms than to emit them into a temporary buffer.
+    // fUniforms should reuse the exact same memory, so this is very cheap.
+    SkDEBUGCODE(size_t prev = fUniforms.buf.size();)
+    fUniforms.buf.resize(kBlitterUniformsCount);
+    skvm::Builder builder;
+    BuildProgram(&builder, fParams.withCoverage(coverage), &fUniforms, &fAlloc);
+    SkASSERTF(fUniforms.buf.size() == prev,
+              "%zu, prev was %zu", fUniforms.buf.size(), prev);
+
+    skvm::Program program = builder.done(DebugName(key).c_str());
+    if (false) {
+        static std::atomic<int> missed{0},
+                total{0};
+        if (!program.hasJIT()) {
+            SkDebugf("\ncouldn't JIT %s\n", DebugName(key).c_str());
+            builder.dump();
+            program.dump();
+
+            missed++;
+        }
+        if (0 == total++) {
+            atexit([]{ SkDebugf("SkVMBlitter compiled %d programs, %d without JIT.\n",
+                                total.load(), missed.load()); });
+        }
+    }
+    return program;
+}
+
+void SkVMBlitter::updateUniforms(int right, int y) {
+    BlitterUniforms uniforms{right, y};
+    memcpy(fUniforms.buf.data(), &uniforms, sizeof(BlitterUniforms));
+}
+
+const void* SkVMBlitter::isSprite(int x, int y) const {
+    if (fSprite.colorType() != kUnknown_SkColorType) {
+        return fSprite.addr(x - fSpriteOffset.x(),
+                            y - fSpriteOffset.y());
+    }
+    return nullptr;
+}
+
+void SkVMBlitter::blitH(int x, int y, int w) {
+    if (fBlitH.empty()) {
+        fBlitH = this->buildProgram(Coverage::Full);
+    }
+    this->updateUniforms(x+w, y);
+    if (const void* sprite = this->isSprite(x,y)) {
+        fBlitH.eval(w, fUniforms.buf.data(), fDevice.addr(x,y), sprite);
+    } else {
+        fBlitH.eval(w, fUniforms.buf.data(), fDevice.addr(x,y));
+    }
+}
+
+void SkVMBlitter::blitAntiH(int x, int y, const SkAlpha cov[], const int16_t runs[]) {
+    if (fBlitAntiH.empty()) {
+        fBlitAntiH = this->buildProgram(Coverage::UniformF);
+    }
+    for (int16_t run = *runs; run > 0; run = *runs) {
+        this->updateUniforms(x+run, y);
+        const float covF = *cov * (1/255.0f);
+        if (const void* sprite = this->isSprite(x,y)) {
+            fBlitAntiH.eval(run, fUniforms.buf.data(), fDevice.addr(x,y), sprite, &covF);
+        } else {
+            fBlitAntiH.eval(run, fUniforms.buf.data(), fDevice.addr(x,y), &covF);
+        }
+        x    += run;
+        runs += run;
+        cov  += run;
+    }
+}
+
+void SkVMBlitter::blitMask(const SkMask& mask, const SkIRect& clip) {
+    if (mask.fFormat == SkMask::kBW_Format) {
+        return SkBlitter::blitMask(mask, clip);
+    }
+
+    const skvm::Program* program = nullptr;
+    switch (mask.fFormat) {
+        default: SkUNREACHABLE;     // ARGB and SDF masks shouldn't make it here.
+
+        case SkMask::k3D_Format:
+            if (fBlitMask3D.empty()) {
+                fBlitMask3D = this->buildProgram(Coverage::Mask3D);
+            }
+            program = &fBlitMask3D;
+            break;
+
+        case SkMask::kA8_Format:
+            if (fBlitMaskA8.empty()) {
+                fBlitMaskA8 = this->buildProgram(Coverage::MaskA8);
+            }
+            program = &fBlitMaskA8;
+            break;
+
+        case SkMask::kLCD16_Format:
+            if (fBlitMaskLCD16.empty()) {
+                fBlitMaskLCD16 = this->buildProgram(Coverage::MaskLCD16);
+            }
+            program = &fBlitMaskLCD16;
+            break;
+    }
+
+    SkASSERT(program);
+    if (program) {
+        for (int y = clip.top(); y < clip.bottom(); y++) {
+             int x = clip.left(),
+                 w = clip.width();
+            void* dptr =        fDevice.writable_addr(x,y);
+            auto  mptr = (const uint8_t*)mask.getAddr(x,y);
+            this->updateUniforms(x+w,y);
+
+            if (program == &fBlitMask3D) {
+                size_t plane = mask.computeImageSize();
+                if (const void* sprite = this->isSprite(x,y)) {
+                    program->eval(w, fUniforms.buf.data(), dptr, sprite, mptr + 1*plane
+                                                                       , mptr + 2*plane
+                                                                       , mptr + 0*plane);
+                } else {
+                    program->eval(w, fUniforms.buf.data(), dptr, mptr + 1*plane
+                                                               , mptr + 2*plane
+                                                               , mptr + 0*plane);
+                }
+            } else {
+                if (const void* sprite = this->isSprite(x,y)) {
+                    program->eval(w, fUniforms.buf.data(), dptr, sprite, mptr);
+                } else {
+                    program->eval(w, fUniforms.buf.data(), dptr, mptr);
+                }
+            }
+        }
+    }
+}
+
+SkVMBlitter* SkVMBlitter::Make(const SkPixmap& device,
                                const SkPaint& paint,
                                const SkMatrixProvider& matrices,
                                SkArenaAlloc* alloc,
                                sk_sp<SkShader> clip) {
     bool ok = true;
-    auto blitter = alloc->make<Blitter>(device, paint, /*sprite=*/nullptr, SkIPoint{0,0},
-                                        matrices, std::move(clip), &ok);
+    SkVMBlitter* blitter = alloc->make<SkVMBlitter>(
+            device, paint, /*sprite=*/nullptr, SkIPoint{0,0}, matrices, std::move(clip), &ok);
     return ok ? blitter : nullptr;
 }
 
-SkBlitter* SkCreateSkVMSpriteBlitter(const SkPixmap& device,
-                                     const SkPaint& paint,
-                                     const SkPixmap& sprite,
-                                     int left, int top,
-                                     SkArenaAlloc* alloc,
-                                     sk_sp<SkShader> clip) {
+SkVMBlitter* SkVMBlitter::Make(const SkPixmap& device,
+                               const SkPaint& paint,
+                               const SkPixmap& sprite,
+                               int left, int top,
+                               SkArenaAlloc* alloc,
+                               sk_sp<SkShader> clip) {
     if (paint.getMaskFilter()) {
         // TODO: SkVM support for mask filters?  definitely possible!
         return nullptr;
     }
     bool ok = true;
-    auto blitter = alloc->make<Blitter>(device, paint, &sprite, SkIPoint{left,top},
-                                        SkSimpleMatrixProvider{SkMatrix{}}, std::move(clip), &ok);
+    auto blitter = alloc->make<SkVMBlitter>(
+            device, paint, &sprite, SkIPoint{left,top},
+            SkSimpleMatrixProvider{SkMatrix{}}, std::move(clip), &ok);
     return ok ? blitter : nullptr;
 }
diff --git a/src/core/SkVMBlitter.h b/src/core/SkVMBlitter.h
new file mode 100644
index 0000000..940ae77
--- /dev/null
+++ b/src/core/SkVMBlitter.h
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkVMBlitter_DEFINED
+#define SkVMBlitter_DEFINED
+
+#include "src/core/SkLRUCache.h"
+#include "src/core/SkVM.h"
+
+class SkVMBlitter final : public SkBlitter {
+public:
+    static SkVMBlitter* Make(const SkPixmap& dst,
+                             const SkPaint&,
+                             const SkMatrixProvider&,
+                             SkArenaAlloc*,
+                             sk_sp<SkShader> clipShader);
+
+    static SkVMBlitter* Make(const SkPixmap& dst,
+                             const SkPaint&,
+                             const SkPixmap& sprite,
+                             int left, int top,
+                             SkArenaAlloc*,
+                             sk_sp<SkShader> clipShader);
+
+    SkVMBlitter(const SkPixmap& device,
+                const SkPaint& paint,
+                const SkPixmap* sprite,
+                SkIPoint spriteOffset,
+                const SkMatrixProvider& matrices,
+                sk_sp<SkShader> clip,
+                bool* ok);
+
+    ~SkVMBlitter() override;
+
+private:
+    enum class Coverage { Full, UniformF, MaskA8, MaskLCD16, Mask3D };
+    struct Key {
+        uint64_t shader,
+                 clip,
+                 blender,
+                 colorSpace;
+        uint8_t  colorType,
+                 alphaType,
+                 coverage;
+        uint8_t  padding8{0};
+        uint32_t padding{0};
+        // Params::{paint,quality,matrices} are only passed to {shader,clip}->program(),
+        // not used here by the blitter itself.  No need to include them in the key;
+        // they'll be folded into the shader key if used.
+
+        bool operator==(const Key& that) const;
+        Key withCoverage(Coverage c) const;
+    };
+
+    struct Params {
+        sk_sp<SkShader>         shader;
+        sk_sp<SkShader>         clip;
+        sk_sp<SkBlender>        blender;    // never null
+        SkColorInfo             dst;
+        Coverage                coverage;
+        SkColor4f               paint;
+        const SkMatrixProvider& matrices;
+
+        Params withCoverage(Coverage c) const;
+    };
+
+    static Params EffectiveParams(const SkPixmap& device,
+                                  const SkPixmap* sprite,
+                                  SkPaint paint,
+                                  const SkMatrixProvider& matrices,
+                                  sk_sp<SkShader> clip);
+    static skvm::Color DstColor(skvm::Builder* p, const Params& params);
+    static void BuildProgram(skvm::Builder* p, const Params& params,
+                             skvm::Uniforms* uniforms, SkArenaAlloc* alloc);
+    static Key CacheKey(const Params& params,
+                        skvm::Uniforms* uniforms, SkArenaAlloc* alloc, bool* ok);
+    static SkLRUCache<Key, skvm::Program>* TryAcquireProgramCache();
+    static SkString DebugName(const Key& key);
+    static void ReleaseProgramCache();
+
+    skvm::Program buildProgram(Coverage coverage);
+    void updateUniforms(int right, int y);
+    const void* isSprite(int x, int y) const;
+
+    void blitH(int x, int y, int w) override;
+    void blitAntiH(int x, int y, const SkAlpha cov[], const int16_t runs[]) override;
+    void blitMask(const SkMask& mask, const SkIRect& clip) override;
+
+    SkPixmap        fDevice;
+    const SkPixmap  fSprite;                  // See isSprite().
+    const SkIPoint  fSpriteOffset;
+    skvm::Uniforms  fUniforms;                // Most data is copied directly into fUniforms,
+    SkArenaAlloc    fAlloc{2*sizeof(void*)};  // but a few effects need to ref large content.
+    const Params    fParams;
+    const Key       fKey;
+    skvm::Program   fBlitH,
+                    fBlitAntiH,
+                    fBlitMaskA8,
+                    fBlitMask3D,
+                    fBlitMaskLCD16;
+};
+#endif  // SkVMBlitter_DEFINED