diff --git a/src/core/SkVM.cpp b/src/core/SkVM.cpp
index b288b6af0e..9e301678b7 100644
--- a/src/core/SkVM.cpp
+++ b/src/core/SkVM.cpp
@@ -6,12 +6,14 @@
  */
 
 #include "include/core/SkStream.h"
+#include "include/private/SkChecksum.h"
 #include "include/private/SkSpinlock.h"
 #include "include/private/SkTFitsIn.h"
 #include "include/private/SkThreadID.h"
 #include "include/private/SkVx.h"
 #include "src/core/SkCpu.h"
 #include "src/core/SkVM.h"
+#include <functional>  // std::hash
 #include <string.h>
 #if defined(SKVM_JIT)
     #include <sys/mman.h>
@@ -404,22 +406,31 @@ namespace skvm {
 
     // TODO: replace with SkOpts::hash()?
     size_t Builder::InstructionHash::operator()(const Instruction& inst) const {
-        return Hash((uint8_t)inst.op)
-            ^ Hash(inst.x)
-            ^ Hash(inst.y)
-            ^ Hash(inst.z)
-            ^ Hash(inst.imm)
-            ^ Hash(inst.death)
-            ^ Hash(inst.can_hoist)
-            ^ Hash(inst.used_in_loop);
+        auto hash = [](auto v) {
+            return std::hash<decltype(v)>{}(v);
+        };
+        return hash((uint8_t)inst.op)
+             ^ hash(inst.x)
+             ^ hash(inst.y)
+             ^ hash(inst.z)
+             ^ hash(inst.imm)
+             ^ hash(inst.death)
+             ^ hash(inst.can_hoist)
+             ^ hash(inst.used_in_loop);
     }
 
+    uint32_t Builder::hash() const { return fHash; }
+
     // Most instructions produce a value and return it by ID,
     // the value-producing instruction's own index in the program vector.
     Val Builder::push(Op op, Val x, Val y, Val z, int imm) {
         Instruction inst{op, x, y, z, imm,
                          /*death=*/0, /*can_hoist=*/true, /*used_in_loop=*/false};
 
+        // This InstructionHash{}() call should be free given we're about to use fIndex below.
+        fHash ^= InstructionHash{}(inst);
+        fHash = SkChecksum::CheapMix(fHash);  // Make sure instruction order matters.
+
         // Basic common subexpression elimination:
         // if we've already seen this exact Instruction, use it instead of creating a new one.
         if (Val* id = fIndex.find(inst)) {
diff --git a/src/core/SkVM.h b/src/core/SkVM.h
index bfd562a40e..0fae975f32 100644
--- a/src/core/SkVM.h
+++ b/src/core/SkVM.h
@@ -10,7 +10,6 @@
 
 #include "include/core/SkTypes.h"
 #include "include/private/SkTHash.h"
-#include <functional>  // std::hash
 #include <vector>      // std::vector
 
 class SkWStream;
@@ -431,12 +430,10 @@ namespace skvm {
 
         void dump(SkWStream* = nullptr) const;
 
+        uint32_t hash() const;
+
     private:
         struct InstructionHash {
-            template <typename T>
-            static size_t Hash(T val) {
-                return std::hash<T>{}(val);
-            }
             size_t operator()(const Instruction& inst) const;
         };
 
@@ -446,6 +443,7 @@ namespace skvm {
         SkTHashMap<Instruction, Val, InstructionHash> fIndex;
         std::vector<Instruction>                      fProgram;
         std::vector<int>                              fStrides;
+        uint32_t                                      fHash{0};
     };
 
     using Reg = int;
diff --git a/src/core/SkVMBlitter.cpp b/src/core/SkVMBlitter.cpp
index 8c2891dd6e..8f846432ae 100644
--- a/src/core/SkVMBlitter.cpp
+++ b/src/core/SkVMBlitter.cpp
@@ -18,54 +18,77 @@ namespace {
 
     enum class Coverage { Full, UniformA8, MaskA8, MaskLCD16, Mask3D };
 
+    struct Params {
+        sk_sp<SkColorSpace> colorSpace;
+        sk_sp<SkShader>     shader;
+        SkColorType         colorType;
+        SkAlphaType         alphaType;
+        SkBlendMode         blendMode;
+        Coverage            coverage;
+
+        Params withCoverage(Coverage c) const {
+            Params p = *this;
+            p.coverage = c;
+            return p;
+        }
+    };
+
     SK_BEGIN_REQUIRE_DENSE;
     struct Key {
-        SkColorType          colorType;
-        SkAlphaType          alphaType;
-        Coverage             coverage;
-        SkBlendMode          blendMode;
-        sk_sp<SkColorSpace>  colorSpace;
-        sk_sp<SkShader>      shader;
-        bool                 applyPaintAlphaToShader;
-        uint8_t              unusedBytes[7];
+        uint64_t colorSpace;
+        uint32_t shader;
+        uint8_t  colorType,
+                 alphaType,
+                 blendMode,
+                 coverage;
+
+        bool operator==(const Key& that) const {
+            return this->colorSpace == that.colorSpace
+                && this->shader     == that.shader
+                && this->colorType  == that.colorType
+                && this->alphaType  == that.alphaType
+                && this->blendMode  == that.blendMode
+                && this->coverage   == that.coverage;
+        }
 
         Key withCoverage(Coverage c) const {
             Key k = *this;
-            k.coverage = c;
+            k.coverage = SkToU8(c);
             return k;
         }
     };
     SK_END_REQUIRE_DENSE;
 
-    static bool operator==(const Key& x, const Key& y) {
-        // N.B. using SkColorSpace::Equals() would make hashing unsound:
-        //      Keys that compare as == could have non-equal hashes.
-        return x.colorType               == y.colorType
-            && x.alphaType               == y.alphaType
-            && x.coverage                == y.coverage
-            && x.blendMode               == y.blendMode
-            && x.colorSpace              == y.colorSpace
-            && x.shader                  == y.shader
-            && x.applyPaintAlphaToShader == y.applyPaintAlphaToShader;
+    static Key key(const Params& params) {
+        uint32_t shaderHash = 0;
+        if (const SkShaderBase* shader = as_SB(params.shader)) {
+            skvm::Builder p;
+            skvm::I32 r,g,b,a;
+            if (shader->program(&p,
+                                params.colorSpace.get(),
+                                skvm::Arg{0}, 0,
+                                &r,&g,&b,&a)) {
+                shaderHash = p.hash();
+            }
+        }
+        return {
+            params.colorSpace ? params.colorSpace->hash() : 0,
+            shaderHash,
+            SkToU8(params.colorType),
+            SkToU8(params.alphaType),
+            SkToU8(params.blendMode),
+            SkToU8(params.coverage),
+        };
     }
 
-    // TODO: we could do with better hashing and equality here in general,
-    // though I'm not sure how strictly important any of these ideas are:
-    //    - use SkColorSpace::hash() and SkColorSpace::Equals() to coalesce equivalent
-    //      color spaces by value
-    //    - come up with a mechanism to have SkShader identify itself except for its
-    //      uniforms, so that we can reuse programs with the same kind of shader but
-    //      different uniforms
-    //    - pack bits of Key more carefully
-
     static SkString debug_name(const Key& key) {
-        return SkStringPrintf("CT%d-AT%d-Cov%d-Blend%d-CS%d-Shader%d",
+        return SkStringPrintf("CT%d-AT%d-Cov%d-Blend%d-CS%llx-Shader%x",
                               key.colorType,
                               key.alphaType,
                               key.coverage,
                               key.blendMode,
-                              SkToBool(key.colorSpace),
-                              SkToBool(key.shader));
+                              key.colorSpace,
+                              key.shader);
     }
 
     static bool debug_dump(const Key& key) {
@@ -160,28 +183,28 @@ namespace {
         skvm::I32 min(skvm::I32 x, skvm::I32 y) { return select(lt(x,y), x,y); }
         skvm::I32 max(skvm::I32 x, skvm::I32 y) { return select(gt(x,y), x,y); }
 
-        static bool CanBuild(const Key& key) {
+        static bool CanBuild(const Params& params) {
             // These checks parallel the TODOs in Builder::Builder().
-            if (key.shader) {
-                // TODO: maybe passing the whole Key is going to be what we'll want here?
-                if (!as_SB(key.shader)->program(nullptr,
-                                                key.colorSpace.get(),
-                                                skvm::Arg{0}, 0,
-                                                nullptr,nullptr,nullptr,nullptr)) {
+            if (params.shader) {
+                // TODO: probably want to pass all of the device's SkColorInfo?
+                if (!as_SB(params.shader)->program(nullptr,
+                                                   params.colorSpace.get(),
+                                                   skvm::Arg{0}, 0,
+                                                   nullptr,nullptr,nullptr,nullptr)) {
                     return false;
                 }
             }
 
-            switch (key.colorType) {
+            switch (params.colorType) {
                 default: return false;
                 case kRGB_565_SkColorType:   break;
                 case kRGBA_8888_SkColorType: break;
                 case kBGRA_8888_SkColorType: break;
             }
 
-            if (key.alphaType == kUnpremul_SkAlphaType) { return false; }
+            if (params.alphaType == kUnpremul_SkAlphaType) { return false; }
 
-            switch (key.blendMode) {
+            switch (params.blendMode) {
                 default: return false;
                 case SkBlendMode::kSrc:     break;
                 case SkBlendMode::kSrcOver: break;
@@ -190,22 +213,22 @@ namespace {
             return true;
         }
 
-        explicit Builder(const Key& key) {
+        explicit Builder(const Params& params) {
         #define TODO SkUNREACHABLE
-            SkASSERT(CanBuild(key));
+            SkASSERT(CanBuild(params));
             skvm::Arg uniforms = uniform(),
-                      dst_ptr  = arg(SkColorTypeBytesPerPixel(key.colorType));
+                      dst_ptr  = arg(SkColorTypeBytesPerPixel(params.colorType));
             // If coverage is Mask3D there'll next come two varyings for mul and add planes,
             // and then finally if coverage is any Mask?? format, a varying for the mask.
 
             Color src = unpack_8888(uniform32(uniforms, offsetof(Uniforms, paint_color)));
-            if (key.shader) {
+            if (params.shader) {
                 skvm::I32 paint_alpha = src.a;
-                SkAssertResult(as_SB(key.shader)->program(this,
-                                                          key.colorSpace.get(),
-                                                          uniforms, sizeof(Uniforms),
-                                                          &src.r, &src.g, &src.b, &src.a));
-                if (key.applyPaintAlphaToShader) {
+                SkAssertResult(as_SB(params.shader)->program(this,
+                                                             params.colorSpace.get(),
+                                                             uniforms, sizeof(Uniforms),
+                                                             &src.r, &src.g, &src.b, &src.a));
+                if (true/*TODO: make this conditional again with a wrapper shader*/) {
                     src.r = scale_unorm8(src.r, paint_alpha);
                     src.g = scale_unorm8(src.g, paint_alpha);
                     src.b = scale_unorm8(src.b, paint_alpha);
@@ -213,7 +236,7 @@ namespace {
                 }
             }
 
-            if (key.coverage == Coverage::Mask3D) {
+            if (params.coverage == Coverage::Mask3D) {
                 skvm::I32 M = load8(varying<uint8_t>()),
                           A = load8(varying<uint8_t>());
 
@@ -230,7 +253,7 @@ namespace {
 
             // load_coverage() returns false when there's no need to apply coverage.
             auto load_coverage = [&](Color* cov) {
-                switch (key.coverage) {
+                switch (params.coverage) {
                     case Coverage::Full: return false;
 
                     case Coverage::UniformA8: cov->r = cov->g = cov->b = cov->a =
@@ -257,8 +280,8 @@ namespace {
             // obviating the need for the lerp afterwards. This early-coverage strategy tends
             // to be both faster and require fewer registers.
             bool lerp_coverage_post_blend = true;
-            if (SkBlendMode_ShouldPreScaleCoverage(key.blendMode,
-                                                   key.coverage == Coverage::MaskLCD16)) {
+            if (SkBlendMode_ShouldPreScaleCoverage(params.blendMode,
+                                                   params.coverage == Coverage::MaskLCD16)) {
                 Color cov;
                 if (load_coverage(&cov)) {
                     src.r = scale_unorm8(src.r, cov.r);
@@ -271,7 +294,7 @@ namespace {
 
             // Load up the destination color.
             SkDEBUGCODE(dst_loaded = true;)
-            switch (key.colorType) {
+            switch (params.colorType) {
                 default: TODO;
                 case kRGB_565_SkColorType:   dst = unpack_565 (load16(dst_ptr)); break;
                 case kRGBA_8888_SkColorType: dst = unpack_8888(load32(dst_ptr)); break;
@@ -283,14 +306,14 @@ namespace {
             // When a destination is tagged opaque, we may assume it both starts and stays fully
             // opaque, ignoring any math that disagrees.  So anything involving force_opaque is
             // optional, and sometimes helps cut a small amount of work in these programs.
-            const bool force_opaque = true && key.alphaType == kOpaque_SkAlphaType;
+            const bool force_opaque = true && params.alphaType == kOpaque_SkAlphaType;
             if (force_opaque) { dst.a = splat(0xff); }
 
             // We'd need to premul dst after loading and unpremul before storing.
-            if (key.alphaType == kUnpremul_SkAlphaType) { TODO; }
+            if (params.alphaType == kUnpremul_SkAlphaType) { TODO; }
 
             // Blend src and dst.
-            switch (key.blendMode) {
+            switch (params.blendMode) {
                 default: TODO;
 
                 case SkBlendMode::kSrc: break;
@@ -316,7 +339,7 @@ namespace {
             if (force_opaque) { src.a = splat(0xff); }
 
             // Store back to the destination.
-            switch (key.colorType) {
+            switch (params.colorType) {
                 default: SkUNREACHABLE;
 
                 case kRGB_565_SkColorType:   store16(dst_ptr, pack_565(src)); break;
@@ -334,16 +357,15 @@ namespace {
 
         Blitter(const SkPixmap& device, const SkPaint& paint)
             : fDevice(device)
-            , fKey {
-                device.colorType(),
-                device.alphaType(),
-                Coverage::Full,
-                paint.getBlendMode(),
+            , fParams {
                 device.refColorSpace(),
                 paint.refShader(),
-                paint.getShader() && paint.getAlphaf() < 1.0f,
-                {0,0,0,0,0,0,0},
+                device.colorType(),
+                device.alphaType(),
+                paint.getBlendMode(),
+                Coverage::Full,
             }
+            , fKey(key(fParams))
             , fUniforms(sizeof(Uniforms))
         {
             // Color filters have been folded back into shader and/or paint color by now.
@@ -353,7 +375,7 @@ namespace {
             SkColorSpaceXformSteps{sk_srgb_singleton(), kUnpremul_SkAlphaType,
                                    device.colorSpace(), kUnpremul_SkAlphaType}.apply(color.vec());
 
-            if (color.fitsInBytes() && Builder::CanBuild(fKey)) {
+            if (color.fitsInBytes() && Builder::CanBuild(fParams)) {
                 uint32_t rgba = color.premul().toBytes_RGBA();
                 memcpy(fUniforms.data() + offsetof(Uniforms, paint_color), &rgba, sizeof(rgba));
                 ok = true;
@@ -384,6 +406,7 @@ namespace {
 
     private:
         SkPixmap             fDevice;  // TODO: can this be const&?
+        const Params         fParams;
         const Key            fKey;
         std::vector<uint8_t> fUniforms;
         skvm::Program fBlitH,
@@ -412,7 +435,7 @@ namespace {
                 atexit([]{ SkDebugf("%d calls to done\n", done.load()); });
             }
         #endif
-            Builder builder{key};
+            Builder builder{fParams.withCoverage(coverage)};
             skvm::Program program = builder.done(debug_name(key).c_str());
             if (!program.hasJIT() && debug_dump(key)) {
                 SkDebugf("\nfalling back to interpreter for blitter with this key.\n");
@@ -423,10 +446,10 @@ namespace {
         }
 
         void updateUniforms() {
-            if (const SkShaderBase* shader = as_SB(fKey.shader)) {
-                size_t extra = shader->uniforms(fKey.colorSpace.get(), nullptr);
+            if (const SkShaderBase* shader = as_SB(fParams.shader)) {
+                size_t extra = shader->uniforms(fParams.colorSpace.get(), nullptr);
                 fUniforms.resize(sizeof(Uniforms) + extra);
-                shader->uniforms(fKey.colorSpace.get(), fUniforms.data() + sizeof(Uniforms));
+                shader->uniforms(fParams.colorSpace.get(), fUniforms.data() + sizeof(Uniforms));
             }
         }