[graphite] Introduce ComputePipeline

* Added the ComputePipelineDesc, ComputePipeline and MtlComputePipeline
  classes as the compute stage equivalents of GraphicsPipelineDesc,
  GraphicsPipeline, and MtlGraphicsPipeline.

* Implemented rudimentary resource tracking for compute pipeline objects
  using a name string that is provided during construction.

* A compute pipeline currently must be constructed with a unique name
  and the complete SkSL program text directly. This is temporary until
  there is a more sophisticated way to represent compute programs.

Bug: b/240604572
Change-Id: I228db6887ce9c9b5feeeb69df21577c05a6ff516
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/563517
Commit-Queue: Arman Uguray <armansito@google.com>
Reviewed-by: Greg Daniel <egdaniel@google.com>
Reviewed-by: Jim Van Verth <jvanverth@google.com>
diff --git a/gn/graphite.gni b/gn/graphite.gni
index 068e7b0..a582940 100644
--- a/gn/graphite.gni
+++ b/gn/graphite.gni
@@ -32,6 +32,8 @@
   "$_src/CommandBuffer.cpp",
   "$_src/CommandBuffer.h",
   "$_src/CommandTypes.h",
+  "$_src/ComputePipeline.cpp",
+  "$_src/ComputePipeline.h",
   "$_src/Context.cpp",
   "$_src/ContextPriv.cpp",
   "$_src/ContextPriv.h",
@@ -160,6 +162,8 @@
   "$_src/mtl/MtlCaps.mm",
   "$_src/mtl/MtlCommandBuffer.h",
   "$_src/mtl/MtlCommandBuffer.mm",
+  "$_src/mtl/MtlComputePipeline.h",
+  "$_src/mtl/MtlComputePipeline.mm",
   "$_src/mtl/MtlGpu.h",
   "$_src/mtl/MtlGpu.mm",
   "$_src/mtl/MtlGraphicsPipeline.h",
diff --git a/src/gpu/graphite/Caps.h b/src/gpu/graphite/Caps.h
index 68491e2..3e67deb 100644
--- a/src/gpu/graphite/Caps.h
+++ b/src/gpu/graphite/Caps.h
@@ -23,6 +23,7 @@
 namespace skgpu::graphite {
 
 struct ContextOptions;
+class ComputePipelineDesc;
 class GraphicsPipelineDesc;
 class GraphiteResourceKey;
 struct RenderPassDesc;
@@ -47,6 +48,7 @@
 
     virtual UniqueKey makeGraphicsPipelineKey(const GraphicsPipelineDesc&,
                                               const RenderPassDesc&) const = 0;
+    virtual UniqueKey makeComputePipelineKey(const ComputePipelineDesc&) const = 0;
 
     bool areColorTypeAndTextureInfoCompatible(SkColorType, const TextureInfo&) const;
 
diff --git a/src/gpu/graphite/ComputePipeline.cpp b/src/gpu/graphite/ComputePipeline.cpp
new file mode 100644
index 0000000..89d8694
--- /dev/null
+++ b/src/gpu/graphite/ComputePipeline.cpp
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2022 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/graphite/ComputePipeline.h"
+
+namespace skgpu::graphite {
+
+ComputePipeline::ComputePipeline(const Gpu* gpu)
+        : Resource(gpu, Ownership::kOwned, SkBudgeted::kYes) {}
+
+}  // namespace skgpu::graphite
diff --git a/src/gpu/graphite/ComputePipeline.h b/src/gpu/graphite/ComputePipeline.h
new file mode 100644
index 0000000..d5a3787
--- /dev/null
+++ b/src/gpu/graphite/ComputePipeline.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef skgpu_graphite_ComputePipeline_DEFINED
+#define skgpu_graphite_ComputePipeline_DEFINED
+
+#include "src/gpu/graphite/Resource.h"
+
+namespace skgpu::graphite {
+
+class Gpu;
+
+/**
+ * ComputePipeline corresponds to a backend specific pipeline used for compute (vs rendering),
+ * e.g. MTLComputePipelineState (Metal),
+ *      CreateComputePipeline (Dawn),
+ *      CreateComputePipelineState (D3D12),
+ *   or VkComputePipelineCreateInfo (Vulkan).
+ */
+class ComputePipeline : public Resource {
+public:
+    ~ComputePipeline() override = default;
+
+    // TODO(b/240615224): The pipeline should return an optional effective local workgroup
+    // size if the value was statically assigned in the shader (when it's not possible to assign
+    // them via specialization constants).
+
+protected:
+    explicit ComputePipeline(const Gpu*);
+};
+
+}  // namespace skgpu::graphite
+
+#endif  // skgpu_graphite_ComputePipeline_DEFINED
diff --git a/src/gpu/graphite/ComputePipelineDesc.h b/src/gpu/graphite/ComputePipelineDesc.h
new file mode 100644
index 0000000..053889b
--- /dev/null
+++ b/src/gpu/graphite/ComputePipelineDesc.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef skgpu_graphite_ComputePipelineDesc_DEFINED
+#define skgpu_graphite_ComputePipelineDesc_DEFINED
+
+#include "include/core/SkSpan.h"
+#include "include/private/SkOpts_spi.h"
+
+#include <array>
+#include <string>
+
+namespace skgpu::graphite {
+
+/**
+ * ComputePipelineDesc represents the state needed to create a backend specific ComputePipeline.
+ */
+class ComputePipelineDesc {
+public:
+    ComputePipelineDesc() = default;
+
+    SkSpan<const uint32_t> asKey() const {
+        return SkSpan(reinterpret_cast<const uint32_t*>(fName.data()),
+                      this->keySize() / sizeof(uint32_t));
+    }
+
+    bool operator==(const ComputePipelineDesc& that) const { return this->fName == that.fName; }
+
+    bool operator!=(const ComputePipelineDesc& other) const { return !(*this == other); }
+
+    // TODO(b/240604614): Until we have a more sophisticated way to dynamically construct compute
+    // shader programs, this currently directly takes the entire program SkSL text as input. The
+    // caching scheme is based entirely on the name and could be improved.
+    const std::string& sksl() const { return fSkSL; }
+    const std::string& name() const { return fName; }
+
+    void setProgram(std::string sksl, std::string name) {
+        SkASSERT(!sksl.empty());
+        SkASSERT(name.size() >= sizeof(uint32_t));
+
+        fSkSL = std::move(sksl);
+        fName = std::move(name);
+    }
+
+    struct Hash {
+        uint32_t operator()(const ComputePipelineDesc& desc) const {
+            return SkOpts::hash_fn(desc.fName.data(), desc.keySize(), 0);
+        }
+    };
+
+private:
+    size_t keySize() const {
+        size_t nameLength = fName.length();
+        return nameLength - (nameLength % sizeof(uint32_t));
+    }
+
+    std::string fSkSL;
+    std::string fName;
+};
+
+}  // namespace skgpu::graphite
+
+#endif  // skgpu_graphite_ComputePipelineDesc_DEFINED
diff --git a/src/gpu/graphite/ContextUtils.cpp b/src/gpu/graphite/ContextUtils.cpp
index 225cf91..5506e3e 100644
--- a/src/gpu/graphite/ContextUtils.cpp
+++ b/src/gpu/graphite/ContextUtils.cpp
@@ -14,6 +14,7 @@
 #include "src/core/SkKeyContext.h"
 #include "src/core/SkPipelineData.h"
 #include "src/core/SkShaderCodeDictionary.h"
+#include "src/gpu/graphite/GraphicsPipelineDesc.h"
 #include "src/gpu/graphite/PaintParams.h"
 #include "src/gpu/graphite/RecorderPriv.h"
 #include "src/gpu/graphite/Renderer.h"
diff --git a/src/gpu/graphite/ResourceProvider.cpp b/src/gpu/graphite/ResourceProvider.cpp
index 2a09ceb..399a29b 100644
--- a/src/gpu/graphite/ResourceProvider.cpp
+++ b/src/gpu/graphite/ResourceProvider.cpp
@@ -10,10 +10,12 @@
 #include "src/gpu/graphite/Buffer.h"
 #include "src/gpu/graphite/Caps.h"
 #include "src/gpu/graphite/CommandBuffer.h"
+#include "src/gpu/graphite/ComputePipeline.h"
 #include "src/gpu/graphite/ContextPriv.h"
 #include "src/gpu/graphite/GlobalCache.h"
 #include "src/gpu/graphite/Gpu.h"
 #include "src/gpu/graphite/GraphicsPipeline.h"
+#include "src/gpu/graphite/GraphicsPipelineDesc.h"
 #include "src/gpu/graphite/ResourceCache.h"
 #include "src/gpu/graphite/Sampler.h"
 #include "src/gpu/graphite/Texture.h"
@@ -28,19 +30,25 @@
         , fGlobalCache(std::move(globalCache)) {
     SkASSERT(fResourceCache);
     fGraphicsPipelineCache.reset(new GraphicsPipelineCache(this));
+    fComputePipelineCache.reset(new ComputePipelineCache(this));
 }
 
 ResourceProvider::~ResourceProvider() {
     fGraphicsPipelineCache.release();
+    fComputePipelineCache.release();
     fResourceCache->shutdown();
 }
 
 sk_sp<GraphicsPipeline> ResourceProvider::findOrCreateGraphicsPipeline(
-        const GraphicsPipelineDesc& pipelineDesc,
-        const RenderPassDesc& renderPassDesc) {
+        const GraphicsPipelineDesc& pipelineDesc, const RenderPassDesc& renderPassDesc) {
     return fGraphicsPipelineCache->refPipeline(fGpu->caps(), pipelineDesc, renderPassDesc);
 }
 
+sk_sp<ComputePipeline> ResourceProvider::findOrCreateComputePipeline(
+        const ComputePipelineDesc& pipelineDesc) {
+    return fComputePipelineCache->refPipeline(fGpu->caps(), pipelineDesc);
+}
+
 SkShaderCodeDictionary* ResourceProvider::shaderCodeDictionary() const {
     return fGlobalCache->shaderCodeDictionary();
 }
@@ -54,8 +62,8 @@
 };
 
 ResourceProvider::GraphicsPipelineCache::GraphicsPipelineCache(ResourceProvider* resourceProvider)
-    : fMap(16) // TODO: find a good value for this
-    , fResourceProvider(resourceProvider) {}
+        : fMap(16)  // TODO: find a good value for this
+        , fResourceProvider(resourceProvider) {}
 
 ResourceProvider::GraphicsPipelineCache::~GraphicsPipelineCache() {
     SkASSERT(0 == fMap.count());
@@ -71,7 +79,7 @@
         const RenderPassDesc& renderPassDesc) {
     UniqueKey pipelineKey = caps->makeGraphicsPipelineKey(pipelineDesc, renderPassDesc);
 
-	std::unique_ptr<Entry>* entry = fMap.find(pipelineKey);
+    std::unique_ptr<Entry>* entry = fMap.find(pipelineKey);
 
     if (!entry) {
         auto pipeline = fResourceProvider->onCreateGraphicsPipeline(pipelineDesc, renderPassDesc);
@@ -83,6 +91,36 @@
     return (*entry)->fPipeline;
 }
 
+struct ResourceProvider::ComputePipelineCache::Entry {
+    Entry(sk_sp<ComputePipeline> pipeline) : fPipeline(std::move(pipeline)) {}
+
+    sk_sp<ComputePipeline> fPipeline;
+};
+
+ResourceProvider::ComputePipelineCache::ComputePipelineCache(ResourceProvider* resourceProvider)
+        : fMap(16)  // TODO: find a good value for this
+        , fResourceProvider(resourceProvider) {}
+
+ResourceProvider::ComputePipelineCache::~ComputePipelineCache() { SkASSERT(0 == fMap.count()); }
+
+void ResourceProvider::ComputePipelineCache::release() { fMap.reset(); }
+
+sk_sp<ComputePipeline> ResourceProvider::ComputePipelineCache::refPipeline(
+        const Caps* caps, const ComputePipelineDesc& pipelineDesc) {
+    UniqueKey pipelineKey = caps->makeComputePipelineKey(pipelineDesc);
+
+    std::unique_ptr<Entry>* entry = fMap.find(pipelineKey);
+
+    if (!entry) {
+        auto pipeline = fResourceProvider->onCreateComputePipeline(pipelineDesc);
+        if (!pipeline) {
+            return nullptr;
+        }
+        entry = fMap.insert(pipelineKey, std::unique_ptr<Entry>(new Entry(std::move(pipeline))));
+    }
+    return (*entry)->fPipeline;
+}
+
 sk_sp<Texture> ResourceProvider::findOrCreateScratchTexture(SkISize dimensions,
                                                             const TextureInfo& info,
                                                             SkBudgeted budgeted) {
@@ -201,4 +239,4 @@
     fRuntimeEffectDictionary.reset();
 }
 
-} // namespace skgpu::graphite
+}  // namespace skgpu::graphite
diff --git a/src/gpu/graphite/ResourceProvider.h b/src/gpu/graphite/ResourceProvider.h
index 042dac0..6133772 100644
--- a/src/gpu/graphite/ResourceProvider.h
+++ b/src/gpu/graphite/ResourceProvider.h
@@ -14,7 +14,6 @@
 #include "src/core/SkRuntimeEffectDictionary.h"
 #include "src/gpu/ResourceKey.h"
 #include "src/gpu/graphite/CommandBuffer.h"
-#include "src/gpu/graphite/GraphicsPipelineDesc.h"
 #include "src/gpu/graphite/ResourceTypes.h"
 
 struct SkSamplingOptions;
@@ -29,9 +28,12 @@
 class BackendTexture;
 class Buffer;
 class Caps;
+class ComputePipeline;
+class ComputePipelineDesc;
 class GlobalCache;
 class Gpu;
 class GraphicsPipeline;
+class GraphicsPipelineDesc;
 class GraphiteResourceKey;
 class ResourceCache;
 class Sampler;
@@ -47,6 +49,8 @@
     sk_sp<GraphicsPipeline> findOrCreateGraphicsPipeline(const GraphicsPipelineDesc&,
                                                          const RenderPassDesc&);
 
+    sk_sp<ComputePipeline> findOrCreateComputePipeline(const ComputePipelineDesc&);
+
     sk_sp<Texture> findOrCreateScratchTexture(SkISize, const TextureInfo&, SkBudgeted);
     virtual sk_sp<Texture> createWrappedTexture(const BackendTexture&) = 0;
 
@@ -81,6 +85,7 @@
 private:
     virtual sk_sp<GraphicsPipeline> onCreateGraphicsPipeline(const GraphicsPipelineDesc&,
                                                              const RenderPassDesc&) = 0;
+    virtual sk_sp<ComputePipeline> onCreateComputePipeline(const ComputePipelineDesc&) = 0;
     virtual sk_sp<Texture> createTexture(SkISize, const TextureInfo&, SkBudgeted) = 0;
     virtual sk_sp<Buffer> createBuffer(size_t size, BufferType type, PrioritizeGpuReads) = 0;
 
@@ -115,12 +120,31 @@
         ResourceProvider* fResourceProvider;
     };
 
+    class ComputePipelineCache {
+    public:
+        ComputePipelineCache(ResourceProvider* resourceProvider);
+        ~ComputePipelineCache();
+
+        void release();
+        sk_sp<ComputePipeline> refPipeline(const Caps* caps, const ComputePipelineDesc&);
+
+    private:
+        struct Entry;
+        struct KeyHash {
+            uint32_t operator()(const UniqueKey& key) const { return key.hash(); }
+        };
+        SkLRUCache<UniqueKey, std::unique_ptr<Entry>, KeyHash> fMap;
+
+        ResourceProvider* fResourceProvider;
+    };
+
     sk_sp<ResourceCache> fResourceCache;
     sk_sp<GlobalCache> fGlobalCache;
 
     // Cache of GraphicsPipelines
-    // TODO: Move this onto GlobalCache
+    // TODO: Move these onto GlobalCache
     std::unique_ptr<GraphicsPipelineCache> fGraphicsPipelineCache;
+    std::unique_ptr<ComputePipelineCache> fComputePipelineCache;
 
     SkRuntimeEffectDictionary fRuntimeEffectDictionary;
 };
diff --git a/src/gpu/graphite/mtl/MtlCaps.h b/src/gpu/graphite/mtl/MtlCaps.h
index e5fca2d..d0ab9a7 100644
--- a/src/gpu/graphite/mtl/MtlCaps.h
+++ b/src/gpu/graphite/mtl/MtlCaps.h
@@ -35,6 +35,7 @@
 
     UniqueKey makeGraphicsPipelineKey(const GraphicsPipelineDesc&,
                                       const RenderPassDesc&) const override;
+    UniqueKey makeComputePipelineKey(const ComputePipelineDesc&) const override;
 
     bool isMac() const { return fGPUFamily == GPUFamily::kMac; }
     bool isApple()const  { return fGPUFamily == GPUFamily::kApple; }
diff --git a/src/gpu/graphite/mtl/MtlCaps.mm b/src/gpu/graphite/mtl/MtlCaps.mm
index c8535e2..8a1d00c 100644
--- a/src/gpu/graphite/mtl/MtlCaps.mm
+++ b/src/gpu/graphite/mtl/MtlCaps.mm
@@ -10,6 +10,7 @@
 #include "include/gpu/graphite/TextureInfo.h"
 #include "include/gpu/graphite/mtl/MtlTypes.h"
 #include "src/gpu/graphite/CommandBuffer.h"
+#include "src/gpu/graphite/ComputePipelineDesc.h"
 #include "src/gpu/graphite/GraphicsPipelineDesc.h"
 #include "src/gpu/graphite/GraphiteResourceKey.h"
 #include "src/gpu/graphite/mtl/MtlUtils.h"
@@ -617,6 +618,27 @@
     return pipelineKey;
 }
 
+UniqueKey MtlCaps::makeComputePipelineKey(const ComputePipelineDesc& pipelineDesc) const {
+    UniqueKey pipelineKey;
+    {
+        static const skgpu::UniqueKey::Domain kComputePipelineDomain = UniqueKey::GenerateDomain();
+        SkSpan<const uint32_t> pipelineDescKey = pipelineDesc.asKey();
+        UniqueKey::Builder builder(
+                &pipelineKey, kComputePipelineDomain, pipelineDescKey.size(), "ComputePipeline");
+        // Add ComputePipelineDesc key
+        for (unsigned int i = 0; i < pipelineDescKey.size(); ++i) {
+            builder[i] = pipelineDescKey[i];
+        }
+
+        // TODO(b/240615224): The local work group size may need to factor into the key on platforms
+        // that don't support specialization constants and require the workgroup/threadgroup size to
+        // be specified in the shader text (D3D12, Vulkan 1.0, and OpenGL).
+
+        builder.finish();
+    }
+    return pipelineKey;
+}
+
 bool MtlCaps::onIsTexturable(const TextureInfo& info) const {
     if (!(info.mtlTextureSpec().fUsage & MTLTextureUsageShaderRead)) {
         return false;
diff --git a/src/gpu/graphite/mtl/MtlComputePipeline.h b/src/gpu/graphite/mtl/MtlComputePipeline.h
new file mode 100644
index 0000000..2298be4
--- /dev/null
+++ b/src/gpu/graphite/mtl/MtlComputePipeline.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef skgpu_graphite_MtlComputePipeline_DEFINED
+#define skgpu_graphite_MtlComputePipeline_DEFINED
+
+#include "include/core/SkRefCnt.h"
+#include "include/ports/SkCFObject.h"
+#include "src/gpu/graphite/ComputePipeline.h"
+
+#import <Metal/Metal.h>
+
+namespace skgpu::graphite {
+
+class ComputePipelineDesc;
+class MtlGpu;
+class MtlResourceProvider;
+
+class MtlComputePipeline final : public ComputePipeline {
+public:
+    static sk_sp<MtlComputePipeline> Make(MtlResourceProvider*,
+                                          const MtlGpu*,
+                                          const ComputePipelineDesc&);
+    ~MtlComputePipeline() override = default;
+
+    id<MTLComputePipelineState> mtlPipelineState() const { return fPipelineState.get(); }
+
+private:
+    MtlComputePipeline(const Gpu* gpu, sk_cfp<id<MTLComputePipelineState>> pso)
+            : ComputePipeline(gpu)
+            , fPipelineState(std::move(pso)) {}
+
+    void freeGpuData() override;
+
+    sk_cfp<id<MTLComputePipelineState>> fPipelineState;
+};
+
+}  // namespace skgpu::graphite
+
+#endif  // skgpu_graphite_MtlComputePipeline_DEFINED
diff --git a/src/gpu/graphite/mtl/MtlComputePipeline.mm b/src/gpu/graphite/mtl/MtlComputePipeline.mm
new file mode 100644
index 0000000..e20c9a4
--- /dev/null
+++ b/src/gpu/graphite/mtl/MtlComputePipeline.mm
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 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/graphite/mtl/MtlComputePipeline.h"
+
+#include "include/gpu/ShaderErrorHandler.h"
+#include "src/gpu/graphite/ComputePipelineDesc.h"
+#include "src/gpu/graphite/Log.h"
+#include "src/gpu/graphite/mtl/MtlGpu.h"
+#include "src/gpu/graphite/mtl/MtlUtils.h"
+
+namespace skgpu::graphite {
+
+// static
+sk_sp<MtlComputePipeline> MtlComputePipeline::Make(MtlResourceProvider* resourceProvider,
+                                                   const MtlGpu* gpu,
+                                                   const ComputePipelineDesc& pipelineDesc) {
+    sk_cfp<MTLComputePipelineDescriptor*> psoDescriptor([MTLComputePipelineDescriptor new]);
+
+    std::string msl;
+    SkSL::Program::Inputs inputs;
+    SkSL::ProgramSettings settings;
+
+    ShaderErrorHandler* errorHandler = gpu->caps()->shaderErrorHandler();
+    if (!SkSLToMSL(gpu,
+                   pipelineDesc.sksl(),
+                   SkSL::ProgramKind::kCompute,
+                   settings,
+                   &msl,
+                   &inputs,
+                   errorHandler)) {
+        return nullptr;
+    }
+
+    sk_cfp<id<MTLLibrary>> shaderLibrary = MtlCompileShaderLibrary(gpu, msl, errorHandler);
+    if (!shaderLibrary) {
+        return nullptr;
+    }
+
+    (*psoDescriptor).label = @(pipelineDesc.name().c_str());
+    (*psoDescriptor).computeFunction = [shaderLibrary.get() newFunctionWithName:@"computeMain"];
+
+    // TODO(b/240604614): Populate input data attribute and buffer layout descriptors using the
+    // `stageInputDescriptor` property based on the contents of `pipelineDesc` (on iOS 10+ or
+    // macOS 10.12+).
+
+    // TODO(b/240604614): Define input buffer mutability using the `buffers` property based on
+    // the contents of `pipelineDesc` (on iOS 11+ or macOS 10.13+).
+
+    // TODO(b/240615224): Metal docs claim that setting the
+    // `threadGroupSizeIsMultipleOfThreadExecutionWidth` to YES may improve performance, IF we can
+    // guarantee that the thread group size used in a dispatch command is a multiple of
+    // `threadExecutionWidth` property of the pipeline state object (otherwise this will cause UB).
+
+    NSError* error;
+    sk_cfp<id<MTLComputePipelineState>> pso([gpu->device()
+            newComputePipelineStateWithDescriptor:psoDescriptor.get()
+                                          options:MTLPipelineOptionNone
+                                       reflection:NULL
+                                            error:&error]);
+    if (!pso) {
+        SKGPU_LOG_E("Compute pipeline creation failure:\n%s", error.debugDescription.UTF8String);
+        return nullptr;
+    }
+
+    return sk_sp<MtlComputePipeline>(new MtlComputePipeline(gpu, std::move(pso)));
+}
+
+void MtlComputePipeline::freeGpuData() { fPipelineState.reset(); }
+
+}  // namespace skgpu::graphite
diff --git a/src/gpu/graphite/mtl/MtlGraphicsPipeline.mm b/src/gpu/graphite/mtl/MtlGraphicsPipeline.mm
index 5616358..6eb8935 100644
--- a/src/gpu/graphite/mtl/MtlGraphicsPipeline.mm
+++ b/src/gpu/graphite/mtl/MtlGraphicsPipeline.mm
@@ -357,7 +357,7 @@
             [gpu->device() newRenderPipelineStateWithDescriptor:psoDescriptor.get()
                                                           error:&error]);
     if (!pso) {
-        SKGPU_LOG_E("Pipeline creation failure:\n%s", error.debugDescription.UTF8String);
+        SKGPU_LOG_E("Render pipeline creation failure:\n%s", error.debugDescription.UTF8String);
         return nullptr;
     }
 
diff --git a/src/gpu/graphite/mtl/MtlResourceProvider.h b/src/gpu/graphite/mtl/MtlResourceProvider.h
index cd27223..d57e9ec 100644
--- a/src/gpu/graphite/mtl/MtlResourceProvider.h
+++ b/src/gpu/graphite/mtl/MtlResourceProvider.h
@@ -36,6 +36,7 @@
     sk_sp<CommandBuffer> createCommandBuffer() override;
     sk_sp<GraphicsPipeline> onCreateGraphicsPipeline(const GraphicsPipelineDesc&,
                                                             const RenderPassDesc&) override;
+    sk_sp<ComputePipeline> onCreateComputePipeline(const ComputePipelineDesc&) override;
     sk_sp<Texture> createTexture(SkISize, const TextureInfo&, SkBudgeted) override;
     sk_sp<Buffer> createBuffer(size_t size, BufferType type, PrioritizeGpuReads) override;
 
diff --git a/src/gpu/graphite/mtl/MtlResourceProvider.mm b/src/gpu/graphite/mtl/MtlResourceProvider.mm
index 572fbc3..e488a22 100644
--- a/src/gpu/graphite/mtl/MtlResourceProvider.mm
+++ b/src/gpu/graphite/mtl/MtlResourceProvider.mm
@@ -12,6 +12,7 @@
 #include "src/gpu/graphite/GraphicsPipelineDesc.h"
 #include "src/gpu/graphite/mtl/MtlBuffer.h"
 #include "src/gpu/graphite/mtl/MtlCommandBuffer.h"
+#include "src/gpu/graphite/mtl/MtlComputePipeline.h"
 #include "src/gpu/graphite/mtl/MtlGpu.h"
 #include "src/gpu/graphite/mtl/MtlGraphicsPipeline.h"
 #include "src/gpu/graphite/mtl/MtlSampler.h"
@@ -44,6 +45,11 @@
                                      renderPassDesc);
 }
 
+sk_sp<ComputePipeline> MtlResourceProvider::onCreateComputePipeline(
+        const ComputePipelineDesc& pipelineDesc) {
+    return MtlComputePipeline::Make(this, this->mtlGpu(), pipelineDesc);
+}
+
 sk_sp<Texture> MtlResourceProvider::createTexture(SkISize dimensions,
                                                   const TextureInfo& info,
                                                   SkBudgeted budgeted) {