Read/Update ColorBuffers from VK when using guest ANGLE

... to avoid the need for any GL operations.

Bug: b/229145718
Test: `launch_cvd --gpu_mode=gfxstream`
Test: `launch_cvd --gpu_mode=gfxstream` with WIP ANGLE
Change-Id: I8fec6a702aac9b0047318887fc7dfcf3f20c2e3c
diff --git a/stream-servers/CMakeLists.txt b/stream-servers/CMakeLists.txt
index f7727aa..ea0fb62 100644
--- a/stream-servers/CMakeLists.txt
+++ b/stream-servers/CMakeLists.txt
@@ -229,6 +229,7 @@
         tests/DisplayVk_unittest.cpp
         tests/VirtioGpuTimelines_unittest.cpp
         vulkan/vk_util_unittest.cpp
+        vulkan/VkFormatUtils_unittest.cpp
         vulkan/VkQsriTimeline_unittest.cpp
         vulkan/VkDecoderGlobalState_unittest.cpp
     )
diff --git a/stream-servers/FrameBuffer.cpp b/stream-servers/FrameBuffer.cpp
index 8d4ed4b..09007e8 100644
--- a/stream-servers/FrameBuffer.cpp
+++ b/stream-servers/FrameBuffer.cpp
@@ -2205,6 +2205,11 @@
                                   GLenum format,
                                   GLenum type,
                                   void* pixels) {
+    if (m_guestUsesAngle) {
+        goldfish_vk::readColorBufferToBytes(p_colorbuffer, x, y, width, height, pixels);
+        return;
+    }
+
     AutoLock mutex(m_lock);
 
     ColorBufferPtr colorBuffer = findColorBuffer(p_colorbuffer);
@@ -2223,6 +2228,11 @@
                                      int height,
                                      void* pixels,
                                      uint32_t pixels_size) {
+    if (m_guestUsesAngle) {
+        goldfish_vk::readColorBufferToBytes(p_colorbuffer, x, y, width, height, pixels);
+        return;
+    }
+
     AutoLock mutex(m_lock);
 
     ColorBufferPtr colorBuffer = findColorBuffer(p_colorbuffer);
@@ -2334,6 +2344,10 @@
         return false;
     }
 
+    if (m_guestUsesAngle) {
+        return goldfish_vk::updateColorBufferFromBytes(p_colorbuffer, x, y, width, height, pixels);
+    }
+
     AutoLock mutex(m_lock);
 
     ColorBufferPtr colorBuffer = findColorBuffer(p_colorbuffer);
@@ -2735,7 +2749,7 @@
 
 bool FrameBuffer::post(HandleType p_colorbuffer, bool needLockAndBind) {
     if (m_guestUsesAngle) {
-        goldfish_vk::updateColorBufferFromVkImage(p_colorbuffer);
+        goldfish_vk::updateColorBufferFromGl(p_colorbuffer);
     }
 
     bool res = postImpl(p_colorbuffer, needLockAndBind);
diff --git a/stream-servers/RenderControl.cpp b/stream-servers/RenderControl.cpp
index cec056f..abe9bdf 100644
--- a/stream-servers/RenderControl.cpp
+++ b/stream-servers/RenderControl.cpp
@@ -895,8 +895,7 @@
     }
 
     // Update from Vulkan if necessary
-    goldfish_vk::updateColorBufferFromVkImage(
-        fb->getWindowSurfaceColorBufferHandle(windowSurface));
+    goldfish_vk::readColorBufferToGl(fb->getWindowSurfaceColorBufferHandle(windowSurface));
 
     if (!fb->flushWindowSurfaceColorBuffer(windowSurface)) {
         GRSYNC_DPRINT("unlock gralloc cb lock }");
@@ -904,8 +903,7 @@
     }
 
     // Update to Vulkan if necessary
-    goldfish_vk::updateVkImageFromColorBuffer(
-        fb->getWindowSurfaceColorBufferHandle(windowSurface));
+    goldfish_vk::updateColorBufferFromGl(fb->getWindowSurfaceColorBufferHandle(windowSurface));
 
     GRSYNC_DPRINT("unlock gralloc cb lock }");
 
@@ -971,7 +969,7 @@
     }
 
     // Update from Vulkan if necessary
-    goldfish_vk::updateColorBufferFromVkImage(colorBuffer);
+    goldfish_vk::readColorBufferToGl(colorBuffer);
 
     fb->post(colorBuffer);
 }
@@ -989,7 +987,7 @@
     }
 
     // Update from Vulkan if necessary
-    goldfish_vk::updateColorBufferFromVkImage(colorBuffer);
+    goldfish_vk::readColorBufferToGl(colorBuffer);
 
     fb->bindColorBufferToTexture(colorBuffer);
 }
@@ -1002,7 +1000,7 @@
     }
 
     // Update from Vulkan if necessary
-    goldfish_vk::updateColorBufferFromVkImage(colorBuffer);
+    goldfish_vk::readColorBufferToGl(colorBuffer);
 
     fb->bindColorBufferToRenderbuffer(colorBuffer);
 }
@@ -1028,7 +1026,7 @@
     }
 
     // Update from Vulkan if necessary
-    goldfish_vk::updateColorBufferFromVkImage(colorBuffer);
+    goldfish_vk::readColorBufferToGl(colorBuffer);
 
     fb->readColorBuffer(colorBuffer, x, y, width, height, format, type, pixels);
 }
@@ -1048,7 +1046,7 @@
 
     // Since this is a modify operation, also read the current contents
     // of the VkImage, if any.
-    goldfish_vk::updateColorBufferFromVkImage(colorBuffer);
+    goldfish_vk::readColorBufferToGl(colorBuffer);
 
     fb->updateColorBuffer(colorBuffer, x, y, width, height, format, type, pixels);
 
@@ -1056,7 +1054,7 @@
     sGrallocSync()->unlockColorBufferPrepare();
 
     // Update to Vulkan if necessary
-    goldfish_vk::updateVkImageFromColorBuffer(colorBuffer);
+    goldfish_vk::updateColorBufferFromGl(colorBuffer);
 
     return 0;
 }
@@ -1077,7 +1075,7 @@
 
     // Since this is a modify operation, also read the current contents
     // of the VkImage, if any.
-    goldfish_vk::updateColorBufferFromVkImage(colorBuffer);
+    goldfish_vk::readColorBufferToGl(colorBuffer);
 
     fb->updateColorBuffer(colorBuffer, x, y, width, height,
                           format, type, pixels);
@@ -1086,7 +1084,7 @@
     sGrallocSync()->unlockColorBufferPrepare();
 
     // Update to Vulkan if necessary
-    goldfish_vk::updateVkImageFromColorBuffer(colorBuffer);
+    goldfish_vk::updateColorBufferFromGl(colorBuffer);
 
     return 0;
 }
@@ -1553,7 +1551,7 @@
     }
 
     // Update from Vulkan if necessary
-    goldfish_vk::updateColorBufferFromVkImage(colorBuffer);
+    goldfish_vk::readColorBufferToGl(colorBuffer);
 
     fb->readColorBuffer(colorBuffer, x, y, width, height, format, type, pixels);
     return 0;
diff --git a/stream-servers/vulkan/Android.bp b/stream-servers/vulkan/Android.bp
index 41cf32e..7303067 100644
--- a/stream-servers/vulkan/Android.bp
+++ b/stream-servers/vulkan/Android.bp
@@ -14,6 +14,7 @@
     static_libs: [
         "gfxstream_base",
         "gfxstream_compressedTextures",
+        "gfxstream_host_common",
         "gfxstream_apigen_codec_common",
         "gfxstream_vulkan_cereal_host",
     ],
@@ -39,6 +40,7 @@
         "VkDecoder.cpp",
         "VkDecoderGlobalState.cpp",
         "VkDecoderSnapshot.cpp",
+        "VkFormatUtils.cpp",
         "VkReconstruction.cpp",
         "VulkanDispatch.cpp",
         "VulkanHandleMapping.cpp",
@@ -52,3 +54,25 @@
         "VkDecoderGlobalState.cpp", // took more than 400 seconds
     ],
 }
+
+// Run with `atest --host gfxstream_vkformatutils_tests`
+cc_test_host {
+    name: "gfxstream_vkformatutils_tests",
+    defaults: [ "gfxstream_defaults" ],
+    srcs: [
+        "VkFormatUtils_unittest.cpp",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    static_libs: [
+        "gfxstream_host_common",
+        "gfxstream_vulkan_server",
+        "libgtest",
+        "libgmock",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+}
\ No newline at end of file
diff --git a/stream-servers/vulkan/CMakeLists.txt b/stream-servers/vulkan/CMakeLists.txt
index 8d209f0..48f2247 100644
--- a/stream-servers/vulkan/CMakeLists.txt
+++ b/stream-servers/vulkan/CMakeLists.txt
@@ -10,6 +10,7 @@
             VkDecoder.cpp
             VkDecoderGlobalState.cpp
             VkDecoderSnapshot.cpp
+            VkFormatUtils.cpp
             VkReconstruction.cpp
             VulkanDispatch.cpp
             VulkanHandleMapping.cpp
diff --git a/stream-servers/vulkan/VkCommonOperations.cpp b/stream-servers/vulkan/VkCommonOperations.cpp
index 2af304b..cfc1c42 100644
--- a/stream-servers/vulkan/VkCommonOperations.cpp
+++ b/stream-servers/vulkan/VkCommonOperations.cpp
@@ -26,6 +26,7 @@
 #include <unordered_set>
 
 #include "FrameBuffer.h"
+#include "VkFormatUtils.h"
 #include "VulkanDispatch.h"
 #include "base/Lock.h"
 #include "base/Lookup.h"
@@ -1849,332 +1850,407 @@
     return res;
 }
 
-bool updateColorBufferFromVkImage(uint32_t colorBufferHandle) {
-    if (!sVkEmulation || !sVkEmulation->live) return false;
-
-    auto vk = sVkEmulation->dvk;
-
-    AutoLock lock(sVkEmulationLock);
-
-    auto infoPtr = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
-
-    if (!infoPtr) {
-        // Color buffer not found; this is usually OK.
+bool colorBufferNeedsTransferBetweenGlAndVk(const VkEmulation::ColorBufferInfo& colorBufferInfo) {
+    // GL is not used.
+    if (colorBufferInfo.vulkanMode == VkEmulation::VulkanMode::VulkanOnly) {
         return false;
     }
 
-    if (!infoPtr->image) {
-        fprintf(stderr, "%s: error: ColorBuffer 0x%x has no VkImage\n", __func__,
-                colorBufferHandle);
-        return false;
-    }
-
-    if (infoPtr->glExported || (infoPtr->vulkanMode == VkEmulation::VulkanMode::VulkanOnly) ||
-        infoPtr->frameworkFormat != FrameworkFormat::FRAMEWORK_FORMAT_GL_COMPATIBLE) {
-        // No sync needed if exported to GL or in Vulkan-only mode
+    // YUV formats require extra conversions.
+    if (colorBufferInfo.frameworkFormat != FrameworkFormat::FRAMEWORK_FORMAT_GL_COMPATIBLE) {
         return true;
     }
 
-    // Record our synchronization commands.
-    VkCommandBufferBeginInfo beginInfo = {
-        VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
-        0,
-        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
-        nullptr /* no inheritance info */,
-    };
-
-    vk->vkBeginCommandBuffer(sVkEmulation->commandBuffer, &beginInfo);
-
-    // From the spec: If an application does not need the contents of a resource
-    // to remain valid when transferring from one queue family to another, then
-    // the ownership transfer should be skipped.
-
-    // We definitely need to transition the image to
-    // VK_TRANSFER_SRC_OPTIMAL and back.
-
-    VkImageMemoryBarrier presentToTransferSrc = {
-        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
-        0,
-        0,
-        VK_ACCESS_HOST_READ_BIT,
-        infoPtr->currentLayout,
-        VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-        VK_QUEUE_FAMILY_IGNORED,
-        VK_QUEUE_FAMILY_IGNORED,
-        infoPtr->image,
-        {
-            VK_IMAGE_ASPECT_COLOR_BIT,
-            0,
-            1,
-            0,
-            1,
-        },
-    };
-
-    vk->vkCmdPipelineBarrier(sVkEmulation->commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
-                             VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1,
-                             &presentToTransferSrc);
-
-    infoPtr->currentLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
-
-    // Copy to staging buffer
-    uint32_t bpp = 4; /* format always rgba8...not */
-    switch (infoPtr->imageCreateInfoShallow.format) {
-        case VK_FORMAT_R5G6B5_UNORM_PACK16:
-            bpp = 2;
-            break;
-        case VK_FORMAT_R8G8B8_UNORM:
-            bpp = 3;
-            break;
-        default:
-        case VK_FORMAT_R8G8B8A8_UNORM:
-            bpp = 4;
-            break;
+    // GL and VK are sharing the same underlying memory.
+    if (colorBufferInfo.glExported) {
+        return false;
     }
-    VkBufferImageCopy region = {
-        0 /* buffer offset */,
-        infoPtr->imageCreateInfoShallow.extent.width,
-        infoPtr->imageCreateInfoShallow.extent.height,
-        {
-            VK_IMAGE_ASPECT_COLOR_BIT,
-            0,
-            0,
-            1,
-        },
-        {0, 0, 0},
-        infoPtr->imageCreateInfoShallow.extent,
-    };
-
-    vk->vkCmdCopyImageToBuffer(sVkEmulation->commandBuffer, infoPtr->image,
-                               VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sVkEmulation->staging.buffer,
-                               1, &region);
-
-    vk->vkEndCommandBuffer(sVkEmulation->commandBuffer);
-
-    VkSubmitInfo submitInfo = {
-        VK_STRUCTURE_TYPE_SUBMIT_INFO, 0, 0,       nullptr, nullptr, 1,
-        &sVkEmulation->commandBuffer,  0, nullptr,
-    };
-
-    {
-        android::base::AutoLock lock(*sVkEmulation->queueLock);
-        vk->vkQueueSubmit(sVkEmulation->queue, 1, &submitInfo, sVkEmulation->commandBufferFence);
-    }
-
-    static constexpr uint64_t ANB_MAX_WAIT_NS = 5ULL * 1000ULL * 1000ULL * 1000ULL;
-
-    vk->vkWaitForFences(sVkEmulation->device, 1, &sVkEmulation->commandBufferFence, VK_TRUE,
-                        ANB_MAX_WAIT_NS);
-    vk->vkResetFences(sVkEmulation->device, 1, &sVkEmulation->commandBufferFence);
-
-    VkMappedMemoryRange toInvalidate = {
-        VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
-        0,
-        sVkEmulation->staging.memory.memory,
-        0,
-        VK_WHOLE_SIZE,
-    };
-
-    vk->vkInvalidateMappedMemoryRanges(sVkEmulation->device, 1, &toInvalidate);
-
-    const std::size_t copiedSize = infoPtr->imageCreateInfoShallow.extent.width *
-                                   infoPtr->imageCreateInfoShallow.extent.height * bpp;
-
-    FrameBuffer::getFB()->replaceColorBufferContents(
-        colorBufferHandle, sVkEmulation->staging.memory.mappedPtr, copiedSize);
 
     return true;
 }
 
-bool updateVkImageFromColorBuffer(uint32_t colorBufferHandle) {
-    if (!sVkEmulation || !sVkEmulation->live) return false;
+bool readColorBufferToGl(uint32_t colorBufferHandle) {
+    if (!sVkEmulation || !sVkEmulation->live) {
+        VK_COMMON_ERROR("VkEmulation not available.");
+        return false;
+    }
 
     auto vk = sVkEmulation->dvk;
 
     AutoLock lock(sVkEmulationLock);
 
-    auto infoPtr = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
-
-    if (!infoPtr) {
-        // Color buffer not found; this is usually OK.
+    auto colorBufferInfo = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
+    if (!colorBufferInfo) {
+        VK_COMMON_ERROR("Failed to read from ColorBuffer:%d, not found.", colorBufferHandle);
         return false;
     }
 
-    if (infoPtr->frameworkFormat == FrameworkFormat::FRAMEWORK_FORMAT_GL_COMPATIBLE &&
-        (infoPtr->glExported || infoPtr->vulkanMode == VkEmulation::VulkanMode::VulkanOnly)) {
-        // No sync needed if exported to GL or in Vulkan-only mode
+    if (!colorBufferNeedsTransferBetweenGlAndVk(*colorBufferInfo)) {
         return true;
     }
 
-    size_t cbNumBytes = 0;
-    bool readRes =
-        FrameBuffer::getFB()->readColorBufferContents(colorBufferHandle, &cbNumBytes, nullptr);
-    if (!readRes) {
-        fprintf(stderr, "%s: Failed to read color buffer 0x%x\n", __func__, colorBufferHandle);
+    VkDeviceSize bytesNeeded = 0;
+    bool result = getFormatTransferInfo(colorBufferInfo->imageCreateInfoShallow.format,
+                                        colorBufferInfo->imageCreateInfoShallow.extent.width,
+                                        colorBufferInfo->imageCreateInfoShallow.extent.height,
+                                        &bytesNeeded, nullptr);
+    if (!result) {
+        VK_COMMON_ERROR("Failed to read from ColorBuffer:%d, failed to get read size.",
+                        colorBufferHandle);
         return false;
     }
 
-    if (cbNumBytes > sVkEmulation->staging.memory.size) {
-        fprintf(stderr,
-                "%s: Not enough space to read to staging buffer. "
-                "Wanted: 0x%llx Have: 0x%llx\n",
-                __func__, (unsigned long long)cbNumBytes,
-                (unsigned long long)(sVkEmulation->staging.memory.size));
+    std::vector<uint8_t> bytes(bytesNeeded);
+
+    result = readColorBufferToBytes(
+        colorBufferHandle, 0, 0, colorBufferInfo->imageCreateInfoShallow.extent.width,
+        colorBufferInfo->imageCreateInfoShallow.extent.height, bytes.data());
+    if (!result) {
+        VK_COMMON_ERROR("Failed to read from ColorBuffer:%d, failed to get read size.",
+                        colorBufferHandle);
         return false;
     }
 
-    readRes = FrameBuffer::getFB()->readColorBufferContents(colorBufferHandle, &cbNumBytes,
-                                                            sVkEmulation->staging.memory.mappedPtr);
+    return FrameBuffer::getFB()->replaceColorBufferContents(colorBufferHandle, bytes.data(),
+                                                            bytes.size());
+}
 
-    if (!readRes) {
-        fprintf(stderr, "%s: Failed to read color buffer 0x%x (at glReadPixels)\n", __func__,
-                colorBufferHandle);
+bool readColorBufferToBytes(uint32_t colorBufferHandle, uint32_t x, uint32_t y, uint32_t w,
+                            uint32_t h, void* outPixels) {
+    if (!sVkEmulation || !sVkEmulation->live) {
+        VK_COMMON_ERROR("VkEmulation not available.");
         return false;
     }
 
+    auto vk = sVkEmulation->dvk;
+
+    AutoLock lock(sVkEmulationLock);
+    return readColorBufferToBytesLocked(colorBufferHandle, x, y, w, h, outPixels);
+}
+
+bool readColorBufferToBytesLocked(uint32_t colorBufferHandle, uint32_t x, uint32_t y, uint32_t w,
+                                  uint32_t h, void* outPixels) {
+    if (!sVkEmulation || !sVkEmulation->live) {
+        VK_COMMON_ERROR("VkEmulation not available.");
+        return false;
+    }
+
+    auto vk = sVkEmulation->dvk;
+
+    auto colorBufferInfo = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
+    if (!colorBufferInfo) {
+        VK_COMMON_ERROR("Failed to read from ColorBuffer:%d, not found.", colorBufferHandle);
+        return false;
+    }
+
+    if (!colorBufferInfo->image) {
+        VK_COMMON_ERROR("Failed to read from ColorBuffer:%d, no VkImage.", colorBufferHandle);
+        return false;
+    }
+
+    if (x != 0 || y != 0 || w != colorBufferInfo->imageCreateInfoShallow.extent.width ||
+        h != colorBufferInfo->imageCreateInfoShallow.extent.height) {
+        VK_COMMON_ERROR("Failed to read from ColorBuffer:%d, unhandled subrect.",
+                        colorBufferHandle);
+        return false;
+    }
+
+    std::size_t bufferCopySize = 0;
+    std::vector<VkBufferImageCopy> bufferImageCopies;
+    if (!getFormatTransferInfo(colorBufferInfo->imageCreateInfoShallow.format,
+                               colorBufferInfo->imageCreateInfoShallow.extent.width,
+                               colorBufferInfo->imageCreateInfoShallow.extent.height,
+                               &bufferCopySize, &bufferImageCopies)) {
+        VK_COMMON_ERROR("Failed to read ColorBuffer:%d, unable to get transfer info.",
+                        colorBufferHandle);
+        return false;
+    }
+
+    // Avoid transitioning from VK_IMAGE_LAYOUT_UNDEFINED. Unfortunetly, Android does not
+    // yet have a mechanism for sharing the expected VkImageLayout. However, the Vulkan
+    // spec's image layout transition sections says "If the old layout is
+    // VK_IMAGE_LAYOUT_UNDEFINED, the contents of that range may be discarded." Some
+    // Vulkan drivers have been observed to actually perform the discard which leads to
+    // ColorBuffer-s being unintentionally cleared. See go/ahb-vkimagelayout for a more
+    // thorough write up.
+    if (colorBufferInfo->currentLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
+        colorBufferInfo->currentLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
+    }
+
     // Record our synchronization commands.
-    VkCommandBufferBeginInfo beginInfo = {
-        VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
-        0,
-        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
-        nullptr /* no inheritance info */,
+    const VkCommandBufferBeginInfo beginInfo = {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+        .pNext = nullptr,
+        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
     };
 
-    vk->vkBeginCommandBuffer(sVkEmulation->commandBuffer, &beginInfo);
+    VkCommandBuffer commandBuffer = sVkEmulation->commandBuffer;
 
-    // From the spec: If an application does not need the contents of a resource
-    // to remain valid when transferring from one queue family to another, then
-    // the ownership transfer should be skipped.
+    VK_CHECK(vk->vkBeginCommandBuffer(commandBuffer, &beginInfo));
 
-    // We definitely need to transition the image to
-    // VK_TRANSFER_SRC_OPTIMAL and back.
-
-    VkImageMemoryBarrier presentToTransferSrc = {
-        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
-        0,
-        0,
-        VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
-        infoPtr->currentLayout,
-        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-        VK_QUEUE_FAMILY_IGNORED,
-        VK_QUEUE_FAMILY_IGNORED,
-        infoPtr->image,
-        {
-            VK_IMAGE_ASPECT_COLOR_BIT,
-            0,
-            1,
-            0,
-            1,
-        },
+    const VkImageMemoryBarrier toTransferSrcImageBarrier = {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+        .pNext = nullptr,
+        .srcAccessMask = 0,
+        .dstAccessMask = VK_ACCESS_HOST_READ_BIT,
+        .oldLayout = colorBufferInfo->currentLayout,
+        .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+        .image = colorBufferInfo->image,
+        .subresourceRange =
+            {
+                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                .baseMipLevel = 0,
+                .levelCount = 1,
+                .baseArrayLayer = 0,
+                .layerCount = 1,
+            },
     };
 
-    infoPtr->currentLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
-
-    vk->vkCmdPipelineBarrier(sVkEmulation->commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+    vk->vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
                              VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1,
-                             &presentToTransferSrc);
+                             &toTransferSrcImageBarrier);
 
-    // Copy to staging buffer
-    std::vector<VkBufferImageCopy> regions;
-    if (infoPtr->frameworkFormat == FrameworkFormat::FRAMEWORK_FORMAT_GL_COMPATIBLE) {
-        regions.push_back({
-            0 /* buffer offset */,
-            infoPtr->imageCreateInfoShallow.extent.width,
-            infoPtr->imageCreateInfoShallow.extent.height,
-            {
-                VK_IMAGE_ASPECT_COLOR_BIT,
-                0,
-                0,
-                1,
-            },
-            {0, 0, 0},
-            infoPtr->imageCreateInfoShallow.extent,
-        });
-    } else {
-        // YUV formats
-        bool swapUV = infoPtr->frameworkFormat == FRAMEWORK_FORMAT_YV12;
-        VkExtent3D subplaneExtent = {infoPtr->imageCreateInfoShallow.extent.width / 2,
-                                     infoPtr->imageCreateInfoShallow.extent.height / 2, 1};
-        regions.push_back({
-            0 /* buffer offset */,
-            infoPtr->imageCreateInfoShallow.extent.width,
-            infoPtr->imageCreateInfoShallow.extent.height,
-            {
-                VK_IMAGE_ASPECT_PLANE_0_BIT,
-                0,
-                0,
-                1,
-            },
-            {0, 0, 0},
-            infoPtr->imageCreateInfoShallow.extent,
-        });
-        regions.push_back({
-            infoPtr->imageCreateInfoShallow.extent.width *
-                infoPtr->imageCreateInfoShallow.extent.height /* buffer offset */,
-            subplaneExtent.width,
-            subplaneExtent.height,
-            {
-                (VkImageAspectFlags)(swapUV ? VK_IMAGE_ASPECT_PLANE_2_BIT
-                                            : VK_IMAGE_ASPECT_PLANE_1_BIT),
-                0,
-                0,
-                1,
-            },
-            {0, 0, 0},
-            subplaneExtent,
-        });
-        if (infoPtr->frameworkFormat == FRAMEWORK_FORMAT_YUV_420_888 ||
-            infoPtr->frameworkFormat == FRAMEWORK_FORMAT_YV12) {
-            regions.push_back({
-                infoPtr->imageCreateInfoShallow.extent.width *
-                        infoPtr->imageCreateInfoShallow.extent.height +
-                    subplaneExtent.width * subplaneExtent.height,
-                subplaneExtent.width,
-                subplaneExtent.height,
-                {
-                    (VkImageAspectFlags)(swapUV ? VK_IMAGE_ASPECT_PLANE_1_BIT
-                                                : VK_IMAGE_ASPECT_PLANE_2_BIT),
-                    0,
-                    0,
-                    1,
-                },
-                {0, 0, 0},
-                subplaneExtent,
-            });
-        }
-    }
+    colorBufferInfo->currentLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
 
-    vk->vkCmdCopyBufferToImage(sVkEmulation->commandBuffer, sVkEmulation->staging.buffer,
-                               infoPtr->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, regions.size(),
-                               regions.data());
+    vk->vkCmdCopyImageToBuffer(commandBuffer, colorBufferInfo->image,
+                               VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sVkEmulation->staging.buffer,
+                               bufferImageCopies.size(), bufferImageCopies.data());
 
-    vk->vkEndCommandBuffer(sVkEmulation->commandBuffer);
+    VK_CHECK(vk->vkEndCommandBuffer(commandBuffer));
 
-    VkSubmitInfo submitInfo = {
-        VK_STRUCTURE_TYPE_SUBMIT_INFO, 0, 0,       nullptr, nullptr, 1,
-        &sVkEmulation->commandBuffer,  0, nullptr,
+    const VkSubmitInfo submitInfo = {
+        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+        .pNext = nullptr,
+        .waitSemaphoreCount = 0,
+        .pWaitSemaphores = nullptr,
+        .pWaitDstStageMask = nullptr,
+        .commandBufferCount = 1,
+        .pCommandBuffers = &commandBuffer,
+        .signalSemaphoreCount = 0,
+        .pSignalSemaphores = nullptr,
     };
 
     {
         android::base::AutoLock lock(*sVkEmulation->queueLock);
-        vk->vkQueueSubmit(sVkEmulation->queue, 1, &submitInfo, sVkEmulation->commandBufferFence);
+        VK_CHECK(vk->vkQueueSubmit(sVkEmulation->queue, 1, &submitInfo,
+                                   sVkEmulation->commandBufferFence));
     }
 
     static constexpr uint64_t ANB_MAX_WAIT_NS = 5ULL * 1000ULL * 1000ULL * 1000ULL;
 
-    vk->vkWaitForFences(sVkEmulation->device, 1, &sVkEmulation->commandBufferFence, VK_TRUE,
-                        ANB_MAX_WAIT_NS);
-    vk->vkResetFences(sVkEmulation->device, 1, &sVkEmulation->commandBufferFence);
+    VK_CHECK(vk->vkWaitForFences(sVkEmulation->device, 1, &sVkEmulation->commandBufferFence,
+                                 VK_TRUE, ANB_MAX_WAIT_NS));
 
-    VkMappedMemoryRange toInvalidate = {
-        VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
-        0,
-        sVkEmulation->staging.memory.memory,
-        0,
-        VK_WHOLE_SIZE,
+    VK_CHECK(vk->vkResetFences(sVkEmulation->device, 1, &sVkEmulation->commandBufferFence));
+
+    const VkMappedMemoryRange toInvalidate = {
+        .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
+        .pNext = nullptr,
+        .memory = sVkEmulation->staging.memory.memory,
+        .offset = 0,
+        .size = VK_WHOLE_SIZE,
     };
 
-    vk->vkInvalidateMappedMemoryRanges(sVkEmulation->device, 1, &toInvalidate);
+    VK_CHECK(vk->vkInvalidateMappedMemoryRanges(sVkEmulation->device, 1, &toInvalidate));
+
+    const auto* stagingBufferPtr = sVkEmulation->staging.memory.mappedPtr;
+    std::memcpy(outPixels, stagingBufferPtr, bufferCopySize);
+
+    return true;
+}
+
+bool updateColorBufferFromGl(uint32_t colorBufferHandle) {
+    if (!sVkEmulation || !sVkEmulation->live) {
+        VK_COMMON_ERROR("VkEmulation not available.");
+        return false;
+    }
+
+    AutoLock lock(sVkEmulationLock);
+
+    auto colorBufferInfo = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
+    if (!colorBufferInfo) {
+        VK_COMMON_ERROR("Failed to update ColorBuffer:%d, not found.", colorBufferHandle);
+        return false;
+    }
+
+    if (!colorBufferNeedsTransferBetweenGlAndVk(*colorBufferInfo)) {
+        return true;
+    }
+
+    size_t bytesNeeded = 0;
+    bool result =
+        FrameBuffer::getFB()->readColorBufferContents(colorBufferHandle, &bytesNeeded, nullptr);
+    if (!result) {
+        VK_COMMON_ERROR("Failed to update ColorBuffer:%d, failed to get read contents size.",
+                        colorBufferHandle);
+        return false;
+    }
+
+    std::vector<uint8_t> bytes(bytesNeeded);
+    result = FrameBuffer::getFB()->readColorBufferContents(colorBufferHandle, &bytesNeeded,
+                                                           bytes.data());
+    if (!result) {
+        VK_COMMON_ERROR("Failed to update ColorBuffer:%d, failed to read contents.",
+                        colorBufferHandle);
+        return false;
+    }
+
+    return updateColorBufferFromBytesLocked(
+        colorBufferHandle, 0, 0, colorBufferInfo->imageCreateInfoShallow.extent.width,
+        colorBufferInfo->imageCreateInfoShallow.extent.height, bytes.data());
+}
+
+bool updateColorBufferFromBytes(uint32_t colorBufferHandle, uint32_t x, uint32_t y, uint32_t w,
+                                uint32_t h, const void* pixels) {
+    if (!sVkEmulation || !sVkEmulation->live) {
+        VK_COMMON_ERROR("VkEmulation not available.");
+        return false;
+    }
+
+    auto vk = sVkEmulation->dvk;
+    AutoLock lock(sVkEmulationLock);
+    return updateColorBufferFromBytesLocked(colorBufferHandle, x, y, w, h, pixels);
+}
+
+bool updateColorBufferFromBytesLocked(uint32_t colorBufferHandle, uint32_t x, uint32_t y,
+                                      uint32_t w, uint32_t h, const void* pixels) {
+    if (!sVkEmulation || !sVkEmulation->live) {
+        VK_COMMON_ERROR("VkEmulation not available.");
+        return false;
+    }
+
+    auto vk = sVkEmulation->dvk;
+
+    auto colorBufferInfo = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
+    if (!colorBufferInfo) {
+        VK_COMMON_ERROR("Failed to update ColorBuffer:%d, not found.", colorBufferHandle);
+        return false;
+    }
+
+    if (!colorBufferInfo->image) {
+        VK_COMMON_ERROR("Failed to update ColorBuffer:%d, no VkImage.", colorBufferHandle);
+        return false;
+    }
+
+    if (x != 0 || y != 0 || w != colorBufferInfo->imageCreateInfoShallow.extent.width ||
+        h != colorBufferInfo->imageCreateInfoShallow.extent.height) {
+        VK_COMMON_ERROR("Failed to update ColorBuffer:%d, unhandled subrect.", colorBufferHandle);
+        return false;
+    }
+
+    std::size_t bufferCopySize = 0;
+    std::vector<VkBufferImageCopy> bufferImageCopies;
+    if (!getFormatTransferInfo(colorBufferInfo->imageCreateInfoShallow.format,
+                               colorBufferInfo->imageCreateInfoShallow.extent.width,
+                               colorBufferInfo->imageCreateInfoShallow.extent.height,
+                               &bufferCopySize, &bufferImageCopies)) {
+        VK_COMMON_ERROR("Failed to update ColorBuffer:%d, unable to get transfer info.",
+                        colorBufferHandle);
+        return false;
+    }
+
+    const VkDeviceSize stagingBufferSize = sVkEmulation->staging.size;
+    if (bufferCopySize > stagingBufferSize) {
+        VK_COMMON_ERROR("Failed to update ColorBuffer:%d, transfer size %" PRIu64
+                        " too large for staging buffer size:%" PRIu64 ".",
+                        colorBufferHandle, bufferCopySize, stagingBufferSize);
+        return false;
+    }
+
+    auto* stagingBufferPtr = sVkEmulation->staging.memory.mappedPtr;
+    std::memcpy(stagingBufferPtr, pixels, bufferCopySize);
+
+    // Avoid transitioning from VK_IMAGE_LAYOUT_UNDEFINED. Unfortunetly, Android does not
+    // yet have a mechanism for sharing the expected VkImageLayout. However, the Vulkan
+    // spec's image layout transition sections says "If the old layout is
+    // VK_IMAGE_LAYOUT_UNDEFINED, the contents of that range may be discarded." Some
+    // Vulkan drivers have been observed to actually perform the discard which leads to
+    // ColorBuffer-s being unintentionally cleared. See go/ahb-vkimagelayout for a more
+    // thorough write up.
+    if (colorBufferInfo->currentLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
+        colorBufferInfo->currentLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
+    }
+
+    // Record our synchronization commands.
+    const VkCommandBufferBeginInfo beginInfo = {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+        .pNext = nullptr,
+        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
+    };
+
+    VkCommandBuffer commandBuffer = sVkEmulation->commandBuffer;
+
+    VK_CHECK(vk->vkBeginCommandBuffer(commandBuffer, &beginInfo));
+
+    const VkImageMemoryBarrier toTransferDstImageBarrier = {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+        .pNext = nullptr,
+        .srcAccessMask = 0,
+        .dstAccessMask = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
+        .oldLayout = colorBufferInfo->currentLayout,
+        .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+        .image = colorBufferInfo->image,
+        .subresourceRange =
+            {
+                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                .baseMipLevel = 0,
+                .levelCount = 1,
+                .baseArrayLayer = 0,
+                .layerCount = 1,
+            },
+    };
+
+    vk->vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+                             VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1,
+                             &toTransferDstImageBarrier);
+
+    colorBufferInfo->currentLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
+
+    // Copy to staging buffer
+    vk->vkCmdCopyBufferToImage(commandBuffer, sVkEmulation->staging.buffer, colorBufferInfo->image,
+                               VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, bufferImageCopies.size(),
+                               bufferImageCopies.data());
+
+    VK_CHECK(vk->vkEndCommandBuffer(commandBuffer));
+
+    const VkSubmitInfo submitInfo = {
+        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+        .pNext = nullptr,
+        .waitSemaphoreCount = 0,
+        .pWaitSemaphores = nullptr,
+        .pWaitDstStageMask = nullptr,
+        .commandBufferCount = 1,
+        .pCommandBuffers = &commandBuffer,
+        .signalSemaphoreCount = 0,
+        .pSignalSemaphores = nullptr,
+    };
+
+    {
+        android::base::AutoLock lock(*sVkEmulation->queueLock);
+        VK_CHECK(vk->vkQueueSubmit(sVkEmulation->queue, 1, &submitInfo,
+                                   sVkEmulation->commandBufferFence));
+    }
+
+    static constexpr uint64_t ANB_MAX_WAIT_NS = 5ULL * 1000ULL * 1000ULL * 1000ULL;
+
+    VK_CHECK(vk->vkWaitForFences(sVkEmulation->device, 1, &sVkEmulation->commandBufferFence,
+                                 VK_TRUE, ANB_MAX_WAIT_NS));
+
+    VK_CHECK(vk->vkResetFences(sVkEmulation->device, 1, &sVkEmulation->commandBufferFence));
+
+    const VkMappedMemoryRange toInvalidate = {
+        .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
+        .pNext = nullptr,
+        .memory = sVkEmulation->staging.memory.memory,
+        .offset = 0,
+        .size = VK_WHOLE_SIZE,
+    };
+    VK_CHECK(vk->vkInvalidateMappedMemoryRanges(sVkEmulation->device, 1, &toInvalidate));
+
     return true;
 }
 
diff --git a/stream-servers/vulkan/VkCommonOperations.h b/stream-servers/vulkan/VkCommonOperations.h
index a2a1bd5..2be2a8a 100644
--- a/stream-servers/vulkan/VkCommonOperations.h
+++ b/stream-servers/vulkan/VkCommonOperations.h
@@ -383,13 +383,23 @@
                         void** mappedPtr = nullptr);
 bool teardownVkColorBuffer(uint32_t colorBufferHandle);
 VkEmulation::ColorBufferInfo getColorBufferInfo(uint32_t colorBufferHandle);
-bool updateColorBufferFromVkImage(uint32_t colorBufferHandle);
-bool updateVkImageFromColorBuffer(uint32_t colorBufferHandle);
 VK_EXT_MEMORY_HANDLE getColorBufferExtMemoryHandle(uint32_t colorBufferHandle);
 MTLTextureRef getColorBufferMTLTexture(uint32_t colorBufferHandle);
 bool setColorBufferVulkanMode(uint32_t colorBufferHandle, uint32_t vulkanMode);
 int32_t mapGpaToBufferHandle(uint32_t bufferHandle, uint64_t gpa, uint64_t size = 0);
 
+bool readColorBufferToGl(uint32_t colorBufferHandle);
+bool readColorBufferToBytes(uint32_t colorBufferHandle, uint32_t x, uint32_t y, uint32_t w,
+                            uint32_t h, void* outPixels);
+bool readColorBufferToBytesLocked(uint32_t colorBufferHandle, uint32_t x, uint32_t y, uint32_t w,
+                                  uint32_t h, void* outPixels);
+
+bool updateColorBufferFromGl(uint32_t colorBufferHandle);
+bool updateColorBufferFromBytes(uint32_t colorBufferHandle, uint32_t x, uint32_t y, uint32_t w,
+                                uint32_t h, const void* pixels);
+bool updateColorBufferFromBytesLocked(uint32_t colorBufferHandle, uint32_t x, uint32_t y,
+                                      uint32_t w, uint32_t h, const void* pixels);
+
 // Data buffer operations
 
 bool setupVkBuffer(uint32_t bufferHandle, bool vulkanOnly = false, uint32_t memoryProperty = 0,
diff --git a/stream-servers/vulkan/VkDecoderGlobalState.cpp b/stream-servers/vulkan/VkDecoderGlobalState.cpp
index 19599a4..9a320f4 100644
--- a/stream-servers/vulkan/VkDecoderGlobalState.cpp
+++ b/stream-servers/vulkan/VkDecoderGlobalState.cpp
@@ -2881,7 +2881,7 @@
                 &localAllocInfo.allocationSize, &localAllocInfo.memoryTypeIndex, &mappedPtr);
 
             if (!vulkanOnly) {
-                updateVkImageFromColorBuffer(importCbInfoPtr->colorBuffer);
+                updateColorBufferFromGl(importCbInfoPtr->colorBuffer);
             }
 
             if (m_emu->instanceSupportsExternalMemoryCapabilities) {
diff --git a/stream-servers/vulkan/VkFormatUtils.cpp b/stream-servers/vulkan/VkFormatUtils.cpp
new file mode 100644
index 0000000..7685ea6
--- /dev/null
+++ b/stream-servers/vulkan/VkFormatUtils.cpp
@@ -0,0 +1,193 @@
+// Copyright 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expresso or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "VkFormatUtils.h"
+
+#include <unordered_map>
+
+namespace {
+
+struct FormatPlaneLayout {
+    uint32_t horizontalSubsampling = 1;
+    uint32_t verticalSubsampling = 1;
+    uint32_t sampleIncrementBytes = 0;
+    VkImageAspectFlags aspectMask = 0;
+};
+
+struct FormatPlaneLayouts {
+    uint32_t horizontalAlignmentPixels = 1;
+    std::vector<FormatPlaneLayout> planeLayouts;
+};
+
+const std::unordered_map<VkFormat, FormatPlaneLayouts>& getFormatPlaneLayoutsMap() {
+    static const auto* kPlaneLayoutsMap = []() {
+        auto* map = new std::unordered_map<VkFormat, FormatPlaneLayouts>({
+            {VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16,
+             {
+                 .horizontalAlignmentPixels = 2,
+                 .planeLayouts =
+                     {
+                         {
+                             .horizontalSubsampling = 1,
+                             .verticalSubsampling = 1,
+                             .sampleIncrementBytes = 2,
+                             .aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT,
+                         },
+                         {
+                             .horizontalSubsampling = 2,
+                             .verticalSubsampling = 2,
+                             .sampleIncrementBytes = 4,
+                             .aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT,
+                         },
+                     },
+             }},
+            {VK_FORMAT_G8_B8R8_2PLANE_420_UNORM,
+             {
+                 .horizontalAlignmentPixels = 2,
+                 .planeLayouts =
+                     {
+                         {
+                             .horizontalSubsampling = 1,
+                             .verticalSubsampling = 1,
+                             .sampleIncrementBytes = 1,
+                             .aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT,
+                         },
+                         {
+                             .horizontalSubsampling = 2,
+                             .verticalSubsampling = 2,
+                             .sampleIncrementBytes = 2,
+                             .aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT,
+                         },
+                     },
+             }},
+            {VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM,
+             {
+                 .horizontalAlignmentPixels = 32,
+                 .planeLayouts =
+                     {
+                         {
+                             .horizontalSubsampling = 1,
+                             .verticalSubsampling = 1,
+                             .sampleIncrementBytes = 1,
+                             .aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT,
+                         },
+                         {
+                             .horizontalSubsampling = 2,
+                             .verticalSubsampling = 2,
+                             .sampleIncrementBytes = 1,
+                             .aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT,
+                         },
+                         {
+                             .horizontalSubsampling = 2,
+                             .verticalSubsampling = 2,
+                             .sampleIncrementBytes = 1,
+                             .aspectMask = VK_IMAGE_ASPECT_PLANE_2_BIT,
+                         },
+                     },
+             }},
+        });
+
+#define ADD_SINGLE_PLANE_FORMAT_INFO(format, bpp)            \
+    (*map)[format] = FormatPlaneLayouts{                     \
+        .horizontalAlignmentPixels = 1,                      \
+        .planeLayouts =                                      \
+            {                                                \
+                {                                            \
+                    .horizontalSubsampling = 1,              \
+                    .verticalSubsampling = 1,                \
+                    .sampleIncrementBytes = bpp,             \
+                    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, \
+                },                                           \
+            },                                               \
+    };
+        LIST_VK_FORMATS_LINEAR(ADD_SINGLE_PLANE_FORMAT_INFO)
+#undef ADD_SINGLE_PLANE_FORMAT_INFO
+
+        return map;
+    }();
+    return *kPlaneLayoutsMap;
+}
+
+inline uint32_t alignToPower2(uint32_t val, uint32_t align) {
+    return (val + (align - 1)) & ~(align - 1);
+}
+
+}  // namespace
+
+const FormatPlaneLayouts* getFormatPlaneLayouts(VkFormat format) {
+    const auto& formatPlaneLayoutsMap = getFormatPlaneLayoutsMap();
+
+    auto it = formatPlaneLayoutsMap.find(format);
+    if (it == formatPlaneLayoutsMap.end()) {
+        return nullptr;
+    }
+    return &it->second;
+}
+
+bool getFormatTransferInfo(VkFormat format, uint32_t width, uint32_t height,
+                           VkDeviceSize* outStagingBufferCopySize,
+                           std::vector<VkBufferImageCopy>* outBufferImageCopies) {
+    const FormatPlaneLayouts* formatInfo = getFormatPlaneLayouts(format);
+    if (formatInfo == nullptr) {
+        ERR("Unhandled format: %s", string_VkFormat(format));
+        return false;
+    }
+
+    const uint32_t alignedWidth = alignToPower2(width, formatInfo->horizontalAlignmentPixels);
+    const uint32_t alignedHeight = height;
+    uint32_t cumulativeOffset = 0;
+    uint32_t cumulativeSize = 0;
+    for (const FormatPlaneLayout& planeInfo : formatInfo->planeLayouts) {
+        const uint32_t planeOffset = cumulativeOffset;
+        const uint32_t planeWidth = alignedWidth / planeInfo.horizontalSubsampling;
+        const uint32_t planeHeight = alignedHeight / planeInfo.verticalSubsampling;
+        const uint32_t planeBpp = planeInfo.sampleIncrementBytes;
+        const uint32_t planeStrideTexels = planeWidth;
+        const uint32_t planeStrideBytes = planeStrideTexels * planeBpp;
+        const uint32_t planeSize = planeHeight * planeStrideBytes;
+        if (outBufferImageCopies) {
+            outBufferImageCopies->emplace_back(VkBufferImageCopy{
+                .bufferOffset = planeOffset,
+                .bufferRowLength = planeStrideTexels,
+                .bufferImageHeight = 0,
+                .imageSubresource =
+                    {
+                        .aspectMask = planeInfo.aspectMask,
+                        .mipLevel = 0,
+                        .baseArrayLayer = 0,
+                        .layerCount = 1,
+                    },
+                .imageOffset =
+                    {
+                        .x = 0,
+                        .y = 0,
+                        .z = 0,
+                    },
+                .imageExtent =
+                    {
+                        .width = planeWidth,
+                        .height = planeHeight,
+                        .depth = 1,
+                    },
+            });
+        }
+        cumulativeOffset += planeSize;
+        cumulativeSize += planeSize;
+    }
+    if (outStagingBufferCopySize) {
+        *outStagingBufferCopySize = cumulativeSize;
+    }
+
+    return true;
+}
diff --git a/stream-servers/vulkan/VkFormatUtils.h b/stream-servers/vulkan/VkFormatUtils.h
index 9cafb27..323146d 100644
--- a/stream-servers/vulkan/VkFormatUtils.h
+++ b/stream-servers/vulkan/VkFormatUtils.h
@@ -15,13 +15,17 @@
 
 #include <vulkan/vulkan_core.h>
 
+#include <vector>
+
+#include "host-common/logging.h"
+#include "vulkan/vk_enum_string_helper.h"
+
 // Header library that captures common patterns when working with
 // Vulkan formats:
 // - Macros to iterate over categories of formats
 // - Add often-used parameters like the bytes per pixel and ASTC block size
 
 #define LIST_VK_FORMATS_LINEAR(f)                      \
-    f(VK_FORMAT_UNDEFINED, 0)                          \
     f(VK_FORMAT_R4G4_UNORM_PACK8, 1)                   \
     f(VK_FORMAT_R4G4B4A4_UNORM_PACK16, 2)              \
     f(VK_FORMAT_B4G4R4A4_UNORM_PACK16, 2)              \
@@ -273,6 +277,7 @@
 
     LIST_VK_FORMATS_LINEAR(VK_FORMATS_LINEAR_GET_PIXEL_SIZE)
 
+    ERR("Unhandled format: %s", string_VkFormat(format));
     return 0;
 }
 
@@ -642,4 +647,11 @@
         default:
             return false;
     }
-}
\ No newline at end of file
+}
+
+// Returns the size in bytes needed to copy an image with the given format,
+// width, and height to a staging buffer and the VkBufferImageCopy-s needed
+// to copy from a staging buffer to destination VkImage.
+bool getFormatTransferInfo(VkFormat format, uint32_t width, uint32_t height,
+                           VkDeviceSize* outStagingBufferCopySize,
+                           std::vector<VkBufferImageCopy>* outBufferImageCopies);
diff --git a/stream-servers/vulkan/VkFormatUtils_unittest.cpp b/stream-servers/vulkan/VkFormatUtils_unittest.cpp
new file mode 100644
index 0000000..3cdb76a
--- /dev/null
+++ b/stream-servers/vulkan/VkFormatUtils_unittest.cpp
@@ -0,0 +1,259 @@
+// copyright (c) 2022 the android open source project
+//
+// licensed under the apache license, version 2.0 (the "license");
+// you may not use this file except in compliance with the license.
+// you may obtain a copy of the license at
+//
+// http://www.apache.org/licenses/license-2.0
+//
+// unless required by applicable law or agreed to in writing, software
+// distributed under the license is distributed on an "as is" basis,
+// without warranties or conditions of any kind, either express or implied.
+// see the license for the specific language governing permissions and
+// limitations under the license.
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "VkFormatUtils.h"
+
+namespace {
+
+using ::testing::AllOf;
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::ExplainMatchResult;
+using ::testing::Field;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+
+MATCHER_P(EqsVkExtent3D, expected, "") {
+    return ExplainMatchResult(AllOf(Field("width", &VkExtent3D::width, Eq(expected.width)),
+                                    Field("height", &VkExtent3D::height, Eq(expected.height)),
+                                    Field("depth", &VkExtent3D::depth, Eq(expected.depth))),
+                              arg, result_listener);
+}
+
+MATCHER_P(EqsVkImageSubresourceLayers, expected, "") {
+    return ExplainMatchResult(
+        AllOf(Field("aspectMask", &VkImageSubresourceLayers::aspectMask, Eq(expected.aspectMask)),
+              Field("mipLevel", &VkImageSubresourceLayers::mipLevel, Eq(expected.mipLevel)),
+              Field("baseArrayLayer", &VkImageSubresourceLayers::baseArrayLayer,
+                    Eq(expected.baseArrayLayer)),
+              Field("layerCount", &VkImageSubresourceLayers::layerCount, Eq(expected.layerCount))),
+        arg, result_listener);
+}
+
+MATCHER_P(EqsVkOffset3D, expected, "") {
+    return ExplainMatchResult(AllOf(Field("x", &VkOffset3D::x, Eq(expected.x)),
+                                    Field("y", &VkOffset3D::y, Eq(expected.y)),
+                                    Field("z", &VkOffset3D::z, Eq(expected.z))),
+                              arg, result_listener);
+}
+
+MATCHER_P(EqsVkBufferImageCopy, expected, "") {
+    return ExplainMatchResult(
+        AllOf(Field("bufferOffset", &VkBufferImageCopy::bufferOffset, Eq(expected.bufferOffset)),
+              Field("bufferRowLength", &VkBufferImageCopy::bufferRowLength,
+                    Eq(expected.bufferRowLength)),
+              Field("bufferImageHeight", &VkBufferImageCopy::bufferImageHeight,
+                    Eq(expected.bufferImageHeight)),
+              Field("imageSubresource", &VkBufferImageCopy::imageSubresource,
+                    EqsVkImageSubresourceLayers(expected.imageSubresource)),
+              Field("imageOffset", &VkBufferImageCopy::imageOffset,
+                    EqsVkOffset3D(expected.imageOffset)),
+              Field("imageExtent", &VkBufferImageCopy::imageExtent,
+                    EqsVkExtent3D(expected.imageExtent))),
+        arg, result_listener);
+}
+
+TEST(VkFormatUtilsTest, GetTransferInfoInvalidFormat) {
+    const VkFormat format = VK_FORMAT_UNDEFINED;
+    const uint32_t width = 16;
+    const uint32_t height = 16;
+    ASSERT_THAT(getFormatTransferInfo(format, width, height, nullptr, nullptr), IsFalse());
+}
+
+TEST(VkFormatUtilsTest, GetTransferInfoRGBA) {
+    const VkFormat format = VK_FORMAT_R8G8B8A8_UNORM;
+    const uint32_t width = 16;
+    const uint32_t height = 16;
+
+    VkDeviceSize bufferCopySize;
+    std::vector<VkBufferImageCopy> bufferImageCopies;
+    ASSERT_THAT(getFormatTransferInfo(format, width, height, &bufferCopySize, &bufferImageCopies),
+                IsTrue());
+    EXPECT_THAT(bufferCopySize, Eq(1024));
+    ASSERT_THAT(bufferImageCopies, ElementsAre(EqsVkBufferImageCopy(VkBufferImageCopy{
+                                       .bufferOffset = 0,
+                                       .bufferRowLength = 16,
+                                       .bufferImageHeight = 0,
+                                       .imageSubresource =
+                                           {
+                                               .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                                               .mipLevel = 0,
+                                               .baseArrayLayer = 0,
+                                               .layerCount = 1,
+                                           },
+                                       .imageOffset =
+                                           {
+                                               .x = 0,
+                                               .y = 0,
+                                               .z = 0,
+                                           },
+                                       .imageExtent =
+                                           {
+                                               .width = 16,
+                                               .height = 16,
+                                               .depth = 1,
+                                           },
+                                   })));
+}
+
+TEST(VkFormatUtilsTest, GetTransferInfoNV12OrNV21) {
+    const VkFormat format = VK_FORMAT_G8_B8R8_2PLANE_420_UNORM;
+    const uint32_t width = 16;
+    const uint32_t height = 16;
+
+    VkDeviceSize bufferCopySize;
+    std::vector<VkBufferImageCopy> bufferImageCopies;
+    ASSERT_THAT(getFormatTransferInfo(format, width, height, &bufferCopySize, &bufferImageCopies),
+                IsTrue());
+    EXPECT_THAT(bufferCopySize, Eq(384));
+    ASSERT_THAT(bufferImageCopies,
+                ElementsAre(EqsVkBufferImageCopy(VkBufferImageCopy{
+                                .bufferOffset = 0,
+                                .bufferRowLength = 16,
+                                .bufferImageHeight = 0,
+                                .imageSubresource =
+                                    {
+                                        .aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT,
+                                        .mipLevel = 0,
+                                        .baseArrayLayer = 0,
+                                        .layerCount = 1,
+                                    },
+                                .imageOffset =
+                                    {
+                                        .x = 0,
+                                        .y = 0,
+                                        .z = 0,
+                                    },
+                                .imageExtent =
+                                    {
+                                        .width = 16,
+                                        .height = 16,
+                                        .depth = 1,
+                                    },
+                            }),
+                            EqsVkBufferImageCopy(VkBufferImageCopy{
+                                .bufferOffset = 256,
+                                .bufferRowLength = 8,
+                                .bufferImageHeight = 0,
+                                .imageSubresource =
+                                    {
+                                        .aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT,
+                                        .mipLevel = 0,
+                                        .baseArrayLayer = 0,
+                                        .layerCount = 1,
+                                    },
+                                .imageOffset =
+                                    {
+                                        .x = 0,
+                                        .y = 0,
+                                        .z = 0,
+                                    },
+                                .imageExtent =
+                                    {
+                                        .width = 8,
+                                        .height = 8,
+                                        .depth = 1,
+                                    },
+                            })));
+}
+
+TEST(VkFormatUtilsTest, GetTransferInfoYV12OrYV21) {
+    const VkFormat format = VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM;
+    const uint32_t width = 32;
+    const uint32_t height = 32;
+
+    VkDeviceSize bufferCopySize;
+    std::vector<VkBufferImageCopy> bufferImageCopies;
+    ASSERT_THAT(getFormatTransferInfo(format, width, height, &bufferCopySize, &bufferImageCopies),
+                IsTrue());
+    EXPECT_THAT(bufferCopySize, Eq(1536));
+    ASSERT_THAT(bufferImageCopies,
+                ElementsAre(EqsVkBufferImageCopy(VkBufferImageCopy{
+                                .bufferOffset = 0,
+                                .bufferRowLength = 32,
+                                .bufferImageHeight = 0,
+                                .imageSubresource =
+                                    {
+                                        .aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT,
+                                        .mipLevel = 0,
+                                        .baseArrayLayer = 0,
+                                        .layerCount = 1,
+                                    },
+                                .imageOffset =
+                                    {
+                                        .x = 0,
+                                        .y = 0,
+                                        .z = 0,
+                                    },
+                                .imageExtent =
+                                    {
+                                        .width = 32,
+                                        .height = 32,
+                                        .depth = 1,
+                                    },
+                            }),
+                            EqsVkBufferImageCopy(VkBufferImageCopy{
+                                .bufferOffset = 1024,
+                                .bufferRowLength = 16,
+                                .bufferImageHeight = 0,
+                                .imageSubresource =
+                                    {
+                                        .aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT,
+                                        .mipLevel = 0,
+                                        .baseArrayLayer = 0,
+                                        .layerCount = 1,
+                                    },
+                                .imageOffset =
+                                    {
+                                        .x = 0,
+                                        .y = 0,
+                                        .z = 0,
+                                    },
+                                .imageExtent =
+                                    {
+                                        .width = 16,
+                                        .height = 16,
+                                        .depth = 1,
+                                    },
+                            }),
+                            EqsVkBufferImageCopy(VkBufferImageCopy{
+                                .bufferOffset = 1280,
+                                .bufferRowLength = 16,
+                                .bufferImageHeight = 0,
+                                .imageSubresource =
+                                    {
+                                        .aspectMask = VK_IMAGE_ASPECT_PLANE_2_BIT,
+                                        .mipLevel = 0,
+                                        .baseArrayLayer = 0,
+                                        .layerCount = 1,
+                                    },
+                                .imageOffset =
+                                    {
+                                        .x = 0,
+                                        .y = 0,
+                                        .z = 0,
+                                    },
+                                .imageExtent =
+                                    {
+                                        .width = 16,
+                                        .height = 16,
+                                        .depth = 1,
+                                    },
+                            })));
+}
+
+}  // namespace
\ No newline at end of file