[Impeller] Support blit passes on Vulkan. (#39438)

fixes: https://github.com/flutter/flutter/issues/112649

diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 9339938..5df8365 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1506,6 +1506,8 @@
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/vertex_descriptor_mtl.mm + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/allocator_vk.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/allocator_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
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/blit_pass_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc + ../../../flutter/LICENSE
@@ -1514,6 +1516,8 @@
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/command_buffer_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/command_pool_vk.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/command_pool_vk.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/commands_vk.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/commands_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/context_vk.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/context_vk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/vulkan/deletion_queue_vk.cc + ../../../flutter/LICENSE
@@ -3987,6 +3991,8 @@
 FILE: ../../../flutter/impeller/renderer/backend/metal/vertex_descriptor_mtl.mm
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/allocator_vk.cc
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/allocator_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
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/blit_pass_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc
@@ -3995,6 +4001,8 @@
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/command_buffer_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/command_pool_vk.cc
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/command_pool_vk.h
+FILE: ../../../flutter/impeller/renderer/backend/vulkan/commands_vk.cc
+FILE: ../../../flutter/impeller/renderer/backend/vulkan/commands_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/context_vk.cc
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/context_vk.h
 FILE: ../../../flutter/impeller/renderer/backend/vulkan/deletion_queue_vk.cc
diff --git a/impeller/renderer/backend/vulkan/BUILD.gn b/impeller/renderer/backend/vulkan/BUILD.gn
index 3ba6c17..dd1514a 100644
--- a/impeller/renderer/backend/vulkan/BUILD.gn
+++ b/impeller/renderer/backend/vulkan/BUILD.gn
@@ -8,6 +8,8 @@
   sources = [
     "allocator_vk.cc",
     "allocator_vk.h",
+    "blit_command_vk.cc",
+    "blit_command_vk.h",
     "blit_pass_vk.cc",
     "blit_pass_vk.h",
     "capabilities_vk.cc",
@@ -16,6 +18,8 @@
     "command_buffer_vk.h",
     "command_pool_vk.cc",
     "command_pool_vk.h",
+    "commands_vk.cc",
+    "commands_vk.h",
     "context_vk.cc",
     "context_vk.h",
     "deletion_queue_vk.cc",
diff --git a/impeller/renderer/backend/vulkan/blit_command_vk.cc b/impeller/renderer/backend/vulkan/blit_command_vk.cc
new file mode 100644
index 0000000..8a9dd4c
--- /dev/null
+++ b/impeller/renderer/backend/vulkan/blit_command_vk.cc
@@ -0,0 +1,182 @@
+// 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/blit_command_vk.h"
+
+#include "impeller/renderer/backend/vulkan/commands_vk.h"
+#include "impeller/renderer/backend/vulkan/texture_vk.h"
+
+namespace impeller {
+
+BlitEncodeVK::~BlitEncodeVK() = default;
+
+//------------------------------------------------------------------------------
+/// BlitCopyTextureToTextureCommandVK
+///
+
+BlitCopyTextureToTextureCommandVK::~BlitCopyTextureToTextureCommandVK() =
+    default;
+
+std::string BlitCopyTextureToTextureCommandVK::GetLabel() const {
+  return label;
+}
+
+[[nodiscard]] bool BlitCopyTextureToTextureCommandVK::Encode(
+    FencedCommandBufferVK* fenced_command_buffer) const {
+  // cast source and destination to TextureVK
+  const auto& source_tex_vk = TextureVK::Cast(*source);
+  const auto& dest_tex_vk = TextureVK::Cast(*destination);
+
+  // get the vulkan image and image view
+  const auto source_image = source_tex_vk.GetImage();
+  const auto dest_image = dest_tex_vk.GetImage();
+
+  // copy the source image to the destination image, from source_region to
+  // destination_origin.
+  vk::ImageCopy image_copy;
+  image_copy.setSrcSubresource(
+      vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1));
+  image_copy.setDstSubresource(
+      vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1));
+
+  image_copy.srcOffset =
+      vk::Offset3D(source_region.origin.x, source_region.origin.y, 0);
+  image_copy.dstOffset =
+      vk::Offset3D(destination_origin.x, destination_origin.y, 0);
+  image_copy.extent =
+      vk::Extent3D(source_region.size.width, source_region.size.height, 1);
+
+  // get single use command buffer
+  auto copy_cmd = fenced_command_buffer->GetSingleUseChild();
+
+  vk::CommandBufferBeginInfo begin_info;
+  begin_info.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
+  auto res = copy_cmd.begin(begin_info);
+
+  if (res != vk::Result::eSuccess) {
+    VALIDATION_LOG << "Failed to begin command buffer: " << vk::to_string(res);
+    return false;
+  }
+
+  // transition the source image to transfer source optimal
+  TransitionImageLayoutCommandVK transition_source_cmd =
+      TransitionImageLayoutCommandVK(source_image, vk::ImageLayout::eUndefined,
+                                     vk::ImageLayout::eTransferSrcOptimal);
+  bool success = transition_source_cmd.Submit(fenced_command_buffer);
+  if (!success) {
+    VALIDATION_LOG << "Failed to transition source image layout";
+    return false;
+  }
+
+  // transition the destination image to transfer destination optimal
+  TransitionImageLayoutCommandVK transition_dest_cmd =
+      TransitionImageLayoutCommandVK(dest_image, vk::ImageLayout::eUndefined,
+                                     vk::ImageLayout::eTransferDstOptimal);
+  success = transition_dest_cmd.Submit(fenced_command_buffer);
+  if (!success) {
+    VALIDATION_LOG << "Failed to transition destination image layout";
+    return false;
+  }
+
+  // issue the copy command
+  copy_cmd.copyImage(source_image, vk::ImageLayout::eTransferSrcOptimal,
+                     dest_image, vk::ImageLayout::eTransferDstOptimal,
+                     image_copy);
+  res = copy_cmd.end();
+  if (res != vk::Result::eSuccess) {
+    VALIDATION_LOG << "Failed to end command buffer: " << vk::to_string(res);
+    return false;
+  }
+
+  return true;
+}
+
+//------------------------------------------------------------------------------
+/// BlitCopyTextureToBufferCommandVK
+///
+
+BlitCopyTextureToBufferCommandVK::~BlitCopyTextureToBufferCommandVK() = default;
+
+std::string BlitCopyTextureToBufferCommandVK::GetLabel() const {
+  return label;
+}
+
+[[nodiscard]] bool BlitCopyTextureToBufferCommandVK::Encode(
+    FencedCommandBufferVK* fenced_command_buffer) const {
+  // cast source and destination to TextureVK
+  const auto& source_tex_vk = TextureVK::Cast(*source);
+  const auto& dest_buf_vk = DeviceBufferVK::Cast(*destination);
+
+  // get the vulkan image and image view
+  const auto source_image = source_tex_vk.GetImage();
+
+  // get buffer image handle
+  const auto dest_buffer = dest_buf_vk.GetVKBufferHandle();
+
+  // copy the source image to the destination buffer, from source_region to
+  // destination_origin.
+  vk::BufferImageCopy image_copy{};
+  image_copy.setBufferOffset(destination_offset);
+  image_copy.setBufferRowLength(0);
+  image_copy.setBufferImageHeight(0);
+  image_copy.setImageSubresource(
+      vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1));
+  image_copy.setImageOffset(
+      vk::Offset3D(source_region.origin.x, source_region.origin.y, 0));
+  image_copy.setImageExtent(
+      vk::Extent3D(source_region.size.width, source_region.size.height, 1));
+
+  // transition the source image to transfer source optimal
+  TransitionImageLayoutCommandVK transition_source_cmd =
+      TransitionImageLayoutCommandVK(source_image, vk::ImageLayout::eUndefined,
+                                     vk::ImageLayout::eTransferSrcOptimal);
+  bool success = transition_source_cmd.Submit(fenced_command_buffer);
+  if (!success) {
+    return false;
+  }
+
+  // get single use command buffer
+  auto copy_cmd = fenced_command_buffer->GetSingleUseChild();
+
+  vk::CommandBufferBeginInfo begin_info;
+  begin_info.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
+  auto res = copy_cmd.begin(begin_info);
+
+  if (res != vk::Result::eSuccess) {
+    VALIDATION_LOG << "Failed to begin command buffer: " << vk::to_string(res);
+    return false;
+  }
+
+  // issue the copy command
+  copy_cmd.copyImageToBuffer(source_image, vk::ImageLayout::eTransferSrcOptimal,
+                             dest_buffer, image_copy);
+  res = copy_cmd.end();
+  if (res != vk::Result::eSuccess) {
+    VALIDATION_LOG << "Failed to end command buffer: " << vk::to_string(res);
+  }
+
+  return true;
+}
+
+//------------------------------------------------------------------------------
+/// BlitGenerateMipmapCommandVK
+///
+
+BlitGenerateMipmapCommandVK::~BlitGenerateMipmapCommandVK() = default;
+
+std::string BlitGenerateMipmapCommandVK::GetLabel() const {
+  return label;
+}
+
+[[nodiscard]] bool BlitGenerateMipmapCommandVK::Encode(
+    FencedCommandBufferVK* fenced_command_buffer) const {
+  // TODO(https://github.com/flutter/flutter/issues/120134): Support generating
+  // mipmaps on Vulkan.
+  IMPELLER_UNIMPLEMENTED;
+  return true;
+}
+
+// END: BlitGenerateMipmapCommandVK
+
+}  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/blit_command_vk.h b/impeller/renderer/backend/vulkan/blit_command_vk.h
new file mode 100644
index 0000000..dda7f42
--- /dev/null
+++ b/impeller/renderer/backend/vulkan/blit_command_vk.h
@@ -0,0 +1,57 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+#include "impeller/base/backend_cast.h"
+#include "impeller/renderer/backend/vulkan/context_vk.h"
+#include "impeller/renderer/backend/vulkan/fenced_command_buffer_vk.h"
+#include "impeller/renderer/blit_command.h"
+#include "impeller/renderer/context.h"
+
+namespace impeller {
+
+/// Mixin for dispatching Vulkan commands.
+struct BlitEncodeVK : BackendCast<BlitEncodeVK, BlitCommand> {
+  virtual ~BlitEncodeVK();
+
+  virtual std::string GetLabel() const = 0;
+
+  [[nodiscard]] virtual bool Encode(
+      FencedCommandBufferVK* fenced_command_buffer) const = 0;
+};
+
+struct BlitCopyTextureToTextureCommandVK
+    : public BlitCopyTextureToTextureCommand,
+      public BlitEncodeVK {
+  ~BlitCopyTextureToTextureCommandVK() override;
+
+  std::string GetLabel() const override;
+
+  [[nodiscard]] bool Encode(
+      FencedCommandBufferVK* fenced_command_buffer) const override;
+};
+
+struct BlitCopyTextureToBufferCommandVK : public BlitCopyTextureToBufferCommand,
+                                          public BlitEncodeVK {
+  ~BlitCopyTextureToBufferCommandVK() override;
+
+  std::string GetLabel() const override;
+
+  [[nodiscard]] bool Encode(
+      FencedCommandBufferVK* fenced_command_buffer) const override;
+};
+
+struct BlitGenerateMipmapCommandVK : public BlitGenerateMipmapCommand,
+                                     public BlitEncodeVK {
+  ~BlitGenerateMipmapCommandVK() override;
+
+  std::string GetLabel() const override;
+
+  [[nodiscard]] bool Encode(
+      FencedCommandBufferVK* fenced_command_buffer) const override;
+};
+
+}  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/blit_pass_vk.cc b/impeller/renderer/backend/vulkan/blit_pass_vk.cc
index 03f0510..bdbd591 100644
--- a/impeller/renderer/backend/vulkan/blit_pass_vk.cc
+++ b/impeller/renderer/backend/vulkan/blit_pass_vk.cc
@@ -4,8 +4,92 @@
 
 #include "impeller/renderer/backend/vulkan/blit_pass_vk.h"
 
+#include "flutter/fml/logging.h"
+#include "flutter/fml/trace_event.h"
+
 namespace impeller {
 
-//
+BlitPassVK::BlitPassVK(std::shared_ptr<FencedCommandBufferVK> command_buffer)
+    : command_buffer_(std::move(command_buffer)) {}
+
+BlitPassVK::~BlitPassVK() = default;
+
+void BlitPassVK::OnSetLabel(std::string label) {
+  if (label.empty()) {
+    return;
+  }
+  label_ = std::move(label);
+}
+
+// |BlitPass|
+bool BlitPassVK::IsValid() const {
+  return command_buffer_ != nullptr;
+}
+
+// |BlitPass|
+bool BlitPassVK::EncodeCommands(
+    const std::shared_ptr<Allocator>& transients_allocator) const {
+  TRACE_EVENT0("impeller", "BlitPassVK::EncodeCommands");
+
+  if (!IsValid()) {
+    return false;
+  }
+
+  for (auto& command : commands_) {
+    bool encode_res = command->Encode(command_buffer_.get());
+    if (!encode_res) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+// |BlitPass|
+bool BlitPassVK::OnCopyTextureToTextureCommand(
+    std::shared_ptr<Texture> source,
+    std::shared_ptr<Texture> destination,
+    IRect source_region,
+    IPoint destination_origin,
+    std::string label) {
+  auto command = std::make_unique<BlitCopyTextureToTextureCommandVK>();
+  command->source = std::move(source);
+  command->destination = std::move(destination);
+  command->source_region = source_region;
+  command->destination_origin = destination_origin;
+  command->label = std::move(label);
+
+  commands_.push_back(std::move(command));
+  return true;
+}
+
+// |BlitPass|
+bool BlitPassVK::OnCopyTextureToBufferCommand(
+    std::shared_ptr<Texture> source,
+    std::shared_ptr<DeviceBuffer> destination,
+    IRect source_region,
+    size_t destination_offset,
+    std::string label) {
+  auto command = std::make_unique<BlitCopyTextureToBufferCommandVK>();
+  command->source = std::move(source);
+  command->destination = std::move(destination);
+  command->source_region = source_region;
+  command->destination_offset = destination_offset;
+  command->label = std::move(label);
+
+  commands_.push_back(std::move(command));
+  return true;
+}
+
+// |BlitPass|
+bool BlitPassVK::OnGenerateMipmapCommand(std::shared_ptr<Texture> texture,
+                                         std::string label) {
+  auto command = std::make_unique<BlitGenerateMipmapCommandVK>();
+  command->texture = std::move(texture);
+  command->label = std::move(label);
+
+  commands_.push_back(std::move(command));
+  return true;
+}
 
 }  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/blit_pass_vk.h b/impeller/renderer/backend/vulkan/blit_pass_vk.h
index a76e5a7..aa8a391 100644
--- a/impeller/renderer/backend/vulkan/blit_pass_vk.h
+++ b/impeller/renderer/backend/vulkan/blit_pass_vk.h
@@ -5,19 +5,25 @@
 #pragma once
 
 #include "flutter/fml/macros.h"
+#include "impeller/renderer/backend/vulkan/blit_command_vk.h"
+#include "impeller/renderer/backend/vulkan/fenced_command_buffer_vk.h"
 #include "impeller/renderer/blit_pass.h"
 
 namespace impeller {
 
 class BlitPassVK final : public BlitPass {
  public:
+  explicit BlitPassVK(std::shared_ptr<FencedCommandBufferVK> command_buffer);
+
   // |BlitPass|
   ~BlitPassVK() override;
 
  private:
   friend class CommandBufferVK;
 
-  BlitPassVK();
+  std::shared_ptr<FencedCommandBufferVK> command_buffer_;
+  std::vector<std::unique_ptr<BlitEncodeVK>> commands_;
+  std::string label_;
 
   // |BlitPass|
   bool IsValid() const override;
diff --git a/impeller/renderer/backend/vulkan/command_buffer_vk.cc b/impeller/renderer/backend/vulkan/command_buffer_vk.cc
index 98f1e86..01fc6f3 100644
--- a/impeller/renderer/backend/vulkan/command_buffer_vk.cc
+++ b/impeller/renderer/backend/vulkan/command_buffer_vk.cc
@@ -9,6 +9,7 @@
 
 #include "flutter/fml/logging.h"
 #include "impeller/base/validation.h"
+#include "impeller/renderer/backend/vulkan/blit_pass_vk.h"
 #include "impeller/renderer/backend/vulkan/context_vk.h"
 #include "impeller/renderer/backend/vulkan/fenced_command_buffer_vk.h"
 #include "impeller/renderer/backend/vulkan/formats_vk.h"
@@ -120,7 +121,16 @@
 
 std::shared_ptr<BlitPass> CommandBufferVK::OnCreateBlitPass() const {
   // TODO(kaushikiska): https://github.com/flutter/flutter/issues/112649
-  return nullptr;
+  if (!IsValid()) {
+    return nullptr;
+  }
+
+  auto pass = std::make_shared<BlitPassVK>(fenced_command_buffer_);
+  if (!pass->IsValid()) {
+    return nullptr;
+  }
+
+  return pass;
 }
 
 std::shared_ptr<ComputePass> CommandBufferVK::OnCreateComputePass() const {
diff --git a/impeller/renderer/backend/vulkan/commands_vk.cc b/impeller/renderer/backend/vulkan/commands_vk.cc
new file mode 100644
index 0000000..6eab778
--- /dev/null
+++ b/impeller/renderer/backend/vulkan/commands_vk.cc
@@ -0,0 +1,67 @@
+// 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/commands_vk.h"
+
+namespace impeller {
+
+TransitionImageLayoutCommandVK::TransitionImageLayoutCommandVK(
+    vk::Image image,
+    vk::ImageLayout old_layout,
+    vk::ImageLayout new_layout)
+    : image_(image), old_layout_(old_layout), new_layout_(new_layout) {}
+
+TransitionImageLayoutCommandVK::~TransitionImageLayoutCommandVK() = default;
+
+bool TransitionImageLayoutCommandVK::Submit(
+    FencedCommandBufferVK* command_buffer) {
+  if (!command_buffer) {
+    return false;
+  }
+
+  vk::ImageMemoryBarrier barrier =
+      vk::ImageMemoryBarrier()
+          .setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite |
+                            vk::AccessFlagBits::eTransferWrite)
+          .setDstAccessMask(vk::AccessFlagBits::eColorAttachmentRead |
+                            vk::AccessFlagBits::eShaderRead)
+          .setOldLayout(old_layout_)
+          .setNewLayout(new_layout_)
+          .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
+          .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
+          .setImage(image_)
+          .setSubresourceRange(
+              vk::ImageSubresourceRange()
+                  .setAspectMask(vk::ImageAspectFlagBits::eColor)
+                  .setBaseMipLevel(0)
+                  .setLevelCount(1)
+                  .setBaseArrayLayer(0)
+                  .setLayerCount(1));
+
+  vk::PipelineStageFlags src_stage = vk::PipelineStageFlagBits::eAllGraphics;
+  vk::PipelineStageFlags dst_stage = vk::PipelineStageFlagBits::eAllGraphics;
+
+  auto transition_cmd = command_buffer->GetSingleUseChild();
+
+  vk::CommandBufferBeginInfo begin_info;
+  begin_info.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
+  auto res = transition_cmd.begin(begin_info);
+
+  if (res != vk::Result::eSuccess) {
+    FML_LOG(ERROR) << "Failed to begin command buffer: " << vk::to_string(res);
+    return false;
+  }
+
+  transition_cmd.pipelineBarrier(src_stage, dst_stage, {}, nullptr, nullptr,
+                                 barrier);
+  res = transition_cmd.end();
+  if (res != vk::Result::eSuccess) {
+    FML_LOG(ERROR) << "Failed to end command buffer: " << vk::to_string(res);
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/commands_vk.h b/impeller/renderer/backend/vulkan/commands_vk.h
new file mode 100644
index 0000000..31c1c12
--- /dev/null
+++ b/impeller/renderer/backend/vulkan/commands_vk.h
@@ -0,0 +1,31 @@
+// 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.
+
+#pragma once
+
+#include "impeller/renderer/backend/vulkan/context_vk.h"
+#include "impeller/renderer/backend/vulkan/fenced_command_buffer_vk.h"
+
+// Set of common utility commands for Vulkan.
+namespace impeller {
+
+class TransitionImageLayoutCommandVK {
+ public:
+  TransitionImageLayoutCommandVK(vk::Image image,
+                                 vk::ImageLayout old_layout,
+                                 vk::ImageLayout new_layout);
+
+  ~TransitionImageLayoutCommandVK();
+
+  bool Submit(FencedCommandBufferVK* command_buffer);
+
+ private:
+  vk::Image image_;
+  vk::ImageLayout old_layout_;
+  vk::ImageLayout new_layout_;
+
+  FML_DISALLOW_COPY_AND_ASSIGN(TransitionImageLayoutCommandVK);
+};
+
+}  // namespace impeller
diff --git a/impeller/renderer/backend/vulkan/render_pass_vk.cc b/impeller/renderer/backend/vulkan/render_pass_vk.cc
index 5c3e398..720ecf9 100644
--- a/impeller/renderer/backend/vulkan/render_pass_vk.cc
+++ b/impeller/renderer/backend/vulkan/render_pass_vk.cc
@@ -9,6 +9,7 @@
 
 #include "fml/logging.h"
 #include "impeller/base/validation.h"
+#include "impeller/renderer/backend/vulkan/commands_vk.h"
 #include "impeller/renderer/backend/vulkan/context_vk.h"
 #include "impeller/renderer/backend/vulkan/device_buffer_vk.h"
 #include "impeller/renderer/backend/vulkan/formats_vk.h"
@@ -394,46 +395,9 @@
 bool RenderPassVK::TransitionImageLayout(vk::Image image,
                                          vk::ImageLayout layout_old,
                                          vk::ImageLayout layout_new) const {
-  auto transition_cmd = command_buffer_->GetSingleUseChild();
-
-  vk::CommandBufferBeginInfo begin_info;
-  begin_info.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
-  auto res = transition_cmd.begin(begin_info);
-
-  if (res != vk::Result::eSuccess) {
-    VALIDATION_LOG << "Failed to begin command buffer: " << vk::to_string(res);
-    return false;
-  }
-
-  vk::ImageMemoryBarrier barrier =
-      vk::ImageMemoryBarrier()
-          .setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite |
-                            vk::AccessFlagBits::eTransferWrite)
-          .setDstAccessMask(vk::AccessFlagBits::eColorAttachmentRead |
-                            vk::AccessFlagBits::eShaderRead)
-          .setOldLayout(layout_old)
-          .setNewLayout(layout_new)
-          .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
-          .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
-          .setImage(image)
-          .setSubresourceRange(
-              vk::ImageSubresourceRange()
-                  .setAspectMask(vk::ImageAspectFlagBits::eColor)
-                  .setBaseMipLevel(0)
-                  .setLevelCount(1)
-                  .setBaseArrayLayer(0)
-                  .setLayerCount(1));
-  transition_cmd.pipelineBarrier(vk::PipelineStageFlagBits::eAllGraphics,
-                                 vk::PipelineStageFlagBits::eAllGraphics, {},
-                                 nullptr, nullptr, barrier);
-
-  res = transition_cmd.end();
-  if (res != vk::Result::eSuccess) {
-    VALIDATION_LOG << "Failed to end command buffer: " << vk::to_string(res);
-    return false;
-  }
-
-  return true;
+  auto transition_cmd =
+      TransitionImageLayoutCommandVK(image, layout_old, layout_new);
+  return transition_cmd.Submit(command_buffer_.get());
 }
 
 bool RenderPassVK::CopyBufferToImage(const TextureVK& texture_vk) const {