[Impeller] dont emulate command buffers for compute on Metal/Vulkan. (#49922)

Part of the work for: https://github.com/flutter/flutter/issues/140804

Removes ComputeCommand and has both ComputePassMTL and ComputePassVK immediately record to their respective comand buffers. This combines the commits for Metal/Vulkan as they are both fairly small. After this, I can add barriers to fix compute for Vulkan.
diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files
index 98b6d25..e4445af 100644
--- a/ci/licenses_golden/excluded_files
+++ b/ci/licenses_golden/excluded_files
@@ -182,7 +182,6 @@
 ../../../flutter/impeller/renderer/backend/vulkan/context_vk_unittests.cc
 ../../../flutter/impeller/renderer/backend/vulkan/descriptor_pool_vk_unittests.cc
 ../../../flutter/impeller/renderer/backend/vulkan/fence_waiter_vk_unittests.cc
-../../../flutter/impeller/renderer/backend/vulkan/pass_bindings_cache_unittests.cc
 ../../../flutter/impeller/renderer/backend/vulkan/resource_manager_vk_unittests.cc
 ../../../flutter/impeller/renderer/backend/vulkan/test
 ../../../flutter/impeller/renderer/blit_pass_unittests.cc
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 6bee18a..58fd408 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -5387,6 +5387,8 @@
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/blit_pass_mtl.mm + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/command_buffer_mtl.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/command_buffer_mtl.mm + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.mm + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/compute_pass_mtl.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/compute_pass_mtl.mm + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/compute_pipeline_mtl.h + ../../../flutter/LICENSE
@@ -5431,8 +5433,6 @@
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/android_hardware_buffer_texture_source_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/barrier_vk.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/barrier_vk.h + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/binding_helpers_vk.cc + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/binding_helpers_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/blit_command_vk.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/blit_command_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/blit_pass_vk.cc + ../../../flutter/LICENSE
@@ -5464,8 +5464,6 @@
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/formats_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/gpu_tracer_vk.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/gpu_tracer_vk.h + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/pass_bindings_cache.cc + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/pass_bindings_cache.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/pipeline_cache_vk.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/pipeline_cache_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/pipeline_library_vk.cc + ../../../flutter/LICENSE
@@ -5519,8 +5517,6 @@
 ORIGIN: ../../../flutter/impeller/renderer/command.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/command_buffer.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/command_buffer.h + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/impeller/renderer/compute_command.cc + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/impeller/renderer/compute_command.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/compute_pass.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/compute_pass.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/compute_pipeline_builder.cc + ../../../flutter/LICENSE
@@ -8231,6 +8227,8 @@
 FILE: ../../../flutter/impeller/renderer/backend/metal/blit_pass_mtl.mm
 FILE: ../../../flutter/impeller/renderer/backend/metal/command_buffer_mtl.h
 FILE: ../../../flutter/impeller/renderer/backend/metal/command_buffer_mtl.mm
+FILE: ../../../flutter/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.h
+FILE: ../../../flutter/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.mm
 FILE: ../../../flutter/impeller/renderer/backend/metal/compute_pass_mtl.h
 FILE: ../../../flutter/impeller/renderer/backend/metal/compute_pass_mtl.mm
 FILE: ../../../flutter/impeller/renderer/backend/metal/compute_pipeline_mtl.h
@@ -8275,8 +8273,6 @@
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/android_hardware_buffer_texture_source_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/barrier_vk.cc
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/barrier_vk.h
-FILE: ../../../flutter/impeller/renderer/backend/vulkan/binding_helpers_vk.cc
-FILE: ../../../flutter/impeller/renderer/backend/vulkan/binding_helpers_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/blit_command_vk.cc
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/blit_command_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/blit_pass_vk.cc
@@ -8309,8 +8305,6 @@
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/gpu_tracer_vk.cc
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/gpu_tracer_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/limits_vk.h
-FILE: ../../../flutter/impeller/renderer/backend/vulkan/pass_bindings_cache.cc
-FILE: ../../../flutter/impeller/renderer/backend/vulkan/pass_bindings_cache.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/pipeline_cache_vk.cc
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/pipeline_cache_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/pipeline_library_vk.cc
@@ -8364,8 +8358,6 @@
 FILE: ../../../flutter/impeller/renderer/command.h
 FILE: ../../../flutter/impeller/renderer/command_buffer.cc
 FILE: ../../../flutter/impeller/renderer/command_buffer.h
-FILE: ../../../flutter/impeller/renderer/compute_command.cc
-FILE: ../../../flutter/impeller/renderer/compute_command.h
 FILE: ../../../flutter/impeller/renderer/compute_pass.cc
 FILE: ../../../flutter/impeller/renderer/compute_pass.h
 FILE: ../../../flutter/impeller/renderer/compute_pipeline_builder.cc
diff --git a/impeller/entity/geometry/point_field_geometry.cc b/impeller/entity/geometry/point_field_geometry.cc
index 27369da..0e7bf69 100644
--- a/impeller/entity/geometry/point_field_geometry.cc
+++ b/impeller/entity/geometry/point_field_geometry.cc
@@ -5,7 +5,6 @@
 #include "impeller/entity/geometry/point_field_geometry.h"
 
 #include "impeller/renderer/command_buffer.h"
-#include "impeller/renderer/compute_command.h"
 
 namespace impeller {
 
@@ -168,9 +167,9 @@
   BufferView output;
   {
     using PS = PointsComputeShader;
-    ComputeCommand cmd;
-    DEBUG_COMMAND_INFO(cmd, "Points Geometry");
-    cmd.pipeline = renderer.GetPointComputePipeline();
+
+    compute_pass->SetPipeline(renderer.GetPointComputePipeline());
+    compute_pass->SetCommandLabel("Points Geometry");
 
     PS::FrameInfo frame_info;
     frame_info.count = points_.size();
@@ -180,11 +179,11 @@
     frame_info.points_per_circle = points_per_circle;
     frame_info.divisions_per_circle = vertices_per_geom;
 
-    PS::BindFrameInfo(cmd, host_buffer.EmplaceUniform(frame_info));
-    PS::BindGeometryData(cmd, geometry_buffer);
-    PS::BindPointData(cmd, points_data);
+    PS::BindFrameInfo(*compute_pass, host_buffer.EmplaceUniform(frame_info));
+    PS::BindGeometryData(*compute_pass, geometry_buffer);
+    PS::BindPointData(*compute_pass, points_data);
 
-    if (!compute_pass->AddCommand(std::move(cmd))) {
+    if (!compute_pass->Compute(ISize(total, 1)).ok()) {
       return {};
     }
     output = geometry_buffer;
@@ -201,9 +200,8 @@
 
     using UV = UvComputeShader;
 
-    ComputeCommand cmd;
-    DEBUG_COMMAND_INFO(cmd, "UV Geometry");
-    cmd.pipeline = renderer.GetUvComputePipeline();
+    compute_pass->SetCommandLabel("UV Geometry");
+    compute_pass->SetPipeline(renderer.GetUvComputePipeline());
 
     UV::FrameInfo frame_info;
     frame_info.count = total;
@@ -211,19 +209,16 @@
     frame_info.texture_origin = {0, 0};
     frame_info.texture_size = Vector2(texture_coverage.value().GetSize());
 
-    UV::BindFrameInfo(cmd, host_buffer.EmplaceUniform(frame_info));
-    UV::BindGeometryData(cmd, geometry_buffer);
-    UV::BindGeometryUVData(cmd, geometry_uv_buffer);
+    UV::BindFrameInfo(*compute_pass, host_buffer.EmplaceUniform(frame_info));
+    UV::BindGeometryData(*compute_pass, geometry_buffer);
+    UV::BindGeometryUVData(*compute_pass, geometry_uv_buffer);
 
-    if (!compute_pass->AddCommand(std::move(cmd))) {
+    if (!compute_pass->Compute(ISize(total, 1)).ok()) {
       return {};
     }
     output = geometry_uv_buffer;
   }
 
-  compute_pass->SetGridSize(ISize(total, 1));
-  compute_pass->SetThreadGroupSize(ISize(total, 1));
-
   if (!compute_pass->EncodeCommands() || !cmd_buffer->SubmitCommands()) {
     return {};
   }
diff --git a/impeller/renderer/BUILD.gn b/impeller/renderer/BUILD.gn
index 39e98fd..1456b65 100644
--- a/impeller/renderer/BUILD.gn
+++ b/impeller/renderer/BUILD.gn
@@ -55,8 +55,6 @@
     "command.h",
     "command_buffer.cc",
     "command_buffer.h",
-    "compute_command.cc",
-    "compute_command.h",
     "compute_pass.cc",
     "compute_pass.h",
     "compute_pipeline_builder.cc",
diff --git a/impeller/renderer/backend/metal/BUILD.gn b/impeller/renderer/backend/metal/BUILD.gn
index 4e64976..2c9e0f2 100644
--- a/impeller/renderer/backend/metal/BUILD.gn
+++ b/impeller/renderer/backend/metal/BUILD.gn
@@ -14,6 +14,8 @@
     "blit_pass_mtl.mm",
     "command_buffer_mtl.h",
     "command_buffer_mtl.mm",
+    "compute_pass_bindings_cache_mtl.h",
+    "compute_pass_bindings_cache_mtl.mm",
     "compute_pass_mtl.h",
     "compute_pass_mtl.mm",
     "compute_pipeline_mtl.h",
diff --git a/impeller/renderer/backend/metal/command_buffer_mtl.mm b/impeller/renderer/backend/metal/command_buffer_mtl.mm
index d9c43d1..5909426 100644
--- a/impeller/renderer/backend/metal/command_buffer_mtl.mm
+++ b/impeller/renderer/backend/metal/command_buffer_mtl.mm
@@ -220,9 +220,13 @@
   if (!buffer_) {
     return nullptr;
   }
+  auto context = context_.lock();
+  if (!context) {
+    return nullptr;
+  }
 
   auto pass =
-      std::shared_ptr<ComputePassMTL>(new ComputePassMTL(context_, buffer_));
+      std::shared_ptr<ComputePassMTL>(new ComputePassMTL(context, buffer_));
   if (!pass->IsValid()) {
     return nullptr;
   }
diff --git a/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.h b/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.h
new file mode 100644
index 0000000..eff7557
--- /dev/null
+++ b/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.h
@@ -0,0 +1,62 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_IMPELLER_RENDERER_BACKEND_METAL_COMPUTE_PASS_BINDINGS_CACHE_MTL_H_
+#define FLUTTER_IMPELLER_RENDERER_BACKEND_METAL_COMPUTE_PASS_BINDINGS_CACHE_MTL_H_
+
+#include <Metal/Metal.h>
+
+#include "flutter/fml/macros.h"
+#include "impeller/renderer/compute_pass.h"
+#include "impeller/renderer/pipeline_descriptor.h"
+
+namespace impeller {
+
+//-----------------------------------------------------------------------------
+/// @brief      Ensures that bindings on the pass are not redundantly set or
+///             updated. Avoids making the driver do additional checks and makes
+///             the frame insights during profiling and instrumentation not
+///             complain about the same.
+///
+///             There should be no change to rendering if this caching was
+///             absent.
+///
+struct ComputePassBindingsCacheMTL {
+  explicit ComputePassBindingsCacheMTL() {}
+
+  ComputePassBindingsCacheMTL(const ComputePassBindingsCacheMTL&) = delete;
+
+  ComputePassBindingsCacheMTL(ComputePassBindingsCacheMTL&&) = delete;
+
+  void SetComputePipelineState(id<MTLComputePipelineState> pipeline);
+
+  id<MTLComputePipelineState> GetPipeline() const;
+
+  void SetEncoder(id<MTLComputeCommandEncoder> encoder);
+
+  void SetBuffer(uint64_t index, uint64_t offset, id<MTLBuffer> buffer);
+
+  void SetTexture(uint64_t index, id<MTLTexture> texture);
+
+  void SetSampler(uint64_t index, id<MTLSamplerState> sampler);
+
+ private:
+  struct BufferOffsetPair {
+    id<MTLBuffer> buffer = nullptr;
+    size_t offset = 0u;
+  };
+  using BufferMap = std::map<uint64_t, BufferOffsetPair>;
+  using TextureMap = std::map<uint64_t, id<MTLTexture>>;
+  using SamplerMap = std::map<uint64_t, id<MTLSamplerState>>;
+
+  id<MTLComputeCommandEncoder> encoder_;
+  id<MTLComputePipelineState> pipeline_ = nullptr;
+  BufferMap buffers_;
+  TextureMap textures_;
+  SamplerMap samplers_;
+};
+
+}  // namespace impeller
+
+#endif  // FLUTTER_IMPELLER_RENDERER_BACKEND_METAL_COMPUTE_PASS_BINDINGS_CACHE_MTL_H_
diff --git a/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.mm b/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.mm
new file mode 100644
index 0000000..50e91e7
--- /dev/null
+++ b/impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.mm
@@ -0,0 +1,73 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.h"
+
+namespace impeller {
+
+void ComputePassBindingsCacheMTL::SetComputePipelineState(
+    id<MTLComputePipelineState> pipeline) {
+  if (pipeline == pipeline_) {
+    return;
+  }
+  pipeline_ = pipeline;
+  [encoder_ setComputePipelineState:pipeline_];
+}
+
+id<MTLComputePipelineState> ComputePassBindingsCacheMTL::GetPipeline() const {
+  return pipeline_;
+}
+
+void ComputePassBindingsCacheMTL::SetEncoder(
+    id<MTLComputeCommandEncoder> encoder) {
+  encoder_ = encoder;
+}
+
+void ComputePassBindingsCacheMTL::SetBuffer(uint64_t index,
+                                            uint64_t offset,
+                                            id<MTLBuffer> buffer) {
+  auto found = buffers_.find(index);
+  if (found != buffers_.end() && found->second.buffer == buffer) {
+    // The right buffer is bound. Check if its offset needs to be updated.
+    if (found->second.offset == offset) {
+      // Buffer and its offset is identical. Nothing to do.
+      return;
+    }
+
+    // Only the offset needs to be updated.
+    found->second.offset = offset;
+
+    [encoder_ setBufferOffset:offset atIndex:index];
+    return;
+  }
+
+  buffers_[index] = {buffer, static_cast<size_t>(offset)};
+  [encoder_ setBuffer:buffer offset:offset atIndex:index];
+}
+
+void ComputePassBindingsCacheMTL::SetTexture(uint64_t index,
+                                             id<MTLTexture> texture) {
+  auto found = textures_.find(index);
+  if (found != textures_.end() && found->second == texture) {
+    // Already bound.
+    return;
+  }
+  textures_[index] = texture;
+  [encoder_ setTexture:texture atIndex:index];
+  return;
+}
+
+void ComputePassBindingsCacheMTL::SetSampler(uint64_t index,
+                                             id<MTLSamplerState> sampler) {
+  auto found = samplers_.find(index);
+  if (found != samplers_.end() && found->second == sampler) {
+    // Already bound.
+    return;
+  }
+  samplers_[index] = sampler;
+  [encoder_ setSamplerState:sampler atIndex:index];
+  return;
+}
+
+}  // namespace impeller
diff --git a/impeller/renderer/backend/metal/compute_pass_mtl.h b/impeller/renderer/backend/metal/compute_pass_mtl.h
index 8127496..91a45ea 100644
--- a/impeller/renderer/backend/metal/compute_pass_mtl.h
+++ b/impeller/renderer/backend/metal/compute_pass_mtl.h
@@ -8,7 +8,9 @@
 #include <Metal/Metal.h>
 
 #include "flutter/fml/macros.h"
+#include "impeller/renderer/backend/metal/compute_pass_bindings_cache_mtl.h"
 #include "impeller/renderer/compute_pass.h"
+#include "impeller/renderer/pipeline_descriptor.h"
 
 namespace impeller {
 
@@ -21,27 +23,48 @@
   friend class CommandBufferMTL;
 
   id<MTLCommandBuffer> buffer_ = nil;
-  std::string label_;
+  id<MTLComputeCommandEncoder> encoder_ = nil;
+  ComputePassBindingsCacheMTL pass_bindings_cache_ =
+      ComputePassBindingsCacheMTL();
   bool is_valid_ = false;
+  bool has_label_ = false;
 
-  ComputePassMTL(std::weak_ptr<const Context> context,
+  ComputePassMTL(std::shared_ptr<const Context> context,
                  id<MTLCommandBuffer> buffer);
 
   // |ComputePass|
   bool IsValid() const override;
 
   // |ComputePass|
+  fml::Status Compute(const ISize& grid_size) override;
+
+  // |ComputePass|
+  void SetCommandLabel(std::string_view label) override;
+
+  // |ComputePass|
   void OnSetLabel(const std::string& label) override;
 
   // |ComputePass|
-  bool OnEncodeCommands(const Context& context,
-                        const ISize& grid_size,
-                        const ISize& thread_group_size) const override;
+  void SetPipeline(const std::shared_ptr<Pipeline<ComputePipelineDescriptor>>&
+                       pipeline) override;
 
-  bool EncodeCommands(const std::shared_ptr<Allocator>& allocator,
-                      id<MTLComputeCommandEncoder> pass,
-                      const ISize& grid_size,
-                      const ISize& thread_group_size) const;
+  // |ComputePass|
+  bool BindResource(ShaderStage stage,
+                    DescriptorType type,
+                    const ShaderUniformSlot& slot,
+                    const ShaderMetadata& metadata,
+                    BufferView view) override;
+
+  // |ComputePass|
+  bool BindResource(ShaderStage stage,
+                    DescriptorType type,
+                    const SampledImageSlot& slot,
+                    const ShaderMetadata& metadata,
+                    std::shared_ptr<const Texture> texture,
+                    std::shared_ptr<const Sampler> sampler) override;
+
+  // |ComputePass|
+  bool EncodeCommands() const override;
 
   ComputePassMTL(const ComputePassMTL&) = delete;
 
diff --git a/impeller/renderer/backend/metal/compute_pass_mtl.mm b/impeller/renderer/backend/metal/compute_pass_mtl.mm
index 5abab34..e322819 100644
--- a/impeller/renderer/backend/metal/compute_pass_mtl.mm
+++ b/impeller/renderer/backend/metal/compute_pass_mtl.mm
@@ -6,13 +6,14 @@
 
 #include <Metal/Metal.h>
 #include <memory>
-#include <variant>
 
 #include "flutter/fml/backtrace.h"
 #include "flutter/fml/closure.h"
 #include "flutter/fml/logging.h"
 #include "flutter/fml/trace_event.h"
+#include "fml/status.h"
 #include "impeller/base/backend_cast.h"
+#include "impeller/core/device_buffer.h"
 #include "impeller/core/formats.h"
 #include "impeller/core/host_buffer.h"
 #include "impeller/core/shader_types.h"
@@ -22,16 +23,20 @@
 #include "impeller/renderer/backend/metal/sampler_mtl.h"
 #include "impeller/renderer/backend/metal/texture_mtl.h"
 #include "impeller/renderer/command.h"
-#include "impeller/renderer/compute_command.h"
 
 namespace impeller {
 
-ComputePassMTL::ComputePassMTL(std::weak_ptr<const Context> context,
+ComputePassMTL::ComputePassMTL(std::shared_ptr<const Context> context,
                                id<MTLCommandBuffer> buffer)
     : ComputePass(std::move(context)), buffer_(buffer) {
   if (!buffer_) {
     return;
   }
+  encoder_ = [buffer_ computeCommandEncoder];
+  if (!encoder_) {
+    return;
+  }
+  pass_bindings_cache_.SetEncoder(encoder_);
   is_valid_ = true;
 }
 
@@ -42,231 +47,114 @@
 }
 
 void ComputePassMTL::OnSetLabel(const std::string& label) {
+#ifdef IMPELLER_DEBUG
   if (label.empty()) {
     return;
   }
-  label_ = label;
+  [encoder_ setLabel:@(label.c_str())];
+#endif  // IMPELLER_DEBUG
 }
 
-bool ComputePassMTL::OnEncodeCommands(const Context& context,
-                                      const ISize& grid_size,
-                                      const ISize& thread_group_size) const {
-  TRACE_EVENT0("impeller", "ComputePassMTL::EncodeCommands");
-  if (!IsValid()) {
-    return false;
-  }
-
-  FML_DCHECK(!grid_size.IsEmpty() && !thread_group_size.IsEmpty());
-
-  // TODO(dnfield): Support non-serial dispatch type on higher iOS versions.
-  auto compute_command_encoder = [buffer_ computeCommandEncoder];
-
-  if (!compute_command_encoder) {
-    return false;
-  }
-
-  if (!label_.empty()) {
-    [compute_command_encoder setLabel:@(label_.c_str())];
-  }
-
-  // Success or failure, the pass must end. The buffer can only process one pass
-  // at a time.
-  fml::ScopedCleanupClosure auto_end(
-      [compute_command_encoder]() { [compute_command_encoder endEncoding]; });
-
-  return EncodeCommands(context.GetResourceAllocator(), compute_command_encoder,
-                        grid_size, thread_group_size);
+void ComputePassMTL::SetCommandLabel(std::string_view label) {
+  has_label_ = true;
+  [encoder_ pushDebugGroup:@(label.data())];
 }
 
-//-----------------------------------------------------------------------------
-/// @brief      Ensures that bindings on the pass are not redundantly set or
-///             updated. Avoids making the driver do additional checks and makes
-///             the frame insights during profiling and instrumentation not
-///             complain about the same.
-///
-///             There should be no change to rendering if this caching was
-///             absent.
-///
-struct ComputePassBindingsCache {
-  explicit ComputePassBindingsCache(id<MTLComputeCommandEncoder> encoder)
-      : encoder_(encoder) {}
+// |ComputePass|
+void ComputePassMTL::SetPipeline(
+    const std::shared_ptr<Pipeline<ComputePipelineDescriptor>>& pipeline) {
+  pass_bindings_cache_.SetComputePipelineState(
+      ComputePipelineMTL::Cast(*pipeline).GetMTLComputePipelineState());
+}
 
-  ComputePassBindingsCache(const ComputePassBindingsCache&) = delete;
-
-  ComputePassBindingsCache(ComputePassBindingsCache&&) = delete;
-
-  void SetComputePipelineState(id<MTLComputePipelineState> pipeline) {
-    if (pipeline == pipeline_) {
-      return;
-    }
-    pipeline_ = pipeline;
-    [encoder_ setComputePipelineState:pipeline_];
-  }
-
-  id<MTLComputePipelineState> GetPipeline() const { return pipeline_; }
-
-  void SetBuffer(uint64_t index, uint64_t offset, id<MTLBuffer> buffer) {
-    auto found = buffers_.find(index);
-    if (found != buffers_.end() && found->second.buffer == buffer) {
-      // The right buffer is bound. Check if its offset needs to be updated.
-      if (found->second.offset == offset) {
-        // Buffer and its offset is identical. Nothing to do.
-        return;
-      }
-
-      // Only the offset needs to be updated.
-      found->second.offset = offset;
-
-      [encoder_ setBufferOffset:offset atIndex:index];
-      return;
-    }
-
-    buffers_[index] = {buffer, static_cast<size_t>(offset)};
-    [encoder_ setBuffer:buffer offset:offset atIndex:index];
-  }
-
-  void SetTexture(uint64_t index, id<MTLTexture> texture) {
-    auto found = textures_.find(index);
-    if (found != textures_.end() && found->second == texture) {
-      // Already bound.
-      return;
-    }
-    textures_[index] = texture;
-    [encoder_ setTexture:texture atIndex:index];
-    return;
-  }
-
-  void SetSampler(uint64_t index, id<MTLSamplerState> sampler) {
-    auto found = samplers_.find(index);
-    if (found != samplers_.end() && found->second == sampler) {
-      // Already bound.
-      return;
-    }
-    samplers_[index] = sampler;
-    [encoder_ setSamplerState:sampler atIndex:index];
-    return;
-  }
-
- private:
-  struct BufferOffsetPair {
-    id<MTLBuffer> buffer = nullptr;
-    size_t offset = 0u;
-  };
-  using BufferMap = std::map<uint64_t, BufferOffsetPair>;
-  using TextureMap = std::map<uint64_t, id<MTLTexture>>;
-  using SamplerMap = std::map<uint64_t, id<MTLSamplerState>>;
-
-  const id<MTLComputeCommandEncoder> encoder_;
-  id<MTLComputePipelineState> pipeline_ = nullptr;
-  BufferMap buffers_;
-  TextureMap textures_;
-  SamplerMap samplers_;
-};
-
-static bool Bind(ComputePassBindingsCache& pass,
-                 Allocator& allocator,
-                 size_t bind_index,
-                 const BufferView& view) {
+// |ComputePass|
+bool ComputePassMTL::BindResource(ShaderStage stage,
+                                  DescriptorType type,
+                                  const ShaderUniformSlot& slot,
+                                  const ShaderMetadata& metadata,
+                                  BufferView view) {
   if (!view.buffer) {
     return false;
   }
 
-  auto device_buffer = view.buffer;
+  const std::shared_ptr<const DeviceBuffer>& device_buffer = view.buffer;
   if (!device_buffer) {
     return false;
   }
 
-  auto buffer = DeviceBufferMTL::Cast(*device_buffer).GetMTLBuffer();
+  id<MTLBuffer> buffer = DeviceBufferMTL::Cast(*device_buffer).GetMTLBuffer();
   // The Metal call is a void return and we don't want to make it on nil.
   if (!buffer) {
     return false;
   }
 
-  pass.SetBuffer(bind_index, view.range.offset, buffer);
+  pass_bindings_cache_.SetBuffer(slot.ext_res_0, view.range.offset, buffer);
   return true;
 }
 
-static bool Bind(ComputePassBindingsCache& pass,
-                 size_t bind_index,
-                 const Sampler& sampler,
-                 const Texture& texture) {
-  if (!sampler.IsValid() || !texture.IsValid()) {
+// |ComputePass|
+bool ComputePassMTL::BindResource(ShaderStage stage,
+                                  DescriptorType type,
+                                  const SampledImageSlot& slot,
+                                  const ShaderMetadata& metadata,
+                                  std::shared_ptr<const Texture> texture,
+                                  std::shared_ptr<const Sampler> sampler) {
+  if (!sampler->IsValid() || !texture->IsValid()) {
     return false;
   }
 
-  pass.SetTexture(bind_index, TextureMTL::Cast(texture).GetMTLTexture());
-  pass.SetSampler(bind_index, SamplerMTL::Cast(sampler).GetMTLSamplerState());
+  pass_bindings_cache_.SetTexture(slot.texture_index,
+                                  TextureMTL::Cast(*texture).GetMTLTexture());
+  pass_bindings_cache_.SetSampler(
+      slot.texture_index, SamplerMTL::Cast(*sampler).GetMTLSamplerState());
   return true;
 }
 
-bool ComputePassMTL::EncodeCommands(const std::shared_ptr<Allocator>& allocator,
-                                    id<MTLComputeCommandEncoder> encoder,
-                                    const ISize& grid_size,
-                                    const ISize& thread_group_size) const {
-  if (grid_size.width == 0 || grid_size.height == 0) {
-    return true;
+fml::Status ComputePassMTL::Compute(const ISize& grid_size) {
+  if (grid_size.IsEmpty()) {
+    return fml::Status(fml::StatusCode::kUnknown,
+                       "Invalid grid size for compute command.");
+  }
+  // TODO(dnfield): use feature detection to support non-uniform threadgroup
+  // sizes.
+  // https://github.com/flutter/flutter/issues/110619
+  auto width = grid_size.width;
+  auto height = grid_size.height;
+
+  auto max_total_threads_per_threadgroup = static_cast<int64_t>(
+      pass_bindings_cache_.GetPipeline().maxTotalThreadsPerThreadgroup);
+
+  // Special case for linear processing.
+  if (height == 1) {
+    int64_t thread_groups = std::max(
+        static_cast<int64_t>(
+            std::ceil(width * 1.0 / max_total_threads_per_threadgroup * 1.0)),
+        1LL);
+    [encoder_
+         dispatchThreadgroups:MTLSizeMake(thread_groups, 1, 1)
+        threadsPerThreadgroup:MTLSizeMake(max_total_threads_per_threadgroup, 1,
+                                          1)];
+  } else {
+    while (width * height > max_total_threads_per_threadgroup) {
+      width = std::max(1LL, width / 2);
+      height = std::max(1LL, height / 2);
+    }
+
+    auto size = MTLSizeMake(width, height, 1);
+    [encoder_ dispatchThreadgroups:size threadsPerThreadgroup:size];
   }
 
-  ComputePassBindingsCache pass_bindings(encoder);
-
-  fml::closure pop_debug_marker = [encoder]() { [encoder popDebugGroup]; };
-  for (const ComputeCommand& command : commands_) {
 #ifdef IMPELLER_DEBUG
-    fml::ScopedCleanupClosure auto_pop_debug_marker(pop_debug_marker);
-    if (!command.label.empty()) {
-      [encoder pushDebugGroup:@(command.label.c_str())];
-    } else {
-      auto_pop_debug_marker.Release();
-    }
-#endif
-
-    pass_bindings.SetComputePipelineState(
-        ComputePipelineMTL::Cast(*command.pipeline)
-            .GetMTLComputePipelineState());
-
-    for (const BufferAndUniformSlot& buffer : command.bindings.buffers) {
-      if (!Bind(pass_bindings, *allocator, buffer.slot.ext_res_0,
-                buffer.view.resource)) {
-        return false;
-      }
-    }
-
-    for (const TextureAndSampler& data : command.bindings.sampled_images) {
-      if (!Bind(pass_bindings, data.slot.texture_index, *data.sampler,
-                *data.texture.resource)) {
-        return false;
-      }
-    }
-
-    // TODO(dnfield): use feature detection to support non-uniform threadgroup
-    // sizes.
-    // https://github.com/flutter/flutter/issues/110619
-    auto width = grid_size.width;
-    auto height = grid_size.height;
-
-    auto maxTotalThreadsPerThreadgroup = static_cast<int64_t>(
-        pass_bindings.GetPipeline().maxTotalThreadsPerThreadgroup);
-
-    // Special case for linear processing.
-    if (height == 1) {
-      int64_t threadGroups = std::max(
-          static_cast<int64_t>(
-              std::ceil(width * 1.0 / maxTotalThreadsPerThreadgroup * 1.0)),
-          1LL);
-      [encoder dispatchThreadgroups:MTLSizeMake(threadGroups, 1, 1)
-              threadsPerThreadgroup:MTLSizeMake(maxTotalThreadsPerThreadgroup,
-                                                1, 1)];
-    } else {
-      while (width * height > maxTotalThreadsPerThreadgroup) {
-        width = std::max(1LL, width / 2);
-        height = std::max(1LL, height / 2);
-      }
-
-      auto size = MTLSizeMake(width, height, 1);
-      [encoder dispatchThreadgroups:size threadsPerThreadgroup:size];
-    }
+  if (has_label_) {
+    [encoder_ popDebugGroup];
   }
+  has_label_ = false;
+#endif  // IMPELLER_DEBUG
+  return fml::Status();
+}
 
+bool ComputePassMTL::EncodeCommands() const {
+  [encoder_ endEncoding];
   return true;
 }
 
diff --git a/impeller/renderer/backend/vulkan/BUILD.gn b/impeller/renderer/backend/vulkan/BUILD.gn
index 7a7822c..63cf211 100644
--- a/impeller/renderer/backend/vulkan/BUILD.gn
+++ b/impeller/renderer/backend/vulkan/BUILD.gn
@@ -14,7 +14,6 @@
     "context_vk_unittests.cc",
     "descriptor_pool_vk_unittests.cc",
     "fence_waiter_vk_unittests.cc",
-    "pass_bindings_cache_unittests.cc",
     "resource_manager_vk_unittests.cc",
     "test/gpu_tracer_unittests.cc",
     "test/mock_vulkan.cc",
@@ -35,8 +34,6 @@
     "android_hardware_buffer_texture_source_vk.h",
     "barrier_vk.cc",
     "barrier_vk.h",
-    "binding_helpers_vk.cc",
-    "binding_helpers_vk.h",
     "blit_command_vk.cc",
     "blit_command_vk.h",
     "blit_pass_vk.cc",
@@ -68,8 +65,6 @@
     "gpu_tracer_vk.cc",
     "gpu_tracer_vk.h",
     "limits_vk.h",
-    "pass_bindings_cache.cc",
-    "pass_bindings_cache.h",
     "pipeline_cache_vk.cc",
     "pipeline_cache_vk.h",
     "pipeline_library_vk.cc",
diff --git a/impeller/renderer/backend/vulkan/binding_helpers_vk.cc b/impeller/renderer/backend/vulkan/binding_helpers_vk.cc
deleted file mode 100644
index 92b3031..0000000
--- a/impeller/renderer/backend/vulkan/binding_helpers_vk.cc
+++ /dev/null
@@ -1,162 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "impeller/renderer/backend/vulkan/binding_helpers_vk.h"
-#include "fml/status.h"
-#include "impeller/core/allocator.h"
-#include "impeller/core/device_buffer.h"
-#include "impeller/core/shader_types.h"
-#include "impeller/renderer/backend/vulkan/command_buffer_vk.h"
-#include "impeller/renderer/backend/vulkan/command_encoder_vk.h"
-#include "impeller/renderer/backend/vulkan/command_pool_vk.h"
-#include "impeller/renderer/backend/vulkan/compute_pipeline_vk.h"
-#include "impeller/renderer/backend/vulkan/context_vk.h"
-#include "impeller/renderer/backend/vulkan/sampler_vk.h"
-#include "impeller/renderer/backend/vulkan/texture_vk.h"
-#include "impeller/renderer/command.h"
-#include "impeller/renderer/compute_command.h"
-#include "vulkan/vulkan_core.h"
-
-namespace impeller {
-
-static bool BindImages(
-    const Bindings& bindings,
-    Allocator& allocator,
-    const std::shared_ptr<CommandEncoderVK>& encoder,
-    vk::DescriptorSet& vk_desc_set,
-    std::array<vk::DescriptorImageInfo, kMaxBindings>& image_workspace,
-    size_t& image_offset,
-    std::array<vk::WriteDescriptorSet, kMaxBindings + kMaxBindings>&
-        write_workspace,
-    size_t& write_offset) {
-  for (const TextureAndSampler& data : bindings.sampled_images) {
-    const std::shared_ptr<const Texture>& texture = data.texture.resource;
-    const TextureVK& texture_vk = TextureVK::Cast(*texture);
-    const SamplerVK& sampler = SamplerVK::Cast(*data.sampler);
-
-    if (!encoder->Track(texture) ||
-        !encoder->Track(sampler.GetSharedSampler())) {
-      return false;
-    }
-
-    const SampledImageSlot& slot = data.slot;
-
-    vk::DescriptorImageInfo image_info;
-    image_info.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
-    image_info.sampler = sampler.GetSampler();
-    image_info.imageView = texture_vk.GetImageView();
-    image_workspace[image_offset++] = image_info;
-
-    vk::WriteDescriptorSet write_set;
-    write_set.dstSet = vk_desc_set;
-    write_set.dstBinding = slot.binding;
-    write_set.descriptorCount = 1u;
-    write_set.descriptorType = vk::DescriptorType::eCombinedImageSampler;
-    write_set.pImageInfo = &image_workspace[image_offset - 1];
-
-    write_workspace[write_offset++] = write_set;
-  }
-
-  return true;
-};
-
-static bool BindBuffers(
-    const Bindings& bindings,
-    Allocator& allocator,
-    const std::shared_ptr<CommandEncoderVK>& encoder,
-    vk::DescriptorSet& vk_desc_set,
-    const std::vector<DescriptorSetLayout>& desc_set,
-    std::array<vk::DescriptorBufferInfo, kMaxBindings>& buffer_workspace,
-    size_t& buffer_offset,
-    std::array<vk::WriteDescriptorSet, kMaxBindings + kMaxBindings>&
-        write_workspace,
-    size_t& write_offset) {
-  for (const BufferAndUniformSlot& data : bindings.buffers) {
-    const std::shared_ptr<const DeviceBuffer>& device_buffer =
-        data.view.resource.buffer;
-
-    auto buffer = DeviceBufferVK::Cast(*device_buffer).GetBuffer();
-    if (!buffer) {
-      return false;
-    }
-
-    if (!encoder->Track(device_buffer)) {
-      return false;
-    }
-
-    uint32_t offset = data.view.resource.range.offset;
-
-    vk::DescriptorBufferInfo buffer_info;
-    buffer_info.buffer = buffer;
-    buffer_info.offset = offset;
-    buffer_info.range = data.view.resource.range.length;
-    buffer_workspace[buffer_offset++] = buffer_info;
-
-    // TODO(jonahwilliams): remove this part by storing more data in
-    // ShaderUniformSlot.
-    const ShaderUniformSlot& uniform = data.slot;
-    auto layout_it =
-        std::find_if(desc_set.begin(), desc_set.end(),
-                     [&uniform](const DescriptorSetLayout& layout) {
-                       return layout.binding == uniform.binding;
-                     });
-    if (layout_it == desc_set.end()) {
-      VALIDATION_LOG << "Failed to get descriptor set layout for binding "
-                     << uniform.binding;
-      return false;
-    }
-    auto layout = *layout_it;
-
-    vk::WriteDescriptorSet write_set;
-    write_set.dstSet = vk_desc_set;
-    write_set.dstBinding = uniform.binding;
-    write_set.descriptorCount = 1u;
-    write_set.descriptorType = ToVKDescriptorType(layout.descriptor_type);
-    write_set.pBufferInfo = &buffer_workspace[buffer_offset - 1];
-
-    write_workspace[write_offset++] = write_set;
-  }
-  return true;
-}
-
-fml::StatusOr<vk::DescriptorSet> AllocateAndBindDescriptorSets(
-    const ContextVK& context,
-    const std::shared_ptr<CommandEncoderVK>& encoder,
-    Allocator& allocator,
-    const ComputeCommand& command,
-    std::array<vk::DescriptorImageInfo, kMaxBindings>& image_workspace,
-    std::array<vk::DescriptorBufferInfo, kMaxBindings>& buffer_workspace,
-    std::array<vk::WriteDescriptorSet, kMaxBindings + kMaxBindings>&
-        write_workspace) {
-  auto descriptor_result = encoder->AllocateDescriptorSets(
-      ComputePipelineVK::Cast(*command.pipeline).GetDescriptorSetLayout(),
-      context);
-  if (!descriptor_result.ok()) {
-    return descriptor_result.status();
-  }
-  auto descriptor_set = descriptor_result.value();
-
-  size_t buffer_offset = 0u;
-  size_t image_offset = 0u;
-  size_t write_offset = 0u;
-
-  auto& pipeline_descriptor = command.pipeline->GetDescriptor();
-  auto& desc_set = pipeline_descriptor.GetDescriptorSetLayouts();
-
-  if (!BindBuffers(command.bindings, allocator, encoder, descriptor_set,
-                   desc_set, buffer_workspace, buffer_offset, write_workspace,
-                   write_offset) ||
-      !BindImages(command.bindings, allocator, encoder, descriptor_set,
-                  image_workspace, image_offset, write_workspace,
-                  write_offset)) {
-    return fml::Status(fml::StatusCode::kUnknown,
-                       "Failed to bind texture or buffer.");
-  }
-  context.GetDevice().updateDescriptorSets(write_offset, write_workspace.data(),
-                                           0u, {});
-
-  return descriptor_set;
-}
-
-}  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/binding_helpers_vk.h b/impeller/renderer/backend/vulkan/binding_helpers_vk.h
deleted file mode 100644
index be35567..0000000
--- a/impeller/renderer/backend/vulkan/binding_helpers_vk.h
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_BINDING_HELPERS_VK_H_
-#define FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_BINDING_HELPERS_VK_H_
-
-#include "fml/status_or.h"
-#include "impeller/renderer/backend/vulkan/context_vk.h"
-#include "impeller/renderer/backend/vulkan/texture_vk.h"
-#include "impeller/renderer/command.h"
-#include "impeller/renderer/compute_command.h"
-
-namespace impeller {
-
-// Limit on the total number of buffer and image bindings that allow the Vulkan
-// backend to avoid dynamic heap allocations.
-static constexpr size_t kMaxBindings = 32;
-
-fml::StatusOr<vk::DescriptorSet> AllocateAndBindDescriptorSets(
-    const ContextVK& context,
-    const std::shared_ptr<CommandEncoderVK>& encoder,
-    Allocator& allocator,
-    const ComputeCommand& command,
-    std::array<vk::DescriptorImageInfo, kMaxBindings>& image_workspace,
-    std::array<vk::DescriptorBufferInfo, kMaxBindings>& buffer_workspace,
-    std::array<vk::WriteDescriptorSet, kMaxBindings + kMaxBindings>&
-        write_workspace);
-
-}  // namespace impeller
-
-#endif  // FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_BINDING_HELPERS_VK_H_
diff --git a/impeller/renderer/backend/vulkan/command_buffer_vk.cc b/impeller/renderer/backend/vulkan/command_buffer_vk.cc
index 6b7b14f..c63dbec 100644
--- a/impeller/renderer/backend/vulkan/command_buffer_vk.cc
+++ b/impeller/renderer/backend/vulkan/command_buffer_vk.cc
@@ -100,8 +100,8 @@
     return nullptr;
   }
   auto pass =
-      std::shared_ptr<ComputePassVK>(new ComputePassVK(context,          //
-                                                       weak_from_this()  //
+      std::shared_ptr<ComputePassVK>(new ComputePassVK(context,            //
+                                                       shared_from_this()  //
                                                        ));
   if (!pass->IsValid()) {
     return nullptr;
diff --git a/impeller/renderer/backend/vulkan/compute_pass_vk.cc b/impeller/renderer/backend/vulkan/compute_pass_vk.cc
index 4788ec1..8324879 100644
--- a/impeller/renderer/backend/vulkan/compute_pass_vk.cc
+++ b/impeller/renderer/backend/vulkan/compute_pass_vk.cc
@@ -4,19 +4,25 @@
 
 #include "impeller/renderer/backend/vulkan/compute_pass_vk.h"
 
-#include "flutter/fml/trace_event.h"
-#include "impeller/renderer/backend/vulkan/binding_helpers_vk.h"
 #include "impeller/renderer/backend/vulkan/command_buffer_vk.h"
 #include "impeller/renderer/backend/vulkan/compute_pipeline_vk.h"
+#include "impeller/renderer/backend/vulkan/formats_vk.h"
+#include "impeller/renderer/backend/vulkan/sampler_vk.h"
 #include "impeller/renderer/backend/vulkan/texture_vk.h"
-#include "impeller/renderer/command.h"
+#include "vulkan/vulkan_structs.hpp"
 
 namespace impeller {
 
-ComputePassVK::ComputePassVK(std::weak_ptr<const Context> context,
-                             std::weak_ptr<CommandBufferVK> command_buffer)
+ComputePassVK::ComputePassVK(std::shared_ptr<const Context> context,
+                             std::shared_ptr<CommandBufferVK> command_buffer)
     : ComputePass(std::move(context)),
       command_buffer_(std::move(command_buffer)) {
+  // TOOD(dnfield): This should be moved to caps. But for now keeping this
+  // in parallel with Metal.
+  max_wg_size_ = ContextVK::Cast(*context_)
+                     .GetPhysicalDevice()
+                     .getProperties()
+                     .limits.maxComputeWorkGroupSize;
   is_valid_ = true;
 }
 
@@ -33,125 +39,182 @@
   label_ = label;
 }
 
-static bool UpdateBindingLayouts(const Bindings& bindings,
-                                 const vk::CommandBuffer& buffer) {
-  BarrierVK barrier;
-  barrier.cmd_buffer = buffer;
-  barrier.src_access = vk::AccessFlagBits::eTransferWrite;
-  barrier.src_stage = vk::PipelineStageFlagBits::eTransfer;
-  barrier.dst_access = vk::AccessFlagBits::eShaderRead;
-  barrier.dst_stage = vk::PipelineStageFlagBits::eComputeShader;
-
-  barrier.new_layout = vk::ImageLayout::eShaderReadOnlyOptimal;
-
-  for (const TextureAndSampler& data : bindings.sampled_images) {
-    if (!TextureVK::Cast(*data.texture.resource).SetLayout(barrier)) {
-      return false;
-    }
-  }
-  return true;
+// |RenderPass|
+void ComputePassVK::SetCommandLabel(std::string_view label) {
+#ifdef IMPELLER_DEBUG
+  command_buffer_->GetEncoder()->PushDebugGroup(label);
+  has_label_ = true;
+#endif  // IMPELLER_DEBUG
 }
 
-static bool UpdateBindingLayouts(const ComputeCommand& command,
-                                 const vk::CommandBuffer& buffer) {
-  return UpdateBindingLayouts(command.bindings, buffer);
+// |ComputePass|
+void ComputePassVK::SetPipeline(
+    const std::shared_ptr<Pipeline<ComputePipelineDescriptor>>& pipeline) {
+  const auto& pipeline_vk = ComputePipelineVK::Cast(*pipeline);
+  const vk::CommandBuffer& command_buffer_vk =
+      command_buffer_->GetEncoder()->GetCommandBuffer();
+  command_buffer_vk.bindPipeline(vk::PipelineBindPoint::eCompute,
+                                 pipeline_vk.GetPipeline());
+  pipeline_layout_ = pipeline_vk.GetPipelineLayout();
+
+  auto descriptor_result =
+      command_buffer_->GetEncoder()->AllocateDescriptorSets(
+          pipeline_vk.GetDescriptorSetLayout(), ContextVK::Cast(*context_));
+  if (!descriptor_result.ok()) {
+    return;
+  }
+  descriptor_set_ = descriptor_result.value();
+  pipeline_valid_ = true;
 }
 
-static bool UpdateBindingLayouts(const std::vector<ComputeCommand>& commands,
-                                 const vk::CommandBuffer& buffer) {
-  for (const ComputeCommand& command : commands) {
-    if (!UpdateBindingLayouts(command, buffer)) {
-      return false;
-    }
-  }
-  return true;
-}
-
-bool ComputePassVK::OnEncodeCommands(const Context& context,
-                                     const ISize& grid_size,
-                                     const ISize& thread_group_size) const {
-  TRACE_EVENT0("impeller", "ComputePassVK::EncodeCommands");
-  if (!IsValid()) {
-    return false;
+// |ComputePass|
+fml::Status ComputePassVK::Compute(const ISize& grid_size) {
+  if (grid_size.IsEmpty() || !pipeline_valid_) {
+    bound_image_offset_ = 0u;
+    bound_buffer_offset_ = 0u;
+    descriptor_write_offset_ = 0u;
+    has_label_ = false;
+    pipeline_valid_ = false;
+    return fml::Status(fml::StatusCode::kCancelled,
+                       "Invalid pipeline or empty grid.");
   }
 
-  FML_DCHECK(!grid_size.IsEmpty() && !thread_group_size.IsEmpty());
-
-  const auto& vk_context = ContextVK::Cast(context);
-  auto command_buffer = command_buffer_.lock();
-  if (!command_buffer) {
-    VALIDATION_LOG << "Command buffer died before commands could be encoded.";
-    return false;
-  }
-  auto encoder = command_buffer->GetEncoder();
-  if (!encoder) {
-    return false;
+  const ContextVK& context_vk = ContextVK::Cast(*context_);
+  for (auto i = 0u; i < descriptor_write_offset_; i++) {
+    write_workspace_[i].dstSet = descriptor_set_;
   }
 
-  fml::ScopedCleanupClosure pop_marker(
-      [&encoder]() { encoder->PopDebugGroup(); });
-  if (!label_.empty()) {
-    encoder->PushDebugGroup(label_.c_str());
+  context_vk.GetDevice().updateDescriptorSets(descriptor_write_offset_,
+                                              write_workspace_.data(), 0u, {});
+  const vk::CommandBuffer& command_buffer_vk =
+      command_buffer_->GetEncoder()->GetCommandBuffer();
+
+  command_buffer_vk.bindDescriptorSets(
+      vk::PipelineBindPoint::eCompute,  // bind point
+      pipeline_layout_,                 // layout
+      0,                                // first set
+      1,                                // set count
+      &descriptor_set_,                 // sets
+      0,                                // offset count
+      nullptr                           // offsets
+  );
+
+  int64_t width = grid_size.width;
+  int64_t height = grid_size.height;
+
+  // Special case for linear processing.
+  if (height == 1) {
+    int64_t minimum = 1;
+    int64_t threadGroups = std::max(
+        static_cast<int64_t>(std::ceil(width * 1.0 / max_wg_size_[0] * 1.0)),
+        minimum);
+    command_buffer_vk.dispatch(threadGroups, 1, 1);
   } else {
-    pop_marker.Release();
+    while (width > max_wg_size_[0]) {
+      width = std::max(static_cast<int64_t>(1), width / 2);
+    }
+    while (height > max_wg_size_[1]) {
+      height = std::max(static_cast<int64_t>(1), height / 2);
+    }
+    command_buffer_vk.dispatch(width, height, 1);
   }
-  auto cmd_buffer = encoder->GetCommandBuffer();
 
-  if (!UpdateBindingLayouts(commands_, cmd_buffer)) {
-    VALIDATION_LOG << "Could not update binding layouts for compute pass.";
+#ifdef IMPELLER_DEBUG
+  if (has_label_) {
+    command_buffer_->GetEncoder()->PopDebugGroup();
+  }
+  has_label_ = false;
+#endif  // IMPELLER_DEBUG
+
+  bound_image_offset_ = 0u;
+  bound_buffer_offset_ = 0u;
+  descriptor_write_offset_ = 0u;
+  has_label_ = false;
+  pipeline_valid_ = false;
+
+  return fml::Status();
+}
+
+// |ResourceBinder|
+bool ComputePassVK::BindResource(ShaderStage stage,
+                                 DescriptorType type,
+                                 const ShaderUniformSlot& slot,
+                                 const ShaderMetadata& metadata,
+                                 BufferView view) {
+  return BindResource(slot.binding, type, view);
+}
+
+// |ResourceBinder|
+bool ComputePassVK::BindResource(ShaderStage stage,
+                                 DescriptorType type,
+                                 const SampledImageSlot& slot,
+                                 const ShaderMetadata& metadata,
+                                 std::shared_ptr<const Texture> texture,
+                                 std::shared_ptr<const Sampler> sampler) {
+  if (bound_image_offset_ >= kMaxBindings) {
     return false;
   }
-  auto& allocator = *context.GetResourceAllocator();
+  const TextureVK& texture_vk = TextureVK::Cast(*texture);
+  const SamplerVK& sampler_vk = SamplerVK::Cast(*sampler);
 
-  TRACE_EVENT0("impeller", "EncodeComputePassCommands");
-  for (const auto& command : commands_) {
-    auto desc_set_result = AllocateAndBindDescriptorSets(
-        vk_context, encoder, allocator, command, image_workspace_,
-        buffer_workspace_, write_workspace_);
-    if (!desc_set_result.ok()) {
-      return false;
-    }
-    auto desc_set = desc_set_result.value();
-
-    const auto& pipeline_vk = ComputePipelineVK::Cast(*command.pipeline);
-
-    cmd_buffer.bindPipeline(vk::PipelineBindPoint::eCompute,
-                            pipeline_vk.GetPipeline());
-    cmd_buffer.bindDescriptorSets(
-        vk::PipelineBindPoint::eCompute,  // bind point
-        pipeline_vk.GetPipelineLayout(),  // layout
-        0,                                // first set
-        {vk::DescriptorSet{desc_set}},    // sets
-        nullptr                           // offsets
-    );
-
-    // TOOD(dnfield): This should be moved to caps. But for now keeping this
-    // in parallel with Metal.
-    auto device_properties = vk_context.GetPhysicalDevice().getProperties();
-
-    auto max_wg_size = device_properties.limits.maxComputeWorkGroupSize;
-
-    int64_t width = grid_size.width;
-    int64_t height = grid_size.height;
-
-    // Special case for linear processing.
-    if (height == 1) {
-      int64_t minimum = 1;
-      int64_t threadGroups = std::max(
-          static_cast<int64_t>(std::ceil(width * 1.0 / max_wg_size[0] * 1.0)),
-          minimum);
-      cmd_buffer.dispatch(threadGroups, 1, 1);
-    } else {
-      while (width > max_wg_size[0]) {
-        width = std::max(static_cast<int64_t>(1), width / 2);
-      }
-      while (height > max_wg_size[1]) {
-        height = std::max(static_cast<int64_t>(1), height / 2);
-      }
-      cmd_buffer.dispatch(width, height, 1);
-    }
+  if (!command_buffer_->GetEncoder()->Track(texture) ||
+      !command_buffer_->GetEncoder()->Track(sampler_vk.GetSharedSampler())) {
+    return false;
   }
 
+  vk::DescriptorImageInfo image_info;
+  image_info.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
+  image_info.sampler = sampler_vk.GetSampler();
+  image_info.imageView = texture_vk.GetImageView();
+  image_workspace_[bound_image_offset_++] = image_info;
+
+  vk::WriteDescriptorSet write_set;
+  write_set.dstBinding = slot.binding;
+  write_set.descriptorCount = 1u;
+  write_set.descriptorType = ToVKDescriptorType(type);
+  write_set.pImageInfo = &image_workspace_[bound_image_offset_ - 1];
+
+  write_workspace_[descriptor_write_offset_++] = write_set;
+  return true;
+}
+
+bool ComputePassVK::BindResource(size_t binding,
+                                 DescriptorType type,
+                                 const BufferView& view) {
+  if (bound_buffer_offset_ >= kMaxBindings) {
+    return false;
+  }
+
+  const std::shared_ptr<const DeviceBuffer>& device_buffer = view.buffer;
+  auto buffer = DeviceBufferVK::Cast(*device_buffer).GetBuffer();
+  if (!buffer) {
+    return false;
+  }
+
+  if (!command_buffer_->GetEncoder()->Track(device_buffer)) {
+    return false;
+  }
+
+  uint32_t offset = view.range.offset;
+
+  vk::DescriptorBufferInfo buffer_info;
+  buffer_info.buffer = buffer;
+  buffer_info.offset = offset;
+  buffer_info.range = view.range.length;
+  buffer_workspace_[bound_buffer_offset_++] = buffer_info;
+
+  vk::WriteDescriptorSet write_set;
+  write_set.dstBinding = binding;
+  write_set.descriptorCount = 1u;
+  write_set.descriptorType = ToVKDescriptorType(type);
+  write_set.pBufferInfo = &buffer_workspace_[bound_buffer_offset_ - 1];
+
+  write_workspace_[descriptor_write_offset_++] = write_set;
+  return true;
+}
+
+// |ComputePass|
+bool ComputePassVK::EncodeCommands() const {
   return true;
 }
 
diff --git a/impeller/renderer/backend/vulkan/compute_pass_vk.h b/impeller/renderer/backend/vulkan/compute_pass_vk.h
index 2e2259c..7b6fad8 100644
--- a/impeller/renderer/backend/vulkan/compute_pass_vk.h
+++ b/impeller/renderer/backend/vulkan/compute_pass_vk.h
@@ -5,8 +5,6 @@
 #ifndef FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_COMPUTE_PASS_VK_H_
 #define FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_COMPUTE_PASS_VK_H_
 
-#include "flutter/fml/macros.h"
-#include "impeller/renderer/backend/vulkan/binding_helpers_vk.h"
 #include "impeller/renderer/backend/vulkan/command_encoder_vk.h"
 #include "impeller/renderer/compute_pass.h"
 
@@ -22,16 +20,26 @@
  private:
   friend class CommandBufferVK;
 
-  std::weak_ptr<CommandBufferVK> command_buffer_;
+  std::shared_ptr<CommandBufferVK> command_buffer_;
   std::string label_;
+  std::array<uint32_t, 3> max_wg_size_ = {};
   bool is_valid_ = false;
-  mutable std::array<vk::DescriptorImageInfo, kMaxBindings> image_workspace_;
-  mutable std::array<vk::DescriptorBufferInfo, kMaxBindings> buffer_workspace_;
-  mutable std::array<vk::WriteDescriptorSet, kMaxBindings + kMaxBindings>
-      write_workspace_;
 
-  ComputePassVK(std::weak_ptr<const Context> context,
-                std::weak_ptr<CommandBufferVK> command_buffer);
+  // Per-command state.
+  std::array<vk::DescriptorImageInfo, kMaxBindings> image_workspace_;
+  std::array<vk::DescriptorBufferInfo, kMaxBindings> buffer_workspace_;
+  std::array<vk::WriteDescriptorSet, kMaxBindings + kMaxBindings>
+      write_workspace_;
+  size_t bound_image_offset_ = 0u;
+  size_t bound_buffer_offset_ = 0u;
+  size_t descriptor_write_offset_ = 0u;
+  bool has_label_ = false;
+  bool pipeline_valid_ = false;
+  vk::DescriptorSet descriptor_set_ = {};
+  vk::PipelineLayout pipeline_layout_ = {};
+
+  ComputePassVK(std::shared_ptr<const Context> context,
+                std::shared_ptr<CommandBufferVK> command_buffer);
 
   // |ComputePass|
   bool IsValid() const override;
@@ -40,9 +48,36 @@
   void OnSetLabel(const std::string& label) override;
 
   // |ComputePass|
-  bool OnEncodeCommands(const Context& context,
-                        const ISize& grid_size,
-                        const ISize& thread_group_size) const override;
+  bool EncodeCommands() const override;
+
+  // |ComputePass|
+  void SetCommandLabel(std::string_view label) override;
+
+  // |ComputePass|
+  void SetPipeline(const std::shared_ptr<Pipeline<ComputePipelineDescriptor>>&
+                       pipeline) override;
+
+  // |ComputePass|
+  fml::Status Compute(const ISize& grid_size) override;
+
+  // |ResourceBinder|
+  bool BindResource(ShaderStage stage,
+                    DescriptorType type,
+                    const ShaderUniformSlot& slot,
+                    const ShaderMetadata& metadata,
+                    BufferView view) override;
+
+  // |ResourceBinder|
+  bool BindResource(ShaderStage stage,
+                    DescriptorType type,
+                    const SampledImageSlot& slot,
+                    const ShaderMetadata& metadata,
+                    std::shared_ptr<const Texture> texture,
+                    std::shared_ptr<const Sampler> sampler) override;
+
+  bool BindResource(size_t binding,
+                    DescriptorType type,
+                    const BufferView& view);
 };
 
 }  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/pass_bindings_cache.cc b/impeller/renderer/backend/vulkan/pass_bindings_cache.cc
deleted file mode 100644
index 00d8774..0000000
--- a/impeller/renderer/backend/vulkan/pass_bindings_cache.cc
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "impeller/renderer/backend/vulkan/pass_bindings_cache.h"
-
-namespace impeller {
-void PassBindingsCache::BindPipeline(vk::CommandBuffer command_buffer,
-                                     vk::PipelineBindPoint pipeline_bind_point,
-                                     vk::Pipeline pipeline) {
-  switch (pipeline_bind_point) {
-    case vk::PipelineBindPoint::eGraphics:
-      if (graphics_pipeline_.has_value() &&
-          graphics_pipeline_.value() == pipeline) {
-        return;
-      }
-      graphics_pipeline_ = pipeline;
-      break;
-    case vk::PipelineBindPoint::eCompute:
-      if (compute_pipeline_.has_value() &&
-          compute_pipeline_.value() == pipeline) {
-        return;
-      }
-      compute_pipeline_ = pipeline;
-      break;
-    default:
-      break;
-  }
-  command_buffer.bindPipeline(pipeline_bind_point, pipeline);
-}
-
-void PassBindingsCache::SetStencilReference(vk::CommandBuffer command_buffer,
-                                            vk::StencilFaceFlags face_mask,
-                                            uint32_t reference) {
-  if (stencil_face_flags_.has_value() &&
-      face_mask == stencil_face_flags_.value() &&
-      reference == stencil_reference_) {
-    return;
-  }
-  stencil_face_flags_ = face_mask;
-  stencil_reference_ = reference;
-  command_buffer.setStencilReference(face_mask, reference);
-}
-
-void PassBindingsCache::SetScissor(vk::CommandBuffer command_buffer,
-                                   uint32_t first_scissor,
-                                   uint32_t scissor_count,
-                                   const vk::Rect2D* scissors) {
-  if (first_scissor == 0 && scissor_count == 1) {
-    if (scissors_.has_value() && scissors_.value() == scissors[0]) {
-      return;
-    }
-    scissors_ = scissors[0];
-  }
-  command_buffer.setScissor(first_scissor, scissor_count, scissors);
-}
-
-void PassBindingsCache::SetViewport(vk::CommandBuffer command_buffer,
-                                    uint32_t first_viewport,
-                                    uint32_t viewport_count,
-                                    const vk::Viewport* viewports) {
-  if (first_viewport == 0 && viewport_count == 1) {
-    // Note that this is doing equality checks on floating point numbers.
-    if (viewport_.has_value() && viewport_.value() == viewports[0]) {
-      return;
-    }
-    viewport_ = viewports[0];
-  }
-  command_buffer.setViewport(first_viewport, viewport_count, viewports);
-}
-
-}  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/pass_bindings_cache.h b/impeller/renderer/backend/vulkan/pass_bindings_cache.h
deleted file mode 100644
index 9eccee0..0000000
--- a/impeller/renderer/backend/vulkan/pass_bindings_cache.h
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_PASS_BINDINGS_CACHE_H_
-#define FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_PASS_BINDINGS_CACHE_H_
-
-#include <cstdint>
-#include <optional>
-
-#include "flutter/impeller/renderer/backend/vulkan/vk.h"
-
-namespace impeller {
-
-class PassBindingsCache {
- public:
-  void BindPipeline(vk::CommandBuffer command_buffer,
-                    vk::PipelineBindPoint pipeline_bind_point,
-                    vk::Pipeline pipeline);
-
-  void SetStencilReference(vk::CommandBuffer command_buffer,
-                           vk::StencilFaceFlags face_mask,
-                           uint32_t reference);
-
-  void SetScissor(vk::CommandBuffer command_buffer,
-                  uint32_t first_scissor,
-                  uint32_t scissor_count,
-                  const vk::Rect2D* scissors);
-
-  void SetViewport(vk::CommandBuffer command_buffer,
-                   uint32_t first_viewport,
-                   uint32_t viewport_count,
-                   const vk::Viewport* viewports);
-
- private:
-  // bindPipeline
-  std::optional<vk::Pipeline> graphics_pipeline_;
-  std::optional<vk::Pipeline> compute_pipeline_;
-  // setStencilReference
-  std::optional<vk::StencilFaceFlags> stencil_face_flags_;
-  uint32_t stencil_reference_ = 0;
-  // setScissor
-  std::optional<vk::Rect2D> scissors_;
-  // setViewport
-  std::optional<vk::Viewport> viewport_;
-};
-
-}  // namespace impeller
-
-#endif  // FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_PASS_BINDINGS_CACHE_H_
diff --git a/impeller/renderer/backend/vulkan/pass_bindings_cache_unittests.cc b/impeller/renderer/backend/vulkan/pass_bindings_cache_unittests.cc
deleted file mode 100644
index e76213c..0000000
--- a/impeller/renderer/backend/vulkan/pass_bindings_cache_unittests.cc
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "flutter/testing/testing.h"
-#include "impeller/renderer/backend/vulkan/command_encoder_vk.h"
-#include "impeller/renderer/backend/vulkan/pass_bindings_cache.h"
-#include "impeller/renderer/backend/vulkan/test/mock_vulkan.h"
-
-namespace impeller {
-namespace testing {
-
-namespace {
-int32_t CountStringViewInstances(const std::vector<std::string>& strings,
-                                 std::string_view target) {
-  int32_t count = 0;
-  for (const std::string& str : strings) {
-    if (str == target) {
-      count++;
-    }
-  }
-  return count;
-}
-}  // namespace
-
-TEST(PassBindingsCacheTest, bindPipeline) {
-  auto context = MockVulkanContextBuilder().Build();
-  PassBindingsCache cache;
-  auto encoder = std::make_unique<CommandEncoderFactoryVK>(context)->Create();
-  auto buffer = encoder->GetCommandBuffer();
-  VkPipeline vk_pipeline = reinterpret_cast<VkPipeline>(0xfeedface);
-  vk::Pipeline pipeline(vk_pipeline);
-  cache.BindPipeline(buffer, vk::PipelineBindPoint::eGraphics, pipeline);
-  cache.BindPipeline(buffer, vk::PipelineBindPoint::eGraphics, pipeline);
-  std::shared_ptr<std::vector<std::string>> functions =
-      GetMockVulkanFunctions(context->GetDevice());
-  EXPECT_EQ(CountStringViewInstances(*functions, "vkCmdBindPipeline"), 1);
-}
-
-TEST(PassBindingsCacheTest, setStencilReference) {
-  auto context = MockVulkanContextBuilder().Build();
-  PassBindingsCache cache;
-  auto encoder = std::make_unique<CommandEncoderFactoryVK>(context)->Create();
-  auto buffer = encoder->GetCommandBuffer();
-  cache.SetStencilReference(
-      buffer, vk::StencilFaceFlagBits::eVkStencilFrontAndBack, 123);
-  cache.SetStencilReference(
-      buffer, vk::StencilFaceFlagBits::eVkStencilFrontAndBack, 123);
-  std::shared_ptr<std::vector<std::string>> functions =
-      GetMockVulkanFunctions(context->GetDevice());
-  EXPECT_EQ(CountStringViewInstances(*functions, "vkCmdSetStencilReference"),
-            1);
-}
-
-TEST(PassBindingsCacheTest, setScissor) {
-  auto context = MockVulkanContextBuilder().Build();
-  PassBindingsCache cache;
-  auto encoder = std::make_unique<CommandEncoderFactoryVK>(context)->Create();
-  auto buffer = encoder->GetCommandBuffer();
-  vk::Rect2D scissors;
-  cache.SetScissor(buffer, 0, 1, &scissors);
-  cache.SetScissor(buffer, 0, 1, &scissors);
-  std::shared_ptr<std::vector<std::string>> functions =
-      GetMockVulkanFunctions(context->GetDevice());
-  EXPECT_EQ(CountStringViewInstances(*functions, "vkCmdSetScissor"), 1);
-}
-
-TEST(PassBindingsCacheTest, setViewport) {
-  auto context = MockVulkanContextBuilder().Build();
-  PassBindingsCache cache;
-  auto encoder = std::make_unique<CommandEncoderFactoryVK>(context)->Create();
-  auto buffer = encoder->GetCommandBuffer();
-  vk::Viewport viewports;
-  cache.SetViewport(buffer, 0, 1, &viewports);
-  cache.SetViewport(buffer, 0, 1, &viewports);
-  std::shared_ptr<std::vector<std::string>> functions =
-      GetMockVulkanFunctions(context->GetDevice());
-  EXPECT_EQ(CountStringViewInstances(*functions, "vkCmdSetViewport"), 1);
-}
-
-}  // namespace testing
-}  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/pipeline_vk.h b/impeller/renderer/backend/vulkan/pipeline_vk.h
index ffe2451..c27d186 100644
--- a/impeller/renderer/backend/vulkan/pipeline_vk.h
+++ b/impeller/renderer/backend/vulkan/pipeline_vk.h
@@ -14,6 +14,10 @@
 
 namespace impeller {
 
+// Limit on the total number of buffer and image bindings that allow the Vulkan
+// backend to avoid dynamic heap allocations.
+static constexpr size_t kMaxBindings = 32;
+
 class PipelineVK final
     : public Pipeline<PipelineDescriptor>,
       public BackendCast<PipelineVK, Pipeline<PipelineDescriptor>> {
diff --git a/impeller/renderer/backend/vulkan/render_pass_vk.cc b/impeller/renderer/backend/vulkan/render_pass_vk.cc
index 0489bfc..5bbe7b4 100644
--- a/impeller/renderer/backend/vulkan/render_pass_vk.cc
+++ b/impeller/renderer/backend/vulkan/render_pass_vk.cc
@@ -14,7 +14,6 @@
 #include "impeller/core/formats.h"
 #include "impeller/core/texture.h"
 #include "impeller/renderer/backend/vulkan/barrier_vk.h"
-#include "impeller/renderer/backend/vulkan/binding_helpers_vk.h"
 #include "impeller/renderer/backend/vulkan/command_buffer_vk.h"
 #include "impeller/renderer/backend/vulkan/command_encoder_vk.h"
 #include "impeller/renderer/backend/vulkan/context_vk.h"
diff --git a/impeller/renderer/backend/vulkan/render_pass_vk.h b/impeller/renderer/backend/vulkan/render_pass_vk.h
index c7da1ae..ad9e89d 100644
--- a/impeller/renderer/backend/vulkan/render_pass_vk.h
+++ b/impeller/renderer/backend/vulkan/render_pass_vk.h
@@ -6,7 +6,6 @@
 #define FLUTTER_IMPELLER_RENDERER_BACKEND_VULKAN_RENDER_PASS_VK_H_
 
 #include "impeller/core/buffer_view.h"
-#include "impeller/renderer/backend/vulkan/binding_helpers_vk.h"
 #include "impeller/renderer/backend/vulkan/context_vk.h"
 #include "impeller/renderer/backend/vulkan/pipeline_vk.h"
 #include "impeller/renderer/backend/vulkan/shared_object_vk.h"
diff --git a/impeller/renderer/compute_command.cc b/impeller/renderer/compute_command.cc
deleted file mode 100644
index e1a0601..0000000
--- a/impeller/renderer/compute_command.cc
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "impeller/renderer/compute_command.h"
-
-#include <utility>
-
-#include "impeller/base/validation.h"
-#include "impeller/core/formats.h"
-#include "impeller/core/shader_types.h"
-
-namespace impeller {
-
-bool ComputeCommand::BindResource(ShaderStage stage,
-                                  DescriptorType type,
-                                  const ShaderUniformSlot& slot,
-                                  const ShaderMetadata& metadata,
-                                  BufferView view) {
-  if (stage != ShaderStage::kCompute) {
-    VALIDATION_LOG << "Use Command for non-compute shader stages.";
-    return false;
-  }
-  if (!view) {
-    return false;
-  }
-
-  bindings.buffers.emplace_back(
-      BufferAndUniformSlot{.slot = slot, .view = {&metadata, std::move(view)}});
-  return true;
-}
-
-bool ComputeCommand::BindResource(ShaderStage stage,
-                                  DescriptorType type,
-                                  const SampledImageSlot& slot,
-                                  const ShaderMetadata& metadata,
-                                  std::shared_ptr<const Texture> texture,
-                                  std::shared_ptr<const Sampler> sampler) {
-  if (stage != ShaderStage::kCompute) {
-    VALIDATION_LOG << "Use Command for non-compute shader stages.";
-    return false;
-  }
-  if (!sampler || !sampler->IsValid()) {
-    return false;
-  }
-  if (!texture || !texture->IsValid()) {
-    return false;
-  }
-
-  bindings.sampled_images.emplace_back(TextureAndSampler{
-      .slot = slot,
-      .texture = {&metadata, std::move(texture)},
-      .sampler = std::move(sampler),
-  });
-
-  return false;
-}
-
-}  // namespace impeller
diff --git a/impeller/renderer/compute_command.h b/impeller/renderer/compute_command.h
deleted file mode 100644
index c0c56e9..0000000
--- a/impeller/renderer/compute_command.h
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef FLUTTER_IMPELLER_RENDERER_COMPUTE_COMMAND_H_
-#define FLUTTER_IMPELLER_RENDERER_COMPUTE_COMMAND_H_
-
-#include <memory>
-#include <string>
-
-#include "impeller/core/buffer_view.h"
-#include "impeller/core/formats.h"
-#include "impeller/core/resource_binder.h"
-#include "impeller/core/sampler.h"
-#include "impeller/core/shader_types.h"
-#include "impeller/core/texture.h"
-#include "impeller/renderer/command.h"
-#include "impeller/renderer/compute_pipeline_descriptor.h"
-#include "impeller/renderer/pipeline.h"
-
-namespace impeller {
-
-//------------------------------------------------------------------------------
-/// @brief      An object used to specify compute work to the GPU along with
-///             references to resources the GPU will used when doing said work.
-///
-///             To construct a valid command, follow these steps:
-///             * Specify a valid pipeline.
-///             * (Optional) Specify a debug label.
-///
-///             Command are very lightweight objects and can be created
-///             frequently and on demand. The resources referenced in commands
-///             views into buffers managed by other allocators and resource
-///             managers.
-///
-struct ComputeCommand : public ResourceBinder {
-  //----------------------------------------------------------------------------
-  /// The pipeline to use for this command.
-  ///
-  std::shared_ptr<Pipeline<ComputePipelineDescriptor>> pipeline;
-  //----------------------------------------------------------------------------
-  /// The buffer, texture, and sampler bindings used by the compute pipeline
-  /// stage.
-  ///
-  Bindings bindings;
-
-#ifdef IMPELLER_DEBUG
-  //----------------------------------------------------------------------------
-  /// The debugging label to use for the command.
-  ///
-  std::string label;
-#endif  // IMPELLER_DEBUG
-
-  // |ResourceBinder|
-  bool BindResource(ShaderStage stage,
-                    DescriptorType type,
-                    const ShaderUniformSlot& slot,
-                    const ShaderMetadata& metadata,
-                    BufferView view) override;
-
-  // |ResourceBinder|
-  bool BindResource(ShaderStage stage,
-                    DescriptorType type,
-                    const SampledImageSlot& slot,
-                    const ShaderMetadata& metadata,
-                    std::shared_ptr<const Texture> texture,
-                    std::shared_ptr<const Sampler> sampler) override;
-
-  constexpr explicit operator bool() const {
-    return pipeline && pipeline->IsValid();
-  }
-};
-
-}  // namespace impeller
-
-#endif  // FLUTTER_IMPELLER_RENDERER_COMPUTE_COMMAND_H_
diff --git a/impeller/renderer/compute_pass.cc b/impeller/renderer/compute_pass.cc
index 8446fff..e069aca 100644
--- a/impeller/renderer/compute_pass.cc
+++ b/impeller/renderer/compute_pass.cc
@@ -3,14 +3,10 @@
 // found in the LICENSE file.
 
 #include "impeller/renderer/compute_pass.h"
-#include <memory>
-
-#include "fml/logging.h"
-#include "impeller/base/validation.h"
 
 namespace impeller {
 
-ComputePass::ComputePass(std::weak_ptr<const Context> context)
+ComputePass::ComputePass(std::shared_ptr<const Context> context)
     : context_(std::move(context)) {}
 
 ComputePass::~ComputePass() = default;
@@ -22,37 +18,4 @@
   OnSetLabel(label);
 }
 
-void ComputePass::SetGridSize(const ISize& size) {
-  grid_size_ = size;
-}
-
-void ComputePass::SetThreadGroupSize(const ISize& size) {
-  thread_group_size_ = size;
-}
-
-bool ComputePass::AddCommand(ComputeCommand command) {
-  if (!command) {
-    VALIDATION_LOG
-        << "Attempted to add an invalid command to the compute pass.";
-    return false;
-  }
-
-  commands_.emplace_back(std::move(command));
-  return true;
-}
-
-bool ComputePass::EncodeCommands() const {
-  if (grid_size_.IsEmpty() || thread_group_size_.IsEmpty()) {
-    FML_DLOG(WARNING) << "Attempted to encode a compute pass with an empty "
-                         "grid or thread group size.";
-    return false;
-  }
-  auto context = context_.lock();
-  // The context could have been collected in the meantime.
-  if (!context) {
-    return false;
-  }
-  return OnEncodeCommands(*context, grid_size_, thread_group_size_);
-}
-
 }  // namespace impeller
diff --git a/impeller/renderer/compute_pass.h b/impeller/renderer/compute_pass.h
index 4ba6e80..40c313e 100644
--- a/impeller/renderer/compute_pass.h
+++ b/impeller/renderer/compute_pass.h
@@ -7,20 +7,20 @@
 
 #include <string>
 
-#include "impeller/renderer/compute_command.h"
+#include "fml/status.h"
+#include "impeller/core/resource_binder.h"
+#include "impeller/renderer/compute_pipeline_descriptor.h"
+#include "impeller/renderer/pipeline_descriptor.h"
 
 namespace impeller {
 
-class HostBuffer;
-class Allocator;
-
 //------------------------------------------------------------------------------
 /// @brief      Compute passes encode compute shader into the underlying command
 ///             buffer.
 ///
 /// @see        `CommandBuffer`
 ///
-class ComputePass {
+class ComputePass : public ResourceBinder {
  public:
   virtual ~ComputePass();
 
@@ -28,48 +28,29 @@
 
   void SetLabel(const std::string& label);
 
-  void SetGridSize(const ISize& size);
+  virtual void SetCommandLabel(std::string_view label) = 0;
 
-  void SetThreadGroupSize(const ISize& size);
+  virtual void SetPipeline(
+      const std::shared_ptr<Pipeline<ComputePipelineDescriptor>>& pipeline) = 0;
 
-  //----------------------------------------------------------------------------
-  /// @brief      Record a command for subsequent encoding to the underlying
-  ///             command buffer. No work is encoded into the command buffer at
-  ///             this time.
-  ///
-  /// @param[in]  command  The command
-  ///
-  /// @return     If the command was valid for subsequent commitment.
-  ///
-  bool AddCommand(ComputeCommand command);
+  virtual fml::Status Compute(const ISize& grid_size) = 0;
 
   //----------------------------------------------------------------------------
   /// @brief      Encode the recorded commands to the underlying command buffer.
   ///
-  /// @param      transients_allocator  The transients allocator.
-  ///
   /// @return     If the commands were encoded to the underlying command
   ///             buffer.
   ///
-  bool EncodeCommands() const;
+  virtual bool EncodeCommands() const = 0;
 
  protected:
-  const std::weak_ptr<const Context> context_;
-  std::vector<ComputeCommand> commands_;
+  const std::shared_ptr<const Context> context_;
 
-  explicit ComputePass(std::weak_ptr<const Context> context);
+  explicit ComputePass(std::shared_ptr<const Context> context);
 
   virtual void OnSetLabel(const std::string& label) = 0;
 
-  virtual bool OnEncodeCommands(const Context& context,
-                                const ISize& grid_size,
-                                const ISize& thread_group_size) const = 0;
-
  private:
-  std::shared_ptr<HostBuffer> transients_buffer_;
-  ISize grid_size_ = ISize(32, 32);
-  ISize thread_group_size_ = ISize(32, 32);
-
   ComputePass(const ComputePass&) = delete;
 
   ComputePass& operator=(const ComputePass&) = delete;
diff --git a/impeller/renderer/compute_subgroup_unittests.cc b/impeller/renderer/compute_subgroup_unittests.cc
index ee26381..144b635 100644
--- a/impeller/renderer/compute_subgroup_unittests.cc
+++ b/impeller/renderer/compute_subgroup_unittests.cc
@@ -24,7 +24,6 @@
 #include "impeller/geometry/path_component.h"
 #include "impeller/playground/compute_playground_test.h"
 #include "impeller/renderer/command_buffer.h"
-#include "impeller/renderer/compute_command.h"
 #include "impeller/renderer/compute_pipeline_builder.h"
 #include "impeller/renderer/compute_tessellator.h"
 #include "impeller/renderer/path_polyline.comp.h"
diff --git a/impeller/renderer/compute_tessellator.cc b/impeller/renderer/compute_tessellator.cc
index ebcf76d..5cd9877 100644
--- a/impeller/renderer/compute_tessellator.cc
+++ b/impeller/renderer/compute_tessellator.cc
@@ -121,21 +121,17 @@
         context->GetPipelineLibrary()->GetPipeline(pipeline_desc).Get();
     FML_DCHECK(compute_pipeline);
 
-    pass->SetGridSize(ISize(line_count, 1));
-    pass->SetThreadGroupSize(ISize(line_count, 1));
+    pass->SetPipeline(compute_pipeline);
+    pass->SetCommandLabel("Generate Polyline");
 
-    ComputeCommand cmd;
-    DEBUG_COMMAND_INFO(cmd, "Generate Polyline");
-    cmd.pipeline = compute_pipeline;
+    PS::BindConfig(*pass, host_buffer.EmplaceUniform(config));
+    PS::BindCubics(*pass, host_buffer.EmplaceStorageBuffer(cubics));
+    PS::BindQuads(*pass, host_buffer.EmplaceStorageBuffer(quads));
+    PS::BindLines(*pass, host_buffer.EmplaceStorageBuffer(lines));
+    PS::BindComponents(*pass, host_buffer.EmplaceStorageBuffer(components));
+    PS::BindPolyline(*pass, DeviceBuffer::AsBufferView(polyline_buffer));
 
-    PS::BindConfig(cmd, host_buffer.EmplaceUniform(config));
-    PS::BindCubics(cmd, host_buffer.EmplaceStorageBuffer(cubics));
-    PS::BindQuads(cmd, host_buffer.EmplaceStorageBuffer(quads));
-    PS::BindLines(cmd, host_buffer.EmplaceStorageBuffer(lines));
-    PS::BindComponents(cmd, host_buffer.EmplaceStorageBuffer(components));
-    PS::BindPolyline(cmd, DeviceBuffer::AsBufferView(polyline_buffer));
-
-    if (!pass->AddCommand(std::move(cmd))) {
+    if (!pass->Compute(ISize(line_count, 1)).ok()) {
       return Status::kCommandInvalid;
     }
   }
@@ -149,12 +145,8 @@
         context->GetPipelineLibrary()->GetPipeline(pipeline_desc).Get();
     FML_DCHECK(compute_pipeline);
 
-    pass->SetGridSize(ISize(line_count, 1));
-    pass->SetThreadGroupSize(ISize(line_count, 1));
-
-    ComputeCommand cmd;
-    DEBUG_COMMAND_INFO(cmd, "Compute Stroke");
-    cmd.pipeline = compute_pipeline;
+    pass->SetPipeline(compute_pipeline);
+    pass->SetCommandLabel("Compute Stroke");
 
     SS::Config config{
         .width = stroke_width_,
@@ -162,13 +154,13 @@
         .join = static_cast<uint32_t>(stroke_join_),
         .miter_limit = miter_limit_,
     };
-    SS::BindConfig(cmd, host_buffer.EmplaceUniform(config));
+    SS::BindConfig(*pass, host_buffer.EmplaceUniform(config));
 
-    SS::BindPolyline(cmd, DeviceBuffer::AsBufferView(polyline_buffer));
-    SS::BindVertexBufferCount(cmd, std::move(vertex_buffer_count));
-    SS::BindVertexBuffer(cmd, std::move(vertex_buffer));
+    SS::BindPolyline(*pass, DeviceBuffer::AsBufferView(polyline_buffer));
+    SS::BindVertexBufferCount(*pass, std::move(vertex_buffer_count));
+    SS::BindVertexBuffer(*pass, std::move(vertex_buffer));
 
-    if (!pass->AddCommand(std::move(cmd))) {
+    if (!pass->Compute(ISize(line_count, 1)).ok()) {
       return Status::kCommandInvalid;
     }
   }
diff --git a/impeller/renderer/compute_unittests.cc b/impeller/renderer/compute_unittests.cc
index ce416c6..d8fd55d 100644
--- a/impeller/renderer/compute_unittests.cc
+++ b/impeller/renderer/compute_unittests.cc
@@ -3,20 +3,14 @@
 // found in the LICENSE file.
 
 #include "flutter/fml/synchronization/waitable_event.h"
-#include "flutter/fml/time/time_point.h"
 #include "flutter/testing/testing.h"
 #include "gmock/gmock.h"
-#include "impeller/base/strings.h"
-#include "impeller/core/formats.h"
 #include "impeller/core/host_buffer.h"
 #include "impeller/fixtures/sample.comp.h"
 #include "impeller/fixtures/stage1.comp.h"
 #include "impeller/fixtures/stage2.comp.h"
-#include "impeller/geometry/path.h"
-#include "impeller/geometry/path_component.h"
 #include "impeller/playground/compute_playground_test.h"
 #include "impeller/renderer/command_buffer.h"
-#include "impeller/renderer/compute_command.h"
 #include "impeller/renderer/compute_pipeline_builder.h"
 #include "impeller/renderer/pipeline_library.h"
 #include "impeller/renderer/prefix_sum_test.comp.h"
@@ -54,11 +48,7 @@
 
   static constexpr size_t kCount = 5;
 
-  pass->SetGridSize(ISize(kCount, 1));
-  pass->SetThreadGroupSize(ISize(kCount, 1));
-
-  ComputeCommand cmd;
-  cmd.pipeline = compute_pipeline;
+  pass->SetPipeline(compute_pipeline);
 
   CS::Info info{.count = kCount};
   CS::Input0<kCount> input_0;
@@ -76,12 +66,12 @@
   auto output_buffer = CreateHostVisibleDeviceBuffer<CS::Output<kCount>>(
       context, "Output Buffer");
 
-  CS::BindInfo(cmd, host_buffer->EmplaceUniform(info));
-  CS::BindInput0(cmd, host_buffer->EmplaceStorageBuffer(input_0));
-  CS::BindInput1(cmd, host_buffer->EmplaceStorageBuffer(input_1));
-  CS::BindOutput(cmd, DeviceBuffer::AsBufferView(output_buffer));
+  CS::BindInfo(*pass, host_buffer->EmplaceUniform(info));
+  CS::BindInput0(*pass, host_buffer->EmplaceStorageBuffer(input_0));
+  CS::BindInput1(*pass, host_buffer->EmplaceStorageBuffer(input_1));
+  CS::BindOutput(*pass, DeviceBuffer::AsBufferView(output_buffer));
 
-  ASSERT_TRUE(pass->AddCommand(std::move(cmd)));
+  ASSERT_TRUE(pass->Compute(ISize(kCount, 1)).ok());
   ASSERT_TRUE(pass->EncodeCommands());
 
   fml::AutoResetWaitableEvent latch;
@@ -131,11 +121,7 @@
 
   static constexpr size_t kCount = 5;
 
-  pass->SetGridSize(ISize(kCount, 1));
-  pass->SetThreadGroupSize(ISize(kCount, 1));
-
-  ComputeCommand cmd;
-  cmd.pipeline = compute_pipeline;
+  pass->SetPipeline(compute_pipeline);
 
   CS::InputData<kCount> input_data;
   input_data.count = kCount;
@@ -146,10 +132,10 @@
   auto output_buffer = CreateHostVisibleDeviceBuffer<CS::OutputData<kCount>>(
       context, "Output Buffer");
 
-  CS::BindInputData(cmd, host_buffer->EmplaceStorageBuffer(input_data));
-  CS::BindOutputData(cmd, DeviceBuffer::AsBufferView(output_buffer));
+  CS::BindInputData(*pass, host_buffer->EmplaceStorageBuffer(input_data));
+  CS::BindOutputData(*pass, DeviceBuffer::AsBufferView(output_buffer));
 
-  ASSERT_TRUE(pass->AddCommand(std::move(cmd)));
+  ASSERT_TRUE(pass->Compute(ISize(kCount, 1)).ok());
   ASSERT_TRUE(pass->EncodeCommands());
 
   fml::AutoResetWaitableEvent latch;
@@ -195,18 +181,14 @@
 
   static constexpr size_t kCount = 2048;
 
-  pass->SetGridSize(ISize(kCount, 1));
-  pass->SetThreadGroupSize(ISize(kCount, 1));
-
-  ComputeCommand cmd;
-  cmd.pipeline = compute_pipeline;
+  pass->SetPipeline(compute_pipeline);
 
   auto output_buffer = CreateHostVisibleDeviceBuffer<CS::OutputData<kCount>>(
       context, "Output Buffer");
 
-  CS::BindOutputData(cmd, DeviceBuffer::AsBufferView(output_buffer));
+  CS::BindOutputData(*pass, DeviceBuffer::AsBufferView(output_buffer));
 
-  ASSERT_TRUE(pass->AddCommand(std::move(cmd)));
+  ASSERT_TRUE(pass->Compute(ISize(kCount, 1)).ok());
   ASSERT_TRUE(pass->EncodeCommands());
 
   fml::AutoResetWaitableEvent latch;
@@ -248,10 +230,7 @@
 
     static constexpr size_t kCount = 1023;
 
-    pass->SetGridSize(ISize(kCount, 1));
-
-    ComputeCommand cmd;
-    cmd.pipeline = compute_pipeline;
+    pass->SetPipeline(compute_pipeline);
 
     CS::InputData<kCount> input_data;
     input_data.count = kCount;
@@ -262,10 +241,10 @@
     auto output_buffer = CreateHostVisibleDeviceBuffer<CS::OutputData<kCount>>(
         context, "Output Buffer");
 
-    CS::BindInputData(cmd, host_buffer->EmplaceStorageBuffer(input_data));
-    CS::BindOutputData(cmd, DeviceBuffer::AsBufferView(output_buffer));
+    CS::BindInputData(*pass, host_buffer->EmplaceStorageBuffer(input_data));
+    CS::BindOutputData(*pass, DeviceBuffer::AsBufferView(output_buffer));
 
-    pass->AddCommand(std::move(cmd));
+    pass->Compute(ISize(kCount, 1));
     pass->EncodeCommands();
     host_buffer->Reset();
     return cmd_buffer->SubmitCommands();
@@ -305,9 +284,6 @@
   static constexpr size_t kCount1 = 5;
   static constexpr size_t kCount2 = kCount1 * 2;
 
-  pass->SetGridSize(ISize(512, 1));
-  pass->SetThreadGroupSize(ISize(512, 1));
-
   CS1::Input<kCount1> input_1;
   input_1.count = kCount1;
   for (size_t i = 0; i < kCount1; i++) {
@@ -326,22 +302,20 @@
       context, "Output Buffer Stage 2");
 
   {
-    ComputeCommand cmd;
-    cmd.pipeline = compute_pipeline_1;
+    pass->SetPipeline(compute_pipeline_1);
 
-    CS1::BindInput(cmd, host_buffer->EmplaceStorageBuffer(input_1));
-    CS1::BindOutput(cmd, DeviceBuffer::AsBufferView(output_buffer_1));
+    CS1::BindInput(*pass, host_buffer->EmplaceStorageBuffer(input_1));
+    CS1::BindOutput(*pass, DeviceBuffer::AsBufferView(output_buffer_1));
 
-    ASSERT_TRUE(pass->AddCommand(std::move(cmd)));
+    ASSERT_TRUE(pass->Compute(ISize(512, 1)).ok());
   }
 
   {
-    ComputeCommand cmd;
-    cmd.pipeline = compute_pipeline_2;
+    pass->SetPipeline(compute_pipeline_2);
 
-    CS1::BindInput(cmd, DeviceBuffer::AsBufferView(output_buffer_1));
-    CS2::BindOutput(cmd, DeviceBuffer::AsBufferView(output_buffer_2));
-    ASSERT_TRUE(pass->AddCommand(std::move(cmd)));
+    CS1::BindInput(*pass, DeviceBuffer::AsBufferView(output_buffer_1));
+    CS2::BindOutput(*pass, DeviceBuffer::AsBufferView(output_buffer_2));
+    ASSERT_TRUE(pass->Compute(ISize(512, 1)).ok());
   }
 
   ASSERT_TRUE(pass->EncodeCommands());
@@ -393,10 +367,7 @@
 
   static constexpr size_t kCount = 5;
 
-  pass->SetGridSize(ISize(kCount, 1));
-
-  ComputeCommand cmd;
-  cmd.pipeline = compute_pipeline;
+  pass->SetPipeline(compute_pipeline);
 
   CS::Info info{.count = kCount};
   CS::Input0<kCount> input_0;
@@ -414,12 +385,12 @@
   auto output_buffer = CreateHostVisibleDeviceBuffer<CS::Output<kCount>>(
       context, "Output Buffer");
 
-  CS::BindInfo(cmd, host_buffer->EmplaceUniform(info));
-  CS::BindInput0(cmd, host_buffer->EmplaceStorageBuffer(input_0));
-  CS::BindInput1(cmd, host_buffer->EmplaceStorageBuffer(input_1));
-  CS::BindOutput(cmd, DeviceBuffer::AsBufferView(output_buffer));
+  CS::BindInfo(*pass, host_buffer->EmplaceUniform(info));
+  CS::BindInput0(*pass, host_buffer->EmplaceStorageBuffer(input_0));
+  CS::BindInput1(*pass, host_buffer->EmplaceStorageBuffer(input_1));
+  CS::BindOutput(*pass, DeviceBuffer::AsBufferView(output_buffer));
 
-  ASSERT_TRUE(pass->AddCommand(std::move(cmd)));
+  ASSERT_TRUE(pass->Compute(ISize(kCount, 1)).ok());
   ASSERT_TRUE(pass->EncodeCommands());
 
   fml::AutoResetWaitableEvent latch;
@@ -469,13 +440,7 @@
 
   static constexpr size_t kCount = 5;
 
-  // Intentionally making the grid size zero in one dimension. No GPU will
-  // tolerate this.
-  pass->SetGridSize(ISize(0, 1));
-  pass->SetThreadGroupSize(ISize(0, 1));
-
-  ComputeCommand cmd;
-  cmd.pipeline = compute_pipeline;
+  pass->SetPipeline(compute_pipeline);
 
   CS::Info info{.count = kCount};
   CS::Input0<kCount> input_0;
@@ -493,13 +458,14 @@
   auto output_buffer = CreateHostVisibleDeviceBuffer<CS::Output<kCount>>(
       context, "Output Buffer");
 
-  CS::BindInfo(cmd, host_buffer->EmplaceUniform(info));
-  CS::BindInput0(cmd, host_buffer->EmplaceStorageBuffer(input_0));
-  CS::BindInput1(cmd, host_buffer->EmplaceStorageBuffer(input_1));
-  CS::BindOutput(cmd, DeviceBuffer::AsBufferView(output_buffer));
+  CS::BindInfo(*pass, host_buffer->EmplaceUniform(info));
+  CS::BindInput0(*pass, host_buffer->EmplaceStorageBuffer(input_0));
+  CS::BindInput1(*pass, host_buffer->EmplaceStorageBuffer(input_1));
+  CS::BindOutput(*pass, DeviceBuffer::AsBufferView(output_buffer));
 
-  ASSERT_TRUE(pass->AddCommand(std::move(cmd)));
-  ASSERT_FALSE(pass->EncodeCommands());
+  // Intentionally making the grid size zero in one dimension. No GPU will
+  // tolerate this.
+  ASSERT_FALSE(pass->Compute(ISize(0, 1)).ok());
 }
 
 }  // namespace testing