Decouple CompositorVk from DisplayVk

... so that Cuttlefish running without native swapchain support
but with ANGLE in the guest can still use CompositorVk for host
composition.

Bug: b/229145718
Test: `launch_cvd --gpu_mode=gfxstream`
Test: `launch_cvd --gpu_mode=gfxstream` with WIP ANGLE
Test: `cd device/generic/vulkan-cereal/build && ./Vulkan_unittests`
Test: `cd device/generic/vulkan-cereal/build && ninja test`
Test: `ctest --test-dir device/generic/vulkan-cereal/build`
Test: `atest --host gfxstream_compositorvk_test`
Change-Id: Idd124257a2f6b84a4c72bec52c179c1f09252692
diff --git a/stream-servers/Android.bp b/stream-servers/Android.bp
index 2318ef7..58e31fd 100644
--- a/stream-servers/Android.bp
+++ b/stream-servers/Android.bp
@@ -47,6 +47,9 @@
         "gfxstream_lz4",
         "gfxstream_compressedTextures",
     ],
+    export_static_lib_headers: [
+        "gfxstream_vulkan_cereal_host",
+    ],
     shared_libs: [
         "liblog", // gfxstream_base uses this via perfetto-libperfettobase
     ],
@@ -95,3 +98,65 @@
         }
     }
 }
+
+cc_library {
+    name: "libgfxstream_test_image_utils",
+    defaults: [ "gfxstream_defaults" ],
+    shared_libs: [
+        "libbase",
+    ],
+    static_libs: [
+        "libgfxstream_stb",
+    ],
+    srcs: [
+        "tests/ImageUtils.cpp",
+    ],
+}
+
+// Run with `atest --host gfxstream_compositorvk_test`
+cc_test_host {
+    name: "gfxstream_compositorvk_test",
+    defaults: [ "gfxstream_defaults" ],
+    srcs: [
+        "tests/CompositorVk_unittest.cpp",
+    ],
+    data: [
+        "tests/testdata/256x256_android.png",
+        "tests/testdata/256x256_android_with_transparency.png",
+        "tests/testdata/256x256_golden_blend_premultiplied.png",
+        "tests/testdata/256x256_golden_crop.png",
+        "tests/testdata/256x256_golden_simple_composition.png",
+        "tests/testdata/256x256_golden_multiple_layers.png",
+        "tests/testdata/256x256_golden_multiple_targets_0.png",
+        "tests/testdata/256x256_golden_multiple_targets_1.png",
+        "tests/testdata/256x256_golden_multiple_targets_2.png",
+        "tests/testdata/256x256_golden_multiple_targets_3.png",
+        "tests/testdata/256x256_golden_multiple_targets_4.png",
+        "tests/testdata/256x256_golden_multiple_targets_5.png",
+        "tests/testdata/256x256_golden_multiple_targets_6.png",
+        "tests/testdata/256x256_golden_multiple_targets_7.png",
+        "tests/testdata/256x256_golden_multiple_targets_8.png",
+        "tests/testdata/256x256_golden_multiple_targets_9.png",
+        "tests/testdata/256x256_golden_transform_none.png",
+        "tests/testdata/256x256_golden_transform_fliph.png",
+        "tests/testdata/256x256_golden_transform_flipv.png",
+        "tests/testdata/256x256_golden_transform_rot90.png",
+        "tests/testdata/256x256_golden_transform_rot180.png",
+        "tests/testdata/256x256_golden_transform_rot270.png",
+        "tests/testdata/256x256_golden_transform_fliphrot90.png",
+        "tests/testdata/256x256_golden_transform_flipvrot90.png",
+    ],
+    shared_libs: [
+        "libbase",
+        "libgfxstream_backend",
+        "libgfxstream_test_image_utils",
+    ],
+    static_libs: [
+        "gfxstream_vulkan_server",
+        "libc++fs",
+    ],
+    test_options: {
+        // Disabled by default as requires Vulkan.
+        unit_test: false,
+    },
+}
diff --git a/stream-servers/CMakeLists.txt b/stream-servers/CMakeLists.txt
index 4e655d5..8f9e1c6 100644
--- a/stream-servers/CMakeLists.txt
+++ b/stream-servers/CMakeLists.txt
@@ -135,7 +135,8 @@
     tests/ShaderUtils.cpp
     tests/GLSnapshotTestDispatch.cpp
     tests/GLSnapshotTestStateUtils.cpp
-    tests/HelloTriangleImp.cpp)
+    tests/HelloTriangleImp.cpp
+    tests/ImageUtils.cpp)
 target_include_directories(
     stream-server-testing-support
     PUBLIC
@@ -149,6 +150,7 @@
     stream-server-testing-support
     PUBLIC
     gfxstream_backend_static
+    gfxstream_stb
     OSWindow
     gtest)
 
@@ -232,6 +234,16 @@
     gtest
     gtest_main
     gmock)
-gtest_discover_tests(Vulkan_unittests)
+gtest_discover_tests(
+    Vulkan_unittests
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+
+file(GLOB Vulkan_unittests_datafiles "tests/testdata/*.png")
+add_custom_command(TARGET Vulkan_unittests POST_BUILD
+                   COMMAND ${CMAKE_COMMAND} -E make_directory
+                        ${CMAKE_BINARY_DIR}/tests/testdata
+                   COMMAND ${CMAKE_COMMAND} -E copy_if_different
+                        ${Vulkan_unittests_datafiles}
+                        ${CMAKE_BINARY_DIR}/tests/testdata)
 
 set_test_include_files()
diff --git a/stream-servers/ColorBuffer.cpp b/stream-servers/ColorBuffer.cpp
index 0eba85a..06c272a 100644
--- a/stream-servers/ColorBuffer.cpp
+++ b/stream-servers/ColorBuffer.cpp
@@ -987,10 +987,8 @@
 #else
     int handle,
 #endif
-    uint64_t size, bool dedicated, bool linearTiling, bool vulkanOnly,
-    std::shared_ptr<DisplayVk::DisplayBufferInfo> displayBufferVk) {
+    uint64_t size, bool dedicated, bool linearTiling, bool vulkanOnly) {
     RecursiveScopedHelperContext context(m_helper);
-    m_displayBufferVk = std::move(displayBufferVk);
     s_gles2.glCreateMemoryObjectsEXT(1, &m_memoryObject);
     if (dedicated) {
         static const GLint DEDICATED_FLAG = GL_TRUE;
diff --git a/stream-servers/ColorBuffer.h b/stream-servers/ColorBuffer.h
index 6a2dc90..0e9388b 100644
--- a/stream-servers/ColorBuffer.h
+++ b/stream-servers/ColorBuffer.h
@@ -255,10 +255,6 @@
     void postLayer(const ComposeLayer& l, int frameWidth, int frameHeight);
     GLuint getTexture();
 
-    const std::shared_ptr<DisplayVk::DisplayBufferInfo>& getDisplayBufferVk()
-        const {
-        return m_displayBufferVk;
-    };
     std::unique_ptr<BorrowedImageInfo> getBorrowedImageInfo();
 
     // ColorBuffer backing change methods
@@ -271,8 +267,7 @@
 #else
         int handle,
 #endif
-        uint64_t size, bool dedicated, bool linearTiling, bool vulkanOnly,
-        std::shared_ptr<DisplayVk::DisplayBufferInfo> displayBufferVk);
+        uint64_t size, bool dedicated, bool linearTiling, bool vulkanOnly);
     // Change to EGL native pixmap
     bool importEglNativePixmap(void* pixmap, bool preserveContent);
     // Change to some other native EGL image.  nativeEglImage must not have
@@ -350,9 +345,6 @@
     GLuint m_buf = 0;
     uint32_t m_displayId = 0;
     bool m_BRSwizzle = false;
-    // Won't share with others so that m_displayBufferVk lives shorter than this
-    // ColorBuffer.
-    std::shared_ptr<DisplayVk::DisplayBufferInfo> m_displayBufferVk;
 };
 
 typedef std::shared_ptr<ColorBuffer> ColorBufferPtr;
diff --git a/stream-servers/FrameBuffer.cpp b/stream-servers/FrameBuffer.cpp
index a3aaab1..8d4ed4b 100644
--- a/stream-servers/FrameBuffer.cpp
+++ b/stream-servers/FrameBuffer.cpp
@@ -16,11 +16,13 @@
 
 #include "FrameBuffer.h"
 
-#include <iomanip>
 #include <stdio.h>
 #include <string.h>
 #include <time.h>
 
+#include <iomanip>
+
+#include "CompositorGl.h"
 #include "DispatchTables.h"
 #include "GLESVersionDetector.h"
 #include "Hwc2.h"
@@ -514,6 +516,9 @@
         feature_is_enabled(
             kFeature_GuestUsesAngle);
 
+    fb->m_useVulkanComposition = feature_is_enabled(kFeature_GuestUsesAngle) ||
+                                 feature_is_enabled(kFeature_VulkanNativeSwapchain);
+
     std::unique_ptr<goldfish_vk::VkEmulationFeatures> vkEmulationFeatures =
         std::make_unique<goldfish_vk::VkEmulationFeatures>(goldfish_vk::VkEmulationFeatures{
             .glInteropSupported = false,  // Set later.
@@ -524,6 +529,7 @@
                 android::base::getEnvironmentVariable(
                     "ANDROID_EMU_VK_DISABLE_USE_CREATE_RESOURCES_WITH_REQUIREMENTS")
                     .empty(),
+            .useVulkanComposition = fb->m_useVulkanComposition,
             .useVulkanNativeSwapchain = feature_is_enabled(kFeature_VulkanNativeSwapchain),
             .guestRenderDoc = std::move(renderDocMultipleVkInstances),
         });
@@ -872,9 +878,18 @@
         }
     }
 
-    GL_LOG("Performing composition using CompositorGl.");
-    fb->m_compositorGl = std::make_unique<CompositorGl>();
-    fb->m_compositor = fb->m_compositorGl.get();
+    if (fb->m_useVulkanComposition) {
+        if (!vkEmu->compositorVk) {
+            ERR("Failed to get CompositorVk from VkEmulation.");
+            return false;
+        }
+        GL_LOG("Performing composition using CompositorVk.");
+        fb->m_compositor = vkEmu->compositorVk.get();
+    } else {
+        GL_LOG("Performing composition using CompositorGl.");
+        fb->m_compositorGl = std::make_unique<CompositorGl>();
+        fb->m_compositor = fb->m_compositorGl.get();
+    }
 
     INFO("Graphics Adapter Vendor %s", fb->m_graphicsAdapterVendor.c_str());
     INFO("Graphics Adapter %s", fb->m_graphicsAdapterName.c_str());
@@ -919,17 +934,8 @@
         ERR("FB: importMemoryToColorBuffer cb handle %#x not found", colorBufferHandle);
         return false;
     }
-
-    std::shared_ptr<DisplayVk::DisplayBufferInfo> db = nullptr;
-    if (m_displayVk) {
-        db = m_displayVk->createDisplayBuffer(image, imageCi);
-        if (!db) {
-            ERR("Fail to create display buffer for ColorBuffer %" PRIu64 ".",
-                static_cast<uint64_t>(colorBufferHandle));
-        }
-    }
     return cb->importMemory(handle, size, dedicated, imageCi.tiling == VK_IMAGE_TILING_LINEAR,
-                            vulkanOnly, std::move(db));
+                            vulkanOnly);
 }
 
 void FrameBuffer::setColorBufferInUse(
@@ -1490,14 +1496,9 @@
                                              genHandle_locked());
 }
 
-void FrameBuffer::createColorBufferWithHandle(
-     int p_width,
-     int p_height,
-     GLenum p_internalFormat,
-     FrameworkFormat p_frameworkFormat,
-     HandleType handle) {
-
-    HandleType resHandle;
+void FrameBuffer::createColorBufferWithHandle(int p_width, int p_height, GLenum p_internalFormat,
+                                              FrameworkFormat p_frameworkFormat,
+                                              HandleType handle) {
     {
         AutoLock mutex(m_lock);
         AutoLock colorBufferMapLock(m_colorBufferMapLock);
@@ -1510,12 +1511,14 @@
             GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER));
         }
 
-        resHandle = createColorBufferWithHandleLocked(
-            p_width, p_height, p_internalFormat, p_frameworkFormat,
-            handle);
+        handle = createColorBufferWithHandleLocked(p_width, p_height, p_internalFormat,
+                                                   p_frameworkFormat, handle);
+        if (!handle) {
+            return;
+        }
     }
 
-    if (m_displayVk && resHandle == handle) {
+    if (m_displayVk || m_guestUsesAngle) {
         goldfish_vk::setupVkColorBuffer(
             handle,
             false /* not vulkan only */,
@@ -3574,13 +3577,6 @@
     m_guestManagedColorBufferLifetime = guestManaged;
 }
 
-VkImageLayout FrameBuffer::getVkImageLayoutForComposeLayer() const {
-    if (m_displayVk) {
-        return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
-    }
-    return VK_IMAGE_LAYOUT_GENERAL;
-}
-
 bool FrameBuffer::platformImportResource(uint32_t handle, uint32_t info, void* resource) {
     if (!resource) {
         ERR("Error: resource was null");
@@ -3645,7 +3641,11 @@
 }
 
 std::unique_ptr<BorrowedImageInfo> FrameBuffer::borrowColorBufferForComposition(
-        uint32_t colorBufferHandle) {
+    uint32_t colorBufferHandle, bool colorBufferIsTarget) {
+    if (m_useVulkanComposition) {
+        return goldfish_vk::borrowColorBufferForComposition(colorBufferHandle, colorBufferIsTarget);
+    }
+
     ColorBufferPtr colorBufferPtr = findColorBuffer(colorBufferHandle);
     if (!colorBufferPtr) {
         ERR("Failed to get borrowed image info for ColorBuffer:%d", colorBufferHandle);
@@ -3656,6 +3656,10 @@
 
 std::unique_ptr<BorrowedImageInfo> FrameBuffer::borrowColorBufferForDisplay(
         uint32_t colorBufferHandle) {
+    if (m_useVulkanComposition) {
+        return goldfish_vk::borrowColorBufferForDisplay(colorBufferHandle);
+    }
+
     ColorBufferPtr colorBufferPtr = findColorBuffer(colorBufferHandle);
     if (!colorBufferPtr) {
         ERR("Failed to get borrowed image info for ColorBuffer:%d", colorBufferHandle);
diff --git a/stream-servers/FrameBuffer.h b/stream-servers/FrameBuffer.h
index a2be6e8..41b46b8 100644
--- a/stream-servers/FrameBuffer.h
+++ b/stream-servers/FrameBuffer.h
@@ -592,9 +592,8 @@
 
     void setGuestManagedColorBufferLifetime(bool guestManaged);
 
-    VkImageLayout getVkImageLayoutForComposeLayer() const;
-
-    std::unique_ptr<BorrowedImageInfo> borrowColorBufferForComposition(uint32_t colorBufferHandle);
+    std::unique_ptr<BorrowedImageInfo> borrowColorBufferForComposition(uint32_t colorBufferHandle,
+                                                                       bool colorBufferIsTarget);
     std::unique_ptr<BorrowedImageInfo> borrowColorBufferForDisplay(uint32_t colorBufferHandle);
 
    private:
@@ -796,6 +795,7 @@
     // FrameBuffer owns the CompositorGl if used as there is no GlEmulation
     // equivalent to VkEmulation,
     std::unique_ptr<CompositorGl> m_compositorGl;
+    bool m_useVulkanComposition = false;
 
     // The implementation for Vulkan native swapchain. Only initialized when useVulkan is set when
     // calling FrameBuffer::initialize(). DisplayVk is actually owned by VkEmulation.
diff --git a/stream-servers/GfxStreamBackend.cpp b/stream-servers/GfxStreamBackend.cpp
index cd0326f..50ef988 100644
--- a/stream-servers/GfxStreamBackend.cpp
+++ b/stream-servers/GfxStreamBackend.cpp
@@ -308,7 +308,6 @@
         }
     }
 
-
     // First we make some agents available.
 
 
diff --git a/stream-servers/PostWorker.cpp b/stream-servers/PostWorker.cpp
index 6eb1ff3..297ae5f 100644
--- a/stream-servers/PostWorker.cpp
+++ b/stream-servers/PostWorker.cpp
@@ -97,9 +97,8 @@
         if (shouldSkip) {
             return;
         }
-        goldfish_vk::acquireColorBuffersForHostComposing({}, cb->getHndl());
-        auto [success, waitForGpu] = m_displayVk->post(cb->getDisplayBufferVk());
-        goldfish_vk::releaseColorBufferFromHostComposing({cb->getHndl()});
+        const auto imageInfo = mFb->borrowColorBufferForDisplay(cb->getHndl());
+        auto [success, waitForGpu] = m_displayVk->post(imageInfo.get());
         if (success) {
             waitForGpu.wait();
         } else {
@@ -264,60 +263,15 @@
         }
     }
 
-    auto targetColorBufferPtr = mFb->findColorBuffer(composeRequest.targetHandle);
-    if (m_displayVk) {
-        auto targetColorBufferPtr = mFb->findColorBuffer(composeRequest.targetHandle);
-        if (!targetColorBufferPtr) {
-            GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER)) <<
-                                "Failed to retrieve the composition target buffer";
-        }
-        // We don't copy the render result to the targetHandle color buffer
-        // when using the Vulkan native host swapchain, because we directly
-        // render to the swapchain image instead of rendering onto a
-        // ColorBuffer, and we don't readback from the ColorBuffer so far.
-        std::vector<ColorBufferPtr> cbs;  // Keep ColorBuffers alive
-        cbs.emplace_back(targetColorBufferPtr);
-        std::vector<std::shared_ptr<DisplayVk::DisplayBufferInfo>>
-            composeBuffers;
-        std::vector<uint32_t> layerColorBufferHandles;
-        for (const ComposeLayer& layer : composeRequest.layers) {
-            auto colorBufferPtr = mFb->findColorBuffer(layer.cbHandle);
-            if (!colorBufferPtr) {
-                composeBuffers.push_back(nullptr);
-                continue;
-            }
-            auto db = colorBufferPtr->getDisplayBufferVk();
-            composeBuffers.push_back(db);
-            if (!db) {
-                continue;
-            }
-            cbs.push_back(colorBufferPtr);
-            layerColorBufferHandles.emplace_back(layer.cbHandle);
-        }
-        goldfish_vk::acquireColorBuffersForHostComposing(layerColorBufferHandles,
-                                                         composeRequest.targetHandle);
-        auto [success, waitForGpu] = m_displayVk->compose(
-            composeRequest.layers, composeBuffers, targetColorBufferPtr->getDisplayBufferVk());
-        goldfish_vk::setColorBufferCurrentLayout(composeRequest.targetHandle,
-                                                 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
-        std::vector<uint32_t> colorBufferHandles(layerColorBufferHandles.begin(),
-                                                 layerColorBufferHandles.end());
-        colorBufferHandles.emplace_back(composeRequest.targetHandle);
-        goldfish_vk::releaseColorBufferFromHostComposing(colorBufferHandles);
-        if (!success) {
-            m_needsToRebindWindow = true;
-            waitForGpu = completedFuture;
-        }
-        m_lastVkComposeColorBuffer = composeRequest.targetHandle;
-        return waitForGpu;
-    }
-
     Compositor::CompositionRequest compositorRequest = {};
-    compositorRequest.target = mFb->borrowColorBufferForComposition(composeRequest.targetHandle);
+    compositorRequest.target = mFb->borrowColorBufferForComposition(composeRequest.targetHandle,
+                                                                    /*colorBufferIsTarget=*/true);
     for (const ComposeLayer& guestLayer : composeRequest.layers) {
         auto& compositorLayer = compositorRequest.layers.emplace_back();
         compositorLayer.props = guestLayer;
-        compositorLayer.source = mFb->borrowColorBufferForComposition(guestLayer.cbHandle);
+        compositorLayer.source =
+            mFb->borrowColorBufferForComposition(guestLayer.cbHandle,
+                                                 /*colorBufferIsTarget=*/false);
     }
 
     return m_compositor->compose(compositorRequest);
diff --git a/stream-servers/tests/CompositorVk_unittest.cpp b/stream-servers/tests/CompositorVk_unittest.cpp
index 8f623f9..a4c62b9 100644
--- a/stream-servers/tests/CompositorVk_unittest.cpp
+++ b/stream-servers/tests/CompositorVk_unittest.cpp
@@ -4,91 +4,70 @@
 
 #include <algorithm>
 #include <array>
+#include <filesystem>
 #include <glm/gtx/matrix_transform_2d.hpp>
 #include <memory>
 #include <optional>
 
+#include "BorrowedImageVk.h"
 #include "base/Lock.h"
+#include "tests/ImageUtils.h"
 #include "tests/VkTestUtils.h"
 #include "vulkan/VulkanDispatch.h"
 #include "vulkan/vk_util.h"
 
+namespace {
+
+static constexpr const bool kDefaultSaveImageIfComparisonFailed = false;
+
+std::string GetTestDataPath(const std::string& basename) {
+    const std::filesystem::path currentPath = std::filesystem::current_path();
+    return currentPath / "tests" / "testdata" / basename;
+}
+
+static constexpr const uint32_t kColorBlack = 0xFF000000;
+static constexpr const uint32_t kColorRed = 0xFF0000FF;
+static constexpr const uint32_t kColorGreen = 0xFF00FF00;
+static constexpr const uint32_t kDefaultImageWidth = 256;
+static constexpr const uint32_t kDefaultImageHeight = 256;
+
 class CompositorVkTest : public ::testing::Test {
    protected:
-    using RenderTarget = emugl::RenderResourceVk<VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
-                                                 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT>;
-    using RenderTexture = emugl::RenderTextureVk;
+    using TargetImage = emugl::RenderResourceVk<VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+                                                VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT>;
+    using SourceImage = emugl::RenderTextureVk;
 
     static void SetUpTestCase() { k_vk = emugl::vkDispatch(false); }
 
-    static constexpr uint32_t k_numOfRenderTargets = 10;
-    static constexpr uint32_t k_renderTargetWidth = 255;
-    static constexpr uint32_t k_renderTargetHeight = 255;
-    static constexpr uint32_t k_renderTargetNumOfPixels =
-        k_renderTargetWidth * k_renderTargetHeight;
-
     void SetUp() override {
         ASSERT_NE(k_vk, nullptr);
         createInstance();
         pickPhysicalDevice();
         createLogicalDevice();
 
-        VkFormatProperties formatProperties;
-        k_vk->vkGetPhysicalDeviceFormatProperties(m_vkPhysicalDevice, RenderTarget::k_vkFormat,
-                                                  &formatProperties);
-        if (!(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
-            GTEST_SKIP();
+        if (!supportsFeatures(TargetImage::k_vkFormat, VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
+            GTEST_SKIP() << "Skipping test as format " << TargetImage::k_vkFormat
+                         << " does not support VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT";
         }
-        k_vk->vkGetPhysicalDeviceFormatProperties(m_vkPhysicalDevice, RenderTexture::k_vkFormat,
-                                                  &formatProperties);
-        if (!(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) {
-            GTEST_SKIP();
+        if (!supportsFeatures(SourceImage::k_vkFormat, VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) {
+            GTEST_SKIP() << "Skipping test as format " << SourceImage::k_vkFormat
+                         << " does not support VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT";
         }
 
-        VkCommandPoolCreateInfo commandPoolCi = {
+        const VkCommandPoolCreateInfo commandPoolCi = {
             .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
-            .queueFamilyIndex = m_compositorQueueFamilyIndex};
+            .queueFamilyIndex = m_compositorQueueFamilyIndex,
+        };
         ASSERT_EQ(k_vk->vkCreateCommandPool(m_vkDevice, &commandPoolCi, nullptr, &m_vkCommandPool),
                   VK_SUCCESS);
 
-        VkCommandBufferAllocateInfo cmdBuffAllocInfo = {
-            .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
-            .commandPool = m_vkCommandPool,
-            .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
-            .commandBufferCount = k_numOfRenderTargets};
-        m_vkCommandBuffers.resize(k_numOfRenderTargets);
-        VK_CHECK(k_vk->vkAllocateCommandBuffers(m_vkDevice, &cmdBuffAllocInfo,
-                                                m_vkCommandBuffers.data()));
-
         k_vk->vkGetDeviceQueue(m_vkDevice, m_compositorQueueFamilyIndex, 0, &m_compositorVkQueue);
-        ASSERT_TRUE(m_compositorVkQueue != VK_NULL_HANDLE);
+        ASSERT_NE(m_compositorVkQueue, VK_NULL_HANDLE);
 
         m_compositorVkQueueLock = std::make_shared<android::base::Lock>();
-
-        for (uint32_t i = 0; i < k_numOfRenderTargets; i++) {
-            auto renderTarget =
-                RenderTarget::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
-                                     m_vkCommandPool, k_renderTargetHeight, k_renderTargetWidth);
-            ASSERT_NE(renderTarget, nullptr);
-            m_renderTargets.emplace_back(std::move(renderTarget));
-        }
-
-        m_renderTargetImageViews.resize(m_renderTargets.size());
-        ASSERT_EQ(std::transform(m_renderTargets.begin(), m_renderTargets.end(),
-                                 m_renderTargetImageViews.begin(),
-                                 [](const std::unique_ptr<const RenderTarget> &renderTarget) {
-                                     return renderTarget->m_vkImageView;
-                                 }),
-                  m_renderTargetImageViews.end());
-        setUpRGBASampler();
     }
 
     void TearDown() override {
-        k_vk->vkDestroySampler(m_vkDevice, m_rgbaVkSampler, nullptr);
-        k_vk->vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, m_vkCommandBuffers.size(),
-                                   m_vkCommandBuffers.data());
-        m_vkCommandBuffers.clear();
-        m_renderTargets.clear();
         k_vk->vkDestroyCommandPool(m_vkDevice, m_vkCommandPool, nullptr);
         k_vk->vkDestroyDevice(m_vkDevice, nullptr);
         m_vkDevice = VK_NULL_HANDLE;
@@ -97,40 +76,176 @@
     }
 
     std::unique_ptr<CompositorVk> createCompositor() {
-        return CompositorVk::create(
-            *k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue, m_compositorVkQueueLock,
-            RenderTarget::k_vkFormat, RenderTarget::k_vkImageLayout, RenderTarget::k_vkImageLayout,
-            m_renderTargetImageViews.size(), m_vkCommandPool, m_rgbaVkSampler);
+        return CompositorVk::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
+                                    m_compositorVkQueueLock, m_compositorQueueFamilyIndex,
+                                    /*maxFramesInFlight=*/3);
     }
 
-    std::vector<std::unique_ptr<CompositorVkRenderTarget>> createCompositorRenderTargets(
-        CompositorVk &compositor) {
-        std::vector<std::unique_ptr<CompositorVkRenderTarget>> res;
-        for (VkImageView imageView : m_renderTargetImageViews) {
-            res.emplace_back(compositor.createRenderTarget(imageView, k_renderTargetWidth,
-                                                           k_renderTargetHeight));
+    template <typename SourceOrTargetImage>
+    std::unique_ptr<const SourceOrTargetImage> createImageWithColor(uint32_t sourceWidth,
+                                                                    uint32_t sourceHeight,
+                                                                    uint32_t sourceColor) {
+        auto source =
+            SourceOrTargetImage::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
+                                        m_vkCommandPool, sourceWidth, sourceHeight);
+        if (source == nullptr) {
+            return nullptr;
         }
-        return res;
+
+        std::vector<uint32_t> sourcePixels(sourceWidth * sourceHeight, sourceColor);
+        if (!source->write(sourcePixels)) {
+            return nullptr;
+        }
+
+        return source;
     }
 
-    void setUpRGBASampler() {
-        VkSamplerCreateInfo samplerCi = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
-                                         .magFilter = VK_FILTER_NEAREST,
-                                         .minFilter = VK_FILTER_NEAREST,
-                                         .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
-                                         .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
-                                         .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
-                                         .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
-                                         .mipLodBias = 0.0f,
-                                         .anisotropyEnable = VK_FALSE,
-                                         .maxAnisotropy = 1.0f,
-                                         .compareEnable = VK_FALSE,
-                                         .compareOp = VK_COMPARE_OP_ALWAYS,
-                                         .minLod = 0.0f,
-                                         .maxLod = 0.0f,
-                                         .borderColor = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK,
-                                         .unnormalizedCoordinates = VK_FALSE};
-        VK_CHECK(k_vk->vkCreateSampler(m_vkDevice, &samplerCi, nullptr, &m_rgbaVkSampler));
+    std::unique_ptr<const SourceImage> createSourceImageFromPng(const std::string& filename) {
+        uint32_t sourceWidth;
+        uint32_t sourceHeight;
+        std::vector<uint32_t> sourcePixels;
+        if (!LoadRGBAFromPng(filename, &sourceWidth, &sourceHeight, &sourcePixels)) {
+            return nullptr;
+        }
+
+        auto source =
+            SourceImage::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
+                                m_vkCommandPool, sourceWidth, sourceHeight);
+        if (source == nullptr) {
+            return nullptr;
+        }
+
+        if (!source->write(sourcePixels)) {
+            return nullptr;
+        }
+
+        return source;
+    }
+
+    bool isRGBAPixelNear(uint32_t actualPixel, uint32_t expectedPixel) {
+        const uint8_t* actualRGBA = reinterpret_cast<const uint8_t*>(&actualPixel);
+        const uint8_t* expectedRGBA = reinterpret_cast<const uint8_t*>(&expectedPixel);
+
+        constexpr const uint32_t kRGBA8888Tolerance = 1;
+        for (uint32_t channel = 0; channel < 4; channel++) {
+            const uint8_t actualChannel = actualRGBA[channel];
+            const uint8_t expectedChannel = expectedRGBA[channel];
+
+            if ((std::max(actualChannel, expectedChannel) -
+                 std::min(actualChannel, expectedChannel)) > kRGBA8888Tolerance) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    bool compareRGBAPixels(const uint32_t* actualPixels, const uint32_t* expectedPixels,
+                           const uint32_t width, const uint32_t height) {
+        bool comparisonFailed = false;
+
+        uint32_t reportedIncorrectPixels = 0;
+        constexpr const uint32_t kMaxReportedIncorrectPixels = 10;
+
+        for (uint32_t y = 0; y < width; y++) {
+            for (uint32_t x = 0; x < height; x++) {
+                const uint32_t actualPixel = actualPixels[y * height + x];
+                const uint32_t expectedPixel = expectedPixels[y * width + x];
+                if (!isRGBAPixelNear(actualPixel, expectedPixel)) {
+                    comparisonFailed = true;
+                    if (reportedIncorrectPixels < kMaxReportedIncorrectPixels) {
+                        reportedIncorrectPixels++;
+                        const uint8_t* actualRGBA = reinterpret_cast<const uint8_t*>(&actualPixel);
+                        const uint8_t* expectedRGBA =
+                            reinterpret_cast<const uint8_t*>(&expectedPixel);
+                        ADD_FAILURE()
+                            << "Pixel comparison failed at (" << x << ", " << y << ") "
+                            << " with actual "
+                            << " r:" << static_cast<int>(actualRGBA[0])
+                            << " g:" << static_cast<int>(actualRGBA[1])
+                            << " b:" << static_cast<int>(actualRGBA[2])
+                            << " a:" << static_cast<int>(actualRGBA[3]) << " but expected "
+                            << " r:" << static_cast<int>(expectedRGBA[0])
+                            << " g:" << static_cast<int>(expectedRGBA[1])
+                            << " b:" << static_cast<int>(expectedRGBA[2])
+                            << " a:" << static_cast<int>(expectedRGBA[3]);
+                    }
+                }
+            }
+        }
+        return comparisonFailed;
+    }
+
+    void compareImageWithGoldenPng(const TargetImage* target, const std::string& filename,
+                                   const bool saveImageIfComparisonFailed) {
+        const uint32_t targetWidth = target->m_width;
+        const uint32_t targetHeight = target->m_height;
+        const auto targetPixelsOpt = target->read();
+        ASSERT_TRUE(targetPixelsOpt.has_value());
+        const auto& targetPixels = *targetPixelsOpt;
+
+        uint32_t goldenWidth;
+        uint32_t goldenHeight;
+        std::vector<uint32_t> goldenPixels;
+        const bool loadedGolden =
+            LoadRGBAFromPng(filename, &goldenWidth, &goldenHeight, &goldenPixels);
+        EXPECT_TRUE(loadedGolden) << "Failed to load golden image from " << filename;
+
+        bool comparisonFailed = !loadedGolden;
+
+        if (loadedGolden) {
+            EXPECT_EQ(target->m_width, goldenWidth)
+                << "Invalid width comparison with golden image from " << filename;
+            EXPECT_EQ(target->m_height, goldenHeight)
+                << "Invalid height comparison with golden image from " << filename;
+            if (targetWidth != goldenWidth || targetHeight != goldenHeight) {
+                comparisonFailed = true;
+            }
+            if (!comparisonFailed) {
+                comparisonFailed = compareRGBAPixels(targetPixels.data(), goldenPixels.data(),
+                                                     goldenWidth, goldenHeight);
+            }
+        }
+
+        if (saveImageIfComparisonFailed && comparisonFailed) {
+            const std::string output =
+                std::filesystem::temp_directory_path() / std::filesystem::path(filename).filename();
+            SaveRGBAToPng(targetWidth, targetHeight, targetPixels.data(), output);
+            ADD_FAILURE() << "Saved composition result to " << output;
+        }
+    }
+
+    template <typename SourceOrTargetImage>
+    std::unique_ptr<BorrowedImageInfoVk> createBorrowedImageInfo(const SourceOrTargetImage* image) {
+        static int sImageId = 0;
+
+        auto ret = std::make_unique<BorrowedImageInfoVk>();
+        ret->id = sImageId++;
+        ret->width = image->m_width;
+        ret->height = image->m_height;
+        ret->image = image->m_vkImage;
+        ret->imageCreateInfo = image->m_vkImageCreateInfo;
+        ret->imageView = image->m_vkImageView;
+        ret->preBorrowLayout = SourceOrTargetImage::k_vkImageLayout;
+        ret->preBorrowQueueFamilyIndex = m_compositorQueueFamilyIndex;
+        ret->postBorrowLayout = SourceOrTargetImage::k_vkImageLayout;
+        ret->postBorrowQueueFamilyIndex = m_compositorQueueFamilyIndex;
+        return ret;
+    }
+
+    void checkImageFilledWith(const TargetImage* image, uint32_t expectedColor) {
+        auto actualPixelsOpt = image->read();
+        ASSERT_TRUE(actualPixelsOpt.has_value());
+        auto& actualPixels = *actualPixelsOpt;
+
+        const std::vector<uint32_t> expectedPixels(image->numOfPixels(), expectedColor);
+        compareRGBAPixels(actualPixels.data(), expectedPixels.data(), image->m_width,
+                          image->m_height);
+    }
+
+    void fillImageWith(const TargetImage* image, uint32_t color) {
+        const std::vector<uint32_t> pixels(image->numOfPixels(), color);
+        ASSERT_TRUE(image->write(pixels)) << "Failed to fill image with color:" << color;
+        checkImageFilledWith(image, color);
     }
 
     static const goldfish_vk::VulkanDispatch *k_vk;
@@ -138,29 +253,29 @@
     VkPhysicalDevice m_vkPhysicalDevice = VK_NULL_HANDLE;
     uint32_t m_compositorQueueFamilyIndex = 0;
     VkDevice m_vkDevice = VK_NULL_HANDLE;
-    std::vector<std::unique_ptr<const RenderTarget>> m_renderTargets;
-    std::vector<VkImageView> m_renderTargetImageViews;
     VkCommandPool m_vkCommandPool = VK_NULL_HANDLE;
     VkQueue m_compositorVkQueue = VK_NULL_HANDLE;
     std::shared_ptr<android::base::Lock> m_compositorVkQueueLock;
-    std::vector<VkCommandBuffer> m_vkCommandBuffers;
-    VkSampler m_rgbaVkSampler = VK_NULL_HANDLE;
 
    private:
     void createInstance() {
-        VkApplicationInfo appInfo = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
-                                     .pNext = nullptr,
-                                     .pApplicationName = "emulator CompositorVk unittest",
-                                     .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
-                                     .pEngineName = "No Engine",
-                                     .engineVersion = VK_MAKE_VERSION(1, 0, 0),
-                                     .apiVersion = VK_API_VERSION_1_1};
-        VkInstanceCreateInfo instanceCi = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
-                                           .pApplicationInfo = &appInfo,
-                                           .enabledExtensionCount = 0,
-                                           .ppEnabledExtensionNames = nullptr};
+        const VkApplicationInfo appInfo = {
+            .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+            .pNext = nullptr,
+            .pApplicationName = "emulator CompositorVk unittest",
+            .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
+            .pEngineName = "No Engine",
+            .engineVersion = VK_MAKE_VERSION(1, 0, 0),
+            .apiVersion = VK_API_VERSION_1_1,
+        };
+        const VkInstanceCreateInfo instanceCi = {
+            .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+            .pApplicationInfo = &appInfo,
+            .enabledExtensionCount = 0,
+            .ppEnabledExtensionNames = nullptr,
+        };
         ASSERT_EQ(k_vk->vkCreateInstance(&instanceCi, nullptr, &m_vkInstance), VK_SUCCESS);
-        ASSERT_TRUE(m_vkInstance != VK_NULL_HANDLE);
+        ASSERT_NE(m_vkInstance, VK_NULL_HANDLE);
     }
 
     void pickPhysicalDevice() {
@@ -181,7 +296,7 @@
                                                            queueFamilyProperties.data());
             uint32_t queueFamilyIndex = 0;
             for (; queueFamilyIndex < queueFamilyCount; queueFamilyIndex++) {
-                if (CompositorVk::validateQueueFamilyProperties(
+                if (CompositorVk::queueSupportsComposition(
                         queueFamilyProperties[queueFamilyIndex])) {
                     break;
                 }
@@ -199,291 +314,499 @@
 
     void createLogicalDevice() {
         const float queuePriority = 1.0f;
-        VkDeviceQueueCreateInfo queueCi = {.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
-                                           .queueFamilyIndex = m_compositorQueueFamilyIndex,
-                                           .queueCount = 1,
-                                           .pQueuePriorities = &queuePriority};
-        VkPhysicalDeviceFeatures2 features = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
-                                              .pNext = nullptr};
-        VkDeviceCreateInfo deviceCi = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
-                                       .pNext = &features,
-                                       .queueCreateInfoCount = 1,
-                                       .pQueueCreateInfos = &queueCi,
-                                       .enabledLayerCount = 0,
-                                       .enabledExtensionCount = 0,
-                                       .ppEnabledExtensionNames = nullptr};
+        const VkDeviceQueueCreateInfo queueCi = {
+            .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
+            .queueFamilyIndex = m_compositorQueueFamilyIndex,
+            .queueCount = 1,
+            .pQueuePriorities = &queuePriority,
+        };
+        const VkPhysicalDeviceFeatures2 features = {
+            .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
+            .pNext = nullptr,
+        };
+        const VkDeviceCreateInfo deviceCi = {
+            .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
+            .pNext = &features,
+            .queueCreateInfoCount = 1,
+            .pQueueCreateInfos = &queueCi,
+            .enabledLayerCount = 0,
+            .enabledExtensionCount = 0,
+            .ppEnabledExtensionNames = nullptr,
+        };
         ASSERT_EQ(k_vk->vkCreateDevice(m_vkPhysicalDevice, &deviceCi, nullptr, &m_vkDevice),
                   VK_SUCCESS);
-        ASSERT_TRUE(m_vkDevice != VK_NULL_HANDLE);
+        ASSERT_NE(m_vkDevice, VK_NULL_HANDLE);
+    }
+
+    bool supportsFeatures(VkFormat format, VkFormatFeatureFlags features) {
+        VkFormatProperties formatProperties = {};
+        k_vk->vkGetPhysicalDeviceFormatProperties(m_vkPhysicalDevice, format, &formatProperties);
+        return (formatProperties.optimalTilingFeatures & features) == features;
     }
 };
 
 const goldfish_vk::VulkanDispatch *CompositorVkTest::k_vk = nullptr;
 
-TEST_F(CompositorVkTest, Init) { ASSERT_NE(createCompositor(), nullptr); }
-
-TEST_F(CompositorVkTest, ValidateQueueFamilyProperties) {
+TEST_F(CompositorVkTest, QueueSupportsComposition) {
     VkQueueFamilyProperties properties = {};
+
     properties.queueFlags &= ~VK_QUEUE_GRAPHICS_BIT;
-    ASSERT_FALSE(CompositorVk::validateQueueFamilyProperties(properties));
+    ASSERT_FALSE(CompositorVk::queueSupportsComposition(properties));
+
     properties.queueFlags |= VK_QUEUE_GRAPHICS_BIT;
-    ASSERT_TRUE(CompositorVk::validateQueueFamilyProperties(properties));
+    ASSERT_TRUE(CompositorVk::queueSupportsComposition(properties));
 }
 
-TEST_F(CompositorVkTest, EmptyCompositionShouldDrawABlackFrame) {
-    std::vector<uint32_t> pixels(k_renderTargetNumOfPixels);
-    for (uint32_t i = 0; i < k_renderTargetNumOfPixels; i++) {
-        uint8_t v = static_cast<uint8_t>((i / 4) & 0xff);
-        uint8_t *pixel = reinterpret_cast<uint8_t *>(&pixels[i]);
-        pixel[0] = v;
-        pixel[1] = v;
-        pixel[2] = v;
-        pixel[3] = 0xff;
-    }
-    for (uint32_t i = 0; i < k_numOfRenderTargets; i++) {
-        ASSERT_TRUE(m_renderTargets[i]->write(pixels));
-        auto maybeImageBytes = m_renderTargets[i]->read();
-        ASSERT_TRUE(maybeImageBytes.has_value());
-        for (uint32_t i = 0; i < k_renderTargetNumOfPixels; i++) {
-            ASSERT_EQ(pixels[i], maybeImageBytes.value()[i]);
-        }
-    }
+TEST_F(CompositorVkTest, Init) { ASSERT_NE(createCompositor(), nullptr); }
 
+TEST_F(CompositorVkTest, EmptyCompositionShouldDrawABlackFrame) {
     auto compositor = createCompositor();
-    auto renderTargets = createCompositorRenderTargets(*compositor);
     ASSERT_NE(compositor, nullptr);
 
-    // render to render targets with event index
-    std::vector<VkCommandBuffer> cmdBuffs = {};
-    for (uint32_t i = 0; i < k_numOfRenderTargets; i++) {
-        VkCommandBufferBeginInfo beginInfo = {
-            .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
-            .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
-        };
-        VK_CHECK(k_vk->vkBeginCommandBuffer(m_vkCommandBuffers[i], &beginInfo));
-        compositor->recordCommandBuffers(i, m_vkCommandBuffers[i], *renderTargets[i]);
-        VK_CHECK(k_vk->vkEndCommandBuffer(m_vkCommandBuffers[i]));
-        if (i % 2 == 0) {
-            cmdBuffs.emplace_back(m_vkCommandBuffers[i]);
-        }
+    std::vector<std::unique_ptr<const TargetImage>> targets;
+    constexpr const uint32_t kNumImages = 10;
+    for (uint32_t i = 0; i < kNumImages; i++) {
+        auto target =
+            TargetImage::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
+                                m_vkCommandPool, kDefaultImageWidth, kDefaultImageHeight);
+        ASSERT_NE(target, nullptr);
+        fillImageWith(target.get(), kColorRed);
+        targets.emplace_back(std::move(target));
     }
-    VkSubmitInfo submitInfo = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
-                               .commandBufferCount = static_cast<uint32_t>(cmdBuffs.size()),
-                               .pCommandBuffers = cmdBuffs.data()};
-    ASSERT_EQ(k_vk->vkQueueSubmit(m_compositorVkQueue, 1, &submitInfo, VK_NULL_HANDLE), VK_SUCCESS);
 
-    ASSERT_EQ(k_vk->vkQueueWaitIdle(m_compositorVkQueue), VK_SUCCESS);
-    for (uint32_t i = 0; i < k_numOfRenderTargets; i++) {
-        auto maybeImagePixels = m_renderTargets[i]->read();
-        ASSERT_TRUE(maybeImagePixels.has_value());
-        const auto &imagePixels = maybeImagePixels.value();
-        for (uint32_t j = 0; j < k_renderTargetNumOfPixels; j++) {
-            const auto pixel = reinterpret_cast<const uint8_t *>(&imagePixels[j]);
-            // should only render to render targets with even index
-            if (i % 2 == 0) {
-                ASSERT_EQ(pixel[0], 0);
-                ASSERT_EQ(pixel[1], 0);
-                ASSERT_EQ(pixel[2], 0);
-                ASSERT_EQ(pixel[3], 0xff);
-            } else {
-                ASSERT_EQ(pixels[j], imagePixels[j]);
-            }
-        }
+    for (uint32_t i = 0; i < kNumImages; i++) {
+        const Compositor::CompositionRequest compositionRequest = {
+            .target = createBorrowedImageInfo(targets[i].get()),
+            .layers = {},  // Note: this is empty!
+        };
+
+        auto compositionCompleteWaitable = compositor->compose(compositionRequest);
+        compositionCompleteWaitable.wait();
+    }
+
+    for (const auto& target : targets) {
+        checkImageFilledWith(target.get(), kColorBlack);
     }
 }
 
 TEST_F(CompositorVkTest, SimpleComposition) {
-    constexpr uint32_t textureLeft = 30;
-    constexpr uint32_t textureRight = 50;
-    constexpr uint32_t textureTop = 10;
-    constexpr uint32_t textureBottom = 40;
-    constexpr uint32_t textureWidth = textureRight - textureLeft;
-    constexpr uint32_t textureHeight = textureBottom - textureTop;
-    auto texture = RenderTexture::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
-                                         m_vkCommandPool, textureWidth, textureHeight);
-    uint32_t textureColor;
-    uint8_t *textureColor_ = reinterpret_cast<uint8_t *>(&textureColor);
-    textureColor_[0] = 0xff;
-    textureColor_[1] = 0;
-    textureColor_[2] = 0;
-    textureColor_[3] = 0xff;
-    std::vector<uint32_t> pixels(textureWidth * textureHeight, textureColor);
-    ASSERT_TRUE(texture->write(pixels));
     auto compositor = createCompositor();
     ASSERT_NE(compositor, nullptr);
 
-    ComposeLayer composeLayer = {
-        0 /* No color buffer handle */,
-        HWC2_COMPOSITION_DEVICE,
-        {
-            /* frame in which the texture shows up on the display */
-            textureLeft,
-            textureTop,
-            textureRight,
-            textureBottom,
-        },
-        {
-            /* how much of the texture itself to show */
-            0.0,
-            0.0,
-            static_cast<float>(textureWidth),
-            static_cast<float>(textureHeight),
-        },
-        HWC2_BLEND_MODE_PREMULTIPLIED /* blend mode */,
-        1.0 /* alpha */,
-        {0, 0, 0, 0} /* color (N/A) */,
-        HWC_TRANSFORM_NONE /* transform (no rotation */,
+    auto source = createSourceImageFromPng(GetTestDataPath("256x256_android.png"));
+    ASSERT_NE(source, nullptr);
+
+    auto target = TargetImage::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
+                                      m_vkCommandPool, source->m_width, source->m_height);
+    ASSERT_NE(target, nullptr);
+    fillImageWith(target.get(), kColorBlack);
+
+    Compositor::CompositionRequest compositionRequest = {
+        .target = createBorrowedImageInfo(target.get()),
     };
+    compositionRequest.layers.emplace_back(Compositor::CompositionRequestLayer{
+        .source = createBorrowedImageInfo(source.get()),
+        .props =
+            {
+                .composeMode = HWC2_COMPOSITION_DEVICE,
+                .displayFrame =
+                    {
+                        .left = 64,
+                        .top = 32,
+                        .right = 128,
+                        .bottom = 160,
+                    },
+                .crop =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<float>(source->m_width),
+                        .bottom = static_cast<float>(source->m_height),
+                    },
+                .blendMode = HWC2_BLEND_MODE_PREMULTIPLIED,
+                .alpha = 1.0,
+                .color =
+                    {
+                        .r = 0,
+                        .g = 0,
+                        .b = 0,
+                        .a = 0,
+                    },
+                .transform = HWC_TRANSFORM_NONE,
+            },
+    });
 
-    std::unique_ptr<ComposeLayerVk> composeLayerVkPtr = ComposeLayerVk::createFromHwc2ComposeLayer(
-        m_rgbaVkSampler, texture->m_vkImageView, composeLayer, textureWidth, textureHeight,
-        k_renderTargetWidth, k_renderTargetHeight);
+    auto compositionCompleteWaitable = compositor->compose(compositionRequest);
+    compositionCompleteWaitable.wait();
 
-    std::vector<std::unique_ptr<ComposeLayerVk>> layers;
-    layers.emplace_back(std::move(composeLayerVkPtr));
-
-    auto composition = std::make_unique<Composition>(std::move(layers));
-    auto renderTargets = createCompositorRenderTargets(*compositor);
-    compositor->setComposition(0, std::move(composition));
-
-    VkCommandBuffer cmdBuff = m_vkCommandBuffers[0];
-    VkCommandBufferBeginInfo beginInfo = {
-        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
-        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
-    };
-    VK_CHECK(k_vk->vkBeginCommandBuffer(cmdBuff, &beginInfo));
-    compositor->recordCommandBuffers(0, cmdBuff, *renderTargets[0]);
-    VK_CHECK(k_vk->vkEndCommandBuffer(cmdBuff));
-
-    VkSubmitInfo submitInfo = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
-                               .commandBufferCount = 1,
-                               .pCommandBuffers = &cmdBuff};
-    ASSERT_EQ(k_vk->vkQueueSubmit(m_compositorVkQueue, 1, &submitInfo, VK_NULL_HANDLE), VK_SUCCESS);
-    ASSERT_EQ(k_vk->vkQueueWaitIdle(m_compositorVkQueue), VK_SUCCESS);
-
-    auto maybeImagePixels = m_renderTargets[0]->read();
-    ASSERT_TRUE(maybeImagePixels.has_value());
-    const auto &imagePixels = maybeImagePixels.value();
-
-    for (uint32_t i = 0; i < k_renderTargetHeight; i++) {
-        for (uint32_t j = 0; j < k_renderTargetWidth; j++) {
-            uint32_t offset = i * k_renderTargetWidth + j;
-            const uint8_t *pixel = reinterpret_cast<const uint8_t *>(&imagePixels[offset]);
-            EXPECT_EQ(pixel[1], 0);
-            EXPECT_EQ(pixel[2], 0);
-            EXPECT_EQ(pixel[3], 0xff);
-            if (i >= textureTop && i < textureBottom && j >= textureLeft && j < textureRight) {
-                EXPECT_EQ(pixel[0], 0xff);
-            } else {
-                EXPECT_EQ(pixel[0], 0);
-            }
-        }
-    }
+    compareImageWithGoldenPng(target.get(),
+                              GetTestDataPath("256x256_golden_simple_composition.png"),
+                              kDefaultSaveImageIfComparisonFailed);
 }
 
-TEST_F(CompositorVkTest, CompositingWithDifferentCompositionOnMultipleTargets) {
-    constexpr uint32_t textureWidth = 20;
-    constexpr uint32_t textureHeight = 30;
-
-    ComposeLayer defaultComposeLayer = {
-        0 /* No color buffer handle */,
-        HWC2_COMPOSITION_DEVICE,
-        {
-            /* display frame (to be replaced for each of k_numOfRenderTargets */
-            0,
-            0,
-            0,
-            0,
-        },
-        {
-            /* how much of the texture itself to show */
-            0.0,
-            0.0,
-            static_cast<float>(textureWidth),
-            static_cast<float>(textureHeight),
-        },
-        HWC2_BLEND_MODE_PREMULTIPLIED /* blend mode */,
-        1.0 /* alpha */,
-        {0, 0, 0, 0} /* color (N/A) */,
-        HWC_TRANSFORM_NONE /* transform (no rotation */,
-    };
-
-    std::vector<ComposeLayer> composeLayers(k_numOfRenderTargets);
-    for (int i = 0; i < k_numOfRenderTargets; i++) {
-        composeLayers[i] = defaultComposeLayer;
-        int left = (i * 30) % (k_renderTargetWidth - textureWidth);
-        int top = (i * 20) % (k_renderTargetHeight - textureHeight);
-        int right = left + textureWidth;
-        int bottom = top + textureHeight;
-        composeLayers[i].displayFrame = {
-            left,
-            top,
-            right,
-            bottom,
-        };
-    }
-
-    auto texture = RenderTexture::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
-                                         m_vkCommandPool, textureWidth, textureHeight);
-    uint32_t textureColor;
-    uint8_t *textureColor_ = reinterpret_cast<uint8_t *>(&textureColor);
-    textureColor_[0] = 0xff;
-    textureColor_[1] = 0;
-    textureColor_[2] = 0;
-    textureColor_[3] = 0xff;
-    std::vector<uint32_t> pixels(textureWidth * textureHeight, textureColor);
-    ASSERT_TRUE(texture->write(pixels));
+TEST_F(CompositorVkTest, BlendPremultiplied) {
     auto compositor = createCompositor();
     ASSERT_NE(compositor, nullptr);
-    auto renderTargets = createCompositorRenderTargets(*compositor);
-    for (int i = 0; i < k_numOfRenderTargets; i++) {
-        std::unique_ptr<ComposeLayerVk> composeLayerVkPtr =
-            ComposeLayerVk::createFromHwc2ComposeLayer(
-                m_rgbaVkSampler, texture->m_vkImageView, composeLayers[i], textureWidth,
-                textureHeight, k_renderTargetWidth, k_renderTargetHeight);
 
-        std::vector<std::unique_ptr<ComposeLayerVk>> layers;
-        layers.emplace_back(std::move(composeLayerVkPtr));
+    auto source =
+        createSourceImageFromPng(GetTestDataPath("256x256_android_with_transparency.png"));
+    ASSERT_NE(source, nullptr);
 
-        auto composition = std::make_unique<Composition>(std::move(layers));
-        compositor->setComposition(i, std::move(composition));
+    auto target = TargetImage::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
+                                      m_vkCommandPool, source->m_width, source->m_height);
+    ASSERT_NE(target, nullptr);
+    fillImageWith(target.get(), kColorBlack);
 
-        VkCommandBuffer cmdBuff = m_vkCommandBuffers[i];
-        VkCommandBufferBeginInfo beginInfo = {
-            .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
-            .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
-        };
-        VK_CHECK(k_vk->vkBeginCommandBuffer(cmdBuff, &beginInfo));
-        compositor->recordCommandBuffers(i, cmdBuff, *renderTargets[i]);
-        VK_CHECK(k_vk->vkEndCommandBuffer(cmdBuff));
+    Compositor::CompositionRequest compositionRequest = {
+        .target = createBorrowedImageInfo(target.get()),
+    };
+    compositionRequest.layers.emplace_back(Compositor::CompositionRequestLayer{
+        .source = createBorrowedImageInfo(source.get()),
+        .props =
+            {
+                .composeMode = HWC2_COMPOSITION_DEVICE,
+                .displayFrame =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<int>(target->m_width),
+                        .bottom = static_cast<int>(target->m_height),
+                    },
+                .crop =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<float>(source->m_width),
+                        .bottom = static_cast<float>(source->m_height),
+                    },
+                .blendMode = HWC2_BLEND_MODE_PREMULTIPLIED,
+                .alpha = 1.0,
+                .color =
+                    {
+                        .r = 0,
+                        .g = 0,
+                        .b = 0,
+                        .a = 0,
+                    },
+                .transform = HWC_TRANSFORM_NONE,
+            },
+    });
 
-        VkSubmitInfo submitInfo = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
-                                   .commandBufferCount = 1,
-                                   .pCommandBuffers = &cmdBuff};
-        ASSERT_EQ(k_vk->vkQueueSubmit(m_compositorVkQueue, 1, &submitInfo, VK_NULL_HANDLE),
-                  VK_SUCCESS);
-        ASSERT_EQ(k_vk->vkQueueWaitIdle(m_compositorVkQueue), VK_SUCCESS);
+    auto compositionCompleteWaitable = compositor->compose(compositionRequest);
+    compositionCompleteWaitable.wait();
 
-        auto maybeImagePixels = m_renderTargets[i]->read();
-        ASSERT_TRUE(maybeImagePixels.has_value());
-        const auto &imagePixels = maybeImagePixels.value();
+    compareImageWithGoldenPng(target.get(),
+                              GetTestDataPath("256x256_golden_blend_premultiplied.png"),
+                              kDefaultSaveImageIfComparisonFailed);
+}
 
-        for (uint32_t j = 0; j < k_renderTargetHeight; j++) {
-            for (uint32_t k = 0; k < k_renderTargetWidth; k++) {
-                uint32_t offset = j * k_renderTargetWidth + k;
-                const uint8_t *pixel = reinterpret_cast<const uint8_t *>(&imagePixels[offset]);
-                EXPECT_EQ(pixel[1], 0);
-                EXPECT_EQ(pixel[2], 0);
-                EXPECT_EQ(pixel[3], 0xff);
-                if (j >= composeLayers[i].displayFrame.top &&
-                    j < composeLayers[i].displayFrame.bottom &&
-                    k >= composeLayers[i].displayFrame.left &&
-                    k < composeLayers[i].displayFrame.right) {
-                    EXPECT_EQ(pixel[0], 0xff);
-                } else {
-                    EXPECT_EQ(pixel[0], 0);
-                }
-            }
-        }
+TEST_F(CompositorVkTest, Crop) {
+    auto compositor = createCompositor();
+    ASSERT_NE(compositor, nullptr);
+
+    auto source = createSourceImageFromPng(GetTestDataPath("256x256_android.png"));
+    ASSERT_NE(source, nullptr);
+
+    auto target = TargetImage::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
+                                      m_vkCommandPool, source->m_width, source->m_height);
+    ASSERT_NE(target, nullptr);
+    fillImageWith(target.get(), kColorBlack);
+
+    Compositor::CompositionRequest compositionRequest = {
+        .target = createBorrowedImageInfo(target.get()),
+    };
+    compositionRequest.layers.emplace_back(Compositor::CompositionRequestLayer{
+        .source = createBorrowedImageInfo(source.get()),
+        .props =
+            {
+                .composeMode = HWC2_COMPOSITION_DEVICE,
+                .displayFrame =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<int>(target->m_width),
+                        .bottom = static_cast<int>(target->m_height),
+                    },
+                .crop =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<float>(source->m_width) / 2.0f,
+                        .bottom = static_cast<float>(source->m_height) / 2.0f,
+                    },
+                .blendMode = HWC2_BLEND_MODE_PREMULTIPLIED,
+                .alpha = 1.0,
+                .color =
+                    {
+                        .r = 0,
+                        .g = 0,
+                        .b = 0,
+                        .a = 0,
+                    },
+                .transform = HWC_TRANSFORM_NONE,
+            },
+    });
+
+    auto compositionCompleteWaitable = compositor->compose(compositionRequest);
+    compositionCompleteWaitable.wait();
+
+    compareImageWithGoldenPng(target.get(), GetTestDataPath("256x256_golden_crop.png"),
+                              kDefaultSaveImageIfComparisonFailed);
+}
+
+TEST_F(CompositorVkTest, Transformations) {
+    auto compositor = createCompositor();
+    ASSERT_NE(compositor, nullptr);
+
+    auto source = createSourceImageFromPng(GetTestDataPath("256x256_android.png"));
+    ASSERT_NE(source, nullptr);
+
+    Compositor::CompositionRequest compositionRequest;
+    compositionRequest.layers.emplace_back(Compositor::CompositionRequestLayer{
+        .source = createBorrowedImageInfo(source.get()),
+        .props =
+            {
+                .composeMode = HWC2_COMPOSITION_DEVICE,
+                .displayFrame =
+                    {
+                        .left = 32,
+                        .top = 32,
+                        .right = 224,
+                        .bottom = 224,
+                    },
+                .crop =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<float>(source->m_width),
+                        .bottom = static_cast<float>(source->m_height),
+                    },
+                .blendMode = HWC2_BLEND_MODE_PREMULTIPLIED,
+                .alpha = 1.0,
+                .color =
+                    {
+                        .r = 0,
+                        .g = 0,
+                        .b = 0,
+                        .a = 0,
+                    },
+                .transform = HWC_TRANSFORM_NONE,
+            },
+    });
+
+    const std::unordered_map<hwc_transform_t, std::string> transformToGolden = {
+        {HWC_TRANSFORM_NONE, "256x256_golden_transform_none.png"},
+        {HWC_TRANSFORM_FLIP_H, "256x256_golden_transform_fliph.png"},
+        {HWC_TRANSFORM_FLIP_V, "256x256_golden_transform_flipv.png"},
+        {HWC_TRANSFORM_ROT_90, "256x256_golden_transform_rot90.png"},
+        {HWC_TRANSFORM_ROT_180, "256x256_golden_transform_rot180.png"},
+        {HWC_TRANSFORM_ROT_270, "256x256_golden_transform_rot270.png"},
+        {HWC_TRANSFORM_FLIP_H_ROT_90, "256x256_golden_transform_fliphrot90.png"},
+        {HWC_TRANSFORM_FLIP_V_ROT_90, "256x256_golden_transform_flipvrot90.png"},
+    };
+
+    for (const auto [transform, golden] : transformToGolden) {
+        auto target = TargetImage::create(*k_vk, m_vkDevice, m_vkPhysicalDevice,
+                                          m_compositorVkQueue, m_vkCommandPool, 256, 256);
+        ASSERT_NE(target, nullptr);
+        fillImageWith(target.get(), kColorBlack);
+
+        compositionRequest.target = createBorrowedImageInfo(target.get());
+        compositionRequest.layers[0].props.transform = transform;
+
+        auto compositionCompleteWaitable = compositor->compose(compositionRequest);
+        compositionCompleteWaitable.wait();
+
+        compareImageWithGoldenPng(target.get(), GetTestDataPath(golden),
+                                  kDefaultSaveImageIfComparisonFailed);
     }
 }
+
+TEST_F(CompositorVkTest, MultipleTargetsComposition) {
+    auto compositor = createCompositor();
+    ASSERT_NE(compositor, nullptr);
+
+    constexpr const uint32_t kNumCompostions = 10;
+
+    auto source = createImageWithColor<SourceImage>(256, 256, kColorGreen);
+    ASSERT_NE(source, nullptr);
+
+    std::vector<std::unique_ptr<const TargetImage>> targets;
+    for (uint32_t i = 0; i < kNumCompostions; i++) {
+        auto target = createImageWithColor<TargetImage>(256, 256, kColorBlack);
+        ASSERT_NE(target, nullptr);
+        targets.emplace_back(std::move(target));
+    }
+
+    Compositor::CompositionRequest compositionRequest = {};
+    compositionRequest.layers.emplace_back(Compositor::CompositionRequestLayer{
+        .source = createBorrowedImageInfo(source.get()),
+        .props =
+            {
+                .composeMode = HWC2_COMPOSITION_DEVICE,
+                .displayFrame =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = 0,
+                        .bottom = static_cast<int>(source->m_height),
+                    },
+                .crop =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<float>(source->m_width),
+                        .bottom = static_cast<float>(source->m_height),
+                    },
+                .blendMode = HWC2_BLEND_MODE_PREMULTIPLIED,
+                .alpha = 1.0,
+                .color =
+                    {
+                        .r = 0,
+                        .g = 0,
+                        .b = 0,
+                        .a = 0,
+                    },
+                .transform = HWC_TRANSFORM_NONE,
+            },
+    });
+
+    const uint32_t displayFrameWidth = 256 / kNumCompostions;
+    for (uint32_t i = 0; i < kNumCompostions; i++) {
+        const auto& target = targets[i];
+
+        compositionRequest.target = createBorrowedImageInfo(target.get());
+        compositionRequest.layers[0].props.displayFrame.left = (i + 0) * displayFrameWidth;
+        compositionRequest.layers[0].props.displayFrame.right = (i + 1) * displayFrameWidth;
+
+        auto compositionCompleteWaitable = compositor->compose(compositionRequest);
+        compositionCompleteWaitable.wait();
+
+        compareImageWithGoldenPng(
+            target.get(),
+            GetTestDataPath("256x256_golden_multiple_targets_" + std::to_string(i) + ".png"),
+            kDefaultSaveImageIfComparisonFailed);
+    }
+}
+
+TEST_F(CompositorVkTest, MultipleLayers) {
+    auto compositor = createCompositor();
+    ASSERT_NE(compositor, nullptr);
+
+    auto source1 = createSourceImageFromPng(GetTestDataPath("256x256_android.png"));
+    ASSERT_NE(source1, nullptr);
+
+    auto source2 =
+        createSourceImageFromPng(GetTestDataPath("256x256_android_with_transparency.png"));
+    ASSERT_NE(source2, nullptr);
+
+    auto target = TargetImage::create(*k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
+                                      m_vkCommandPool, kDefaultImageWidth, kDefaultImageHeight);
+    ASSERT_NE(target, nullptr);
+    fillImageWith(target.get(), kColorBlack);
+
+    Compositor::CompositionRequest compositionRequest = {
+        .target = createBorrowedImageInfo(target.get()),
+    };
+    compositionRequest.layers.emplace_back(Compositor::CompositionRequestLayer{
+        .source = createBorrowedImageInfo(source1.get()),
+        .props =
+            {
+                .composeMode = HWC2_COMPOSITION_DEVICE,
+                .displayFrame =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<int>(target->m_width),
+                        .bottom = static_cast<int>(target->m_height),
+                    },
+                .crop =
+                    {
+                        .left = 32.0,
+                        .top = 32.0,
+                        .right = static_cast<float>(source1->m_width) - 32.0f,
+                        .bottom = static_cast<float>(source1->m_height) - 32.0f,
+                    },
+                .blendMode = HWC2_BLEND_MODE_PREMULTIPLIED,
+                .alpha = 1.0,
+                .color =
+                    {
+                        .r = 0,
+                        .g = 0,
+                        .b = 0,
+                        .a = 0,
+                    },
+                .transform = HWC_TRANSFORM_NONE,
+            },
+    });
+    compositionRequest.layers.emplace_back(Compositor::CompositionRequestLayer{
+        .source = createBorrowedImageInfo(source2.get()),
+        .props =
+            {
+                .composeMode = HWC2_COMPOSITION_DEVICE,
+                .displayFrame =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<int>(target->m_width),
+                        .bottom = static_cast<int>(target->m_height),
+                    },
+                .crop =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<float>(source2->m_width) / 2.0f,
+                        .bottom = static_cast<float>(source2->m_height) / 2.0f,
+                    },
+                .blendMode = HWC2_BLEND_MODE_PREMULTIPLIED,
+                .alpha = 1.0,
+                .color =
+                    {
+                        .r = 0,
+                        .g = 0,
+                        .b = 0,
+                        .a = 0,
+                    },
+                .transform = HWC_TRANSFORM_ROT_90,
+            },
+    });
+    compositionRequest.layers.emplace_back(Compositor::CompositionRequestLayer{
+        .source = createBorrowedImageInfo(source2.get()),
+        .props =
+            {
+                .composeMode = HWC2_COMPOSITION_DEVICE,
+                .displayFrame =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<int>(target->m_width) / 2,
+                        .bottom = static_cast<int>(target->m_height),
+                    },
+                .crop =
+                    {
+                        .left = 0,
+                        .top = 0,
+                        .right = static_cast<float>(source2->m_width),
+                        .bottom = static_cast<float>(source2->m_height),
+                    },
+                .blendMode = HWC2_BLEND_MODE_PREMULTIPLIED,
+                .alpha = 1.0,
+                .color =
+                    {
+                        .r = 0,
+                        .g = 0,
+                        .b = 0,
+                        .a = 0,
+                    },
+                .transform = HWC_TRANSFORM_FLIP_V_ROT_90,
+            },
+    });
+
+    auto compositionCompleteWaitable = compositor->compose(compositionRequest);
+    compositionCompleteWaitable.wait();
+
+    compareImageWithGoldenPng(target.get(), GetTestDataPath("256x256_golden_multiple_layers.png"),
+                              kDefaultSaveImageIfComparisonFailed);
+}
+
+}  // namespace
diff --git a/stream-servers/tests/DisplayVk_unittest.cpp b/stream-servers/tests/DisplayVk_unittest.cpp
index 7c36164..a70dc1f 100644
--- a/stream-servers/tests/DisplayVk_unittest.cpp
+++ b/stream-servers/tests/DisplayVk_unittest.cpp
@@ -1,7 +1,10 @@
+// Note: needs to be included before DisplayVk to avoid conflicts
+// between gtest and x11 headers.
 #include <gtest/gtest.h>
 
 #include "DisplayVk.h"
 
+#include "BorrowedImageVk.h"
 #include "Standalone.h"
 #include "base/Lock.h"
 #include "tests/VkTestUtils.h"
@@ -9,6 +12,8 @@
 
 class DisplayVkTest : public ::testing::Test {
    protected:
+    using RenderTexture = emugl::RenderTextureVk;
+
     static void SetUpTestCase() { k_vk = emugl::vkDispatch(false); }
 
     void SetUp() override {
@@ -52,7 +57,22 @@
         }
     }
 
-    using RenderTexture = emugl::RenderTextureVk;
+    std::unique_ptr<BorrowedImageInfoVk> createBorrowedImageInfo(
+        const std::unique_ptr<const RenderTexture>& texture) {
+        static uint32_t sTextureId = 0;
+
+        auto info = std::make_unique<BorrowedImageInfoVk>();
+        info->id = sTextureId++;
+        info->width = texture->m_vkImageCreateInfo.extent.width;
+        info->height = texture->m_vkImageCreateInfo.extent.height;
+        info->image = texture->m_vkImage;
+        info->imageCreateInfo = texture->m_vkImageCreateInfo;
+        info->preBorrowLayout = RenderTexture::k_vkImageLayout;
+        info->preBorrowQueueFamilyIndex = m_compositorQueueFamilyIndex;
+        info->postBorrowLayout = RenderTexture::k_vkImageLayout;
+        info->postBorrowQueueFamilyIndex = m_compositorQueueFamilyIndex;
+        return info;
+    }
 
     static const goldfish_vk::VulkanDispatch *k_vk;
     static const uint32_t k_width = 0x100;
@@ -133,7 +153,7 @@
                     maybeSwapChainQueueFamilyIndex = queueFamilyIndex;
                 }
                 if (!maybeCompositorQueueFamilyIndex.has_value() &&
-                    CompositorVk::validateQueueFamilyProperties(queueProps[queueFamilyIndex])) {
+                    CompositorVk::queueSupportsComposition(queueProps[queueFamilyIndex])) {
                     maybeCompositorQueueFamilyIndex = queueFamilyIndex;
                 }
             }
@@ -192,8 +212,8 @@
                                          m_vkCommandPool, textureWidth, textureHeight);
     std::vector<uint32_t> pixels(textureWidth * textureHeight, 0);
     ASSERT_TRUE(texture->write(pixels));
-    auto cbvk = displayVk.createDisplayBuffer(texture->m_vkImage, texture->m_vkImageCreateInfo);
-    ASSERT_TRUE(std::get<0>(displayVk.post(cbvk)));
+    const auto imageInfo = createBorrowedImageInfo(texture);
+    ASSERT_TRUE(std::get<0>(displayVk.post(imageInfo.get())));
 }
 
 TEST_F(DisplayVkTest, SimplePost) {
@@ -212,10 +232,10 @@
         }
     }
     ASSERT_TRUE(texture->write(pixels));
-    auto cbvk = m_displayVk->createDisplayBuffer(texture->m_vkImage, texture->m_vkImageCreateInfo);
+    const auto imageInfo = createBorrowedImageInfo(texture);
     std::vector<std::shared_future<void>> waitForGpuFutures;
     for (uint32_t i = 0; i < 10; i++) {
-        auto [success, waitForGpuFuture] = m_displayVk->post(cbvk);
+        auto [success, waitForGpuFuture] = m_displayVk->post(imageInfo.get());
         ASSERT_TRUE(success);
         waitForGpuFutures.emplace_back(std::move(waitForGpuFuture));
     }
@@ -239,16 +259,14 @@
     std::vector<uint32_t> greenPixels(textureWidth * textureHeight, green);
     ASSERT_TRUE(redTexture->write(redPixels));
     ASSERT_TRUE(greenTexture->write(greenPixels));
-    auto redCbvk =
-        m_displayVk->createDisplayBuffer(redTexture->m_vkImage, redTexture->m_vkImageCreateInfo);
-    auto greenCbvk = m_displayVk->createDisplayBuffer(greenTexture->m_vkImage,
-                                                      greenTexture->m_vkImageCreateInfo);
+    const auto redImageInfo = createBorrowedImageInfo(redTexture);
+    const auto greenImageInfo = createBorrowedImageInfo(greenTexture);
     std::vector<std::shared_future<void>> waitForGpuFutures;
     for (uint32_t i = 0; i < 10; i++) {
-        auto [success, waitForGpuFuture] = m_displayVk->post(redCbvk);
+        auto [success, waitForGpuFuture] = m_displayVk->post(redImageInfo.get());
         ASSERT_TRUE(success);
         waitForGpuFutures.emplace_back(std::move(waitForGpuFuture));
-        std::tie(success, waitForGpuFuture) = m_displayVk->post(greenCbvk);
+        std::tie(success, waitForGpuFuture) = m_displayVk->post(greenImageInfo.get());
         ASSERT_TRUE(success);
         waitForGpuFutures.emplace_back(std::move(waitForGpuFuture));
     }
diff --git a/stream-servers/tests/ImageUtils.cpp b/stream-servers/tests/ImageUtils.cpp
new file mode 100644
index 0000000..f9de13f
--- /dev/null
+++ b/stream-servers/tests/ImageUtils.cpp
@@ -0,0 +1,58 @@
+// 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 "ImageUtils.h"
+
+#include <stb/stb_image.h>
+#include <stb/stb_image_write.h>
+
+#include <cstring>
+
+bool LoadRGBAFromPng(const std::string& filename, uint32_t* outWidth, uint32_t* outHeight,
+                     std::vector<uint32_t>* outPixels) {
+    *outWidth = 0;
+    *outHeight = 0;
+    outPixels->clear();
+
+    int decodedWidth;
+    int decodedHeight;
+    int decodedChannels;
+    unsigned char* decodedPixels =
+        stbi_load(filename.c_str(), &decodedWidth, &decodedHeight, &decodedChannels,
+                  /*desiredChannels=*/4);
+    if (decodedPixels == nullptr) {
+        return false;
+    }
+
+    const std::size_t decodedSize = decodedWidth * decodedHeight;
+    const std::size_t decodedSizeBytes = decodedSize * 4;
+
+    *outWidth = static_cast<uint32_t>(decodedWidth);
+    *outHeight = static_cast<uint32_t>(decodedHeight);
+    outPixels->resize(decodedSize, 0);
+    std::memcpy(outPixels->data(), decodedPixels, decodedSizeBytes);
+
+    stbi_image_free(decodedPixels);
+
+    return true;
+}
+
+bool SaveRGBAToPng(uint32_t width, uint32_t height, const uint32_t* pixels,
+                   const std::string& filename) {
+    if (stbi_write_png(filename.c_str(), width, height,
+                       /*channels=*/4, pixels, width * 4) == 0) {
+        return false;
+    }
+    return true;
+}
diff --git a/stream-servers/tests/ImageUtils.h b/stream-servers/tests/ImageUtils.h
new file mode 100644
index 0000000..567741b
--- /dev/null
+++ b/stream-servers/tests/ImageUtils.h
@@ -0,0 +1,25 @@
+// 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.
+
+#pragma once
+
+#include <cctype>
+#include <string>
+#include <vector>
+
+bool LoadRGBAFromPng(const std::string& filename, uint32_t* outWidth, uint32_t* outHeight,
+                     std::vector<uint32_t>* outPixels);
+
+bool SaveRGBAToPng(uint32_t width, uint32_t height, const uint32_t* pixels,
+                   const std::string& filename);
diff --git a/stream-servers/tests/VkTestUtils.h b/stream-servers/tests/VkTestUtils.h
index 0a6e9af..b509135 100644
--- a/stream-servers/tests/VkTestUtils.h
+++ b/stream-servers/tests/VkTestUtils.h
@@ -48,7 +48,7 @@
 template <VkImageLayout imageLayout, VkImageUsageFlags imageUsage>
 struct RenderResourceVk : public RenderResourceVkBase {
    public:
-    static constexpr VkFormat k_vkFormat = VK_FORMAT_R8G8B8A8_SRGB;
+    static constexpr VkFormat k_vkFormat = VK_FORMAT_R8G8B8A8_UNORM;
     static constexpr uint32_t k_bpp = 4;
     static constexpr VkImageLayout k_vkImageLayout = imageLayout;
 
diff --git a/stream-servers/tests/testdata/256x256_android.png b/stream-servers/tests/testdata/256x256_android.png
new file mode 100644
index 0000000..4dd93d7
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_android.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_android_with_transparency.png b/stream-servers/tests/testdata/256x256_android_with_transparency.png
new file mode 100644
index 0000000..1618bd6
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_android_with_transparency.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_blend_premultiplied.png b/stream-servers/tests/testdata/256x256_golden_blend_premultiplied.png
new file mode 100644
index 0000000..8cb1d16
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_blend_premultiplied.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_crop.png b/stream-servers/tests/testdata/256x256_golden_crop.png
new file mode 100644
index 0000000..59417c5
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_crop.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_layers.png b/stream-servers/tests/testdata/256x256_golden_multiple_layers.png
new file mode 100644
index 0000000..dab9f08
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_layers.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_0.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_0.png
new file mode 100644
index 0000000..26fc138
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_0.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_1.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_1.png
new file mode 100644
index 0000000..8ff75c5
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_1.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_2.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_2.png
new file mode 100644
index 0000000..40f4a05
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_2.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_3.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_3.png
new file mode 100644
index 0000000..ea8489d
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_3.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_4.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_4.png
new file mode 100644
index 0000000..96f28ab
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_4.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_5.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_5.png
new file mode 100644
index 0000000..f94807c
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_5.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_6.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_6.png
new file mode 100644
index 0000000..3f228ec
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_6.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_7.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_7.png
new file mode 100644
index 0000000..e973b57
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_7.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_8.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_8.png
new file mode 100644
index 0000000..4cd7269
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_8.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_multiple_targets_9.png b/stream-servers/tests/testdata/256x256_golden_multiple_targets_9.png
new file mode 100644
index 0000000..b3c8f0b
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_multiple_targets_9.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_simple_composition.png b/stream-servers/tests/testdata/256x256_golden_simple_composition.png
new file mode 100644
index 0000000..a2345d0
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_simple_composition.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_transform_fliph.png b/stream-servers/tests/testdata/256x256_golden_transform_fliph.png
new file mode 100644
index 0000000..96e8c11
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_transform_fliph.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_transform_fliphrot90.png b/stream-servers/tests/testdata/256x256_golden_transform_fliphrot90.png
new file mode 100644
index 0000000..ad77440
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_transform_fliphrot90.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_transform_flipv.png b/stream-servers/tests/testdata/256x256_golden_transform_flipv.png
new file mode 100644
index 0000000..1b42862
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_transform_flipv.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_transform_flipvrot90.png b/stream-servers/tests/testdata/256x256_golden_transform_flipvrot90.png
new file mode 100644
index 0000000..3b6c18a
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_transform_flipvrot90.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_transform_none.png b/stream-servers/tests/testdata/256x256_golden_transform_none.png
new file mode 100644
index 0000000..4055960
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_transform_none.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_transform_rot180.png b/stream-servers/tests/testdata/256x256_golden_transform_rot180.png
new file mode 100644
index 0000000..b7a30c6
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_transform_rot180.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_transform_rot270.png b/stream-servers/tests/testdata/256x256_golden_transform_rot270.png
new file mode 100644
index 0000000..db1dc9f
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_transform_rot270.png
Binary files differ
diff --git a/stream-servers/tests/testdata/256x256_golden_transform_rot90.png b/stream-servers/tests/testdata/256x256_golden_transform_rot90.png
new file mode 100644
index 0000000..6580bb3
--- /dev/null
+++ b/stream-servers/tests/testdata/256x256_golden_transform_rot90.png
Binary files differ
diff --git a/stream-servers/vulkan/Android.bp b/stream-servers/vulkan/Android.bp
index 4191120..41cf32e 100644
--- a/stream-servers/vulkan/Android.bp
+++ b/stream-servers/vulkan/Android.bp
@@ -30,6 +30,7 @@
         "-Wno-unreachable-code-loop-increment",
     ],
     srcs: [
+        "BorrowedImageVk.cpp",
         "CompositorVk.cpp",
         "DisplayVk.cpp",
         "SwapChainStateVk.cpp",
diff --git a/stream-servers/vulkan/BorrowedImageVk.cpp b/stream-servers/vulkan/BorrowedImageVk.cpp
new file mode 100644
index 0000000..7f84547
--- /dev/null
+++ b/stream-servers/vulkan/BorrowedImageVk.cpp
@@ -0,0 +1,113 @@
+// 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 "BorrowedImageVk.h"
+
+void addNeededBarriersToUseBorrowedImage(
+    const BorrowedImageInfoVk& borrowedImageInfo, uint32_t usedQueueFamilyIndex,
+    VkImageLayout usedInitialImageLayout, VkImageLayout usedFinalImageLayout,
+    std::vector<VkImageMemoryBarrier>* preUseQueueTransferBarriers,
+    std::vector<VkImageMemoryBarrier>* preUseLayoutTransitionBarriers,
+    std::vector<VkImageMemoryBarrier>* postUseLayoutTransitionBarriers,
+    std::vector<VkImageMemoryBarrier>* postUseQueueTransferBarriers) {
+    if (borrowedImageInfo.preBorrowQueueFamilyIndex != usedQueueFamilyIndex) {
+        const VkImageMemoryBarrier queueTransferBarrier = {
+            .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+            .pNext = nullptr,
+            .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
+            .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT,
+            .oldLayout = borrowedImageInfo.preBorrowLayout,
+            .newLayout = borrowedImageInfo.preBorrowLayout,
+            .srcQueueFamilyIndex = borrowedImageInfo.preBorrowQueueFamilyIndex,
+            .dstQueueFamilyIndex = usedQueueFamilyIndex,
+            .image = borrowedImageInfo.image,
+            .subresourceRange =
+                {
+                    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                    .baseMipLevel = 0,
+                    .levelCount = 1,
+                    .baseArrayLayer = 0,
+                    .layerCount = 1,
+                },
+        };
+        preUseQueueTransferBarriers->emplace_back(queueTransferBarrier);
+    }
+    if (borrowedImageInfo.preBorrowLayout != usedInitialImageLayout &&
+        usedInitialImageLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
+        const VkImageMemoryBarrier layoutTransitionBarrier = {
+            .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+            .pNext = nullptr,
+            .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
+            .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT,
+            .oldLayout = borrowedImageInfo.preBorrowLayout,
+            .newLayout = usedInitialImageLayout,
+            .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+            .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+            .image = borrowedImageInfo.image,
+            .subresourceRange =
+                {
+                    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                    .baseMipLevel = 0,
+                    .levelCount = 1,
+                    .baseArrayLayer = 0,
+                    .layerCount = 1,
+                },
+        };
+        preUseLayoutTransitionBarriers->emplace_back(layoutTransitionBarrier);
+    }
+    if (borrowedImageInfo.postBorrowLayout != usedFinalImageLayout) {
+        const VkImageMemoryBarrier layoutTransitionBarrier = {
+            .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+            .pNext = nullptr,
+            .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
+            .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT,
+            .oldLayout = usedFinalImageLayout,
+            .newLayout = borrowedImageInfo.postBorrowLayout,
+            .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+            .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+            .image = borrowedImageInfo.image,
+            .subresourceRange =
+                {
+                    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                    .baseMipLevel = 0,
+                    .levelCount = 1,
+                    .baseArrayLayer = 0,
+                    .layerCount = 1,
+                },
+        };
+        postUseLayoutTransitionBarriers->emplace_back(layoutTransitionBarrier);
+    }
+    if (borrowedImageInfo.postBorrowQueueFamilyIndex != usedQueueFamilyIndex) {
+        const VkImageMemoryBarrier queueTransferBarrier = {
+            .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+            .pNext = nullptr,
+            .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
+            .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT,
+            .oldLayout = borrowedImageInfo.postBorrowLayout,
+            .newLayout = borrowedImageInfo.postBorrowLayout,
+            .srcQueueFamilyIndex = usedQueueFamilyIndex,
+            .dstQueueFamilyIndex = borrowedImageInfo.postBorrowQueueFamilyIndex,
+            .image = borrowedImageInfo.image,
+            .subresourceRange =
+                {
+                    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                    .baseMipLevel = 0,
+                    .levelCount = 1,
+                    .baseArrayLayer = 0,
+                    .layerCount = 1,
+                },
+        };
+        postUseQueueTransferBarriers->emplace_back(queueTransferBarrier);
+    }
+}
diff --git a/stream-servers/BorrowedImageVk.h b/stream-servers/vulkan/BorrowedImageVk.h
similarity index 77%
rename from stream-servers/BorrowedImageVk.h
rename to stream-servers/vulkan/BorrowedImageVk.h
index 2ee18dd..87902d3 100644
--- a/stream-servers/BorrowedImageVk.h
+++ b/stream-servers/vulkan/BorrowedImageVk.h
@@ -14,6 +14,9 @@
 
 #pragma once
 
+#include <cstdint>
+#include <vector>
+
 #include "BorrowedImage.h"
 #include "vulkan/cereal/common/goldfish_vk_dispatch.h"
 
@@ -45,3 +48,11 @@
     // after composition.
     uint32_t postBorrowQueueFamilyIndex = 0;
 };
+
+void addNeededBarriersToUseBorrowedImage(
+    const BorrowedImageInfoVk& borrowedImageInfo, uint32_t usedQueueFamilyIndex,
+    VkImageLayout usedInitialImageLayout, VkImageLayout usedFinalImageLayout,
+    std::vector<VkImageMemoryBarrier>* preUseQueueTransferBarriers,
+    std::vector<VkImageMemoryBarrier>* preUseLayoutTransitionBarriers,
+    std::vector<VkImageMemoryBarrier>* postUseLayoutTransitionBarriers,
+    std::vector<VkImageMemoryBarrier>* postUseQueueTransferBarriers);
diff --git a/stream-servers/vulkan/CMakeLists.txt b/stream-servers/vulkan/CMakeLists.txt
index 0dc73bf..ef13d4c 100644
--- a/stream-servers/vulkan/CMakeLists.txt
+++ b/stream-servers/vulkan/CMakeLists.txt
@@ -1,6 +1,7 @@
 add_subdirectory(cereal)
 
 add_library(gfxstream-vulkan-server
+            BorrowedImageVk.cpp
             CompositorVk.cpp
             DisplayVk.cpp
             SwapChainStateVk.cpp
diff --git a/stream-servers/vulkan/CompositorVk.cpp b/stream-servers/vulkan/CompositorVk.cpp
index 3f73086..685585f 100644
--- a/stream-servers/vulkan/CompositorVk.cpp
+++ b/stream-servers/vulkan/CompositorVk.cpp
@@ -7,7 +7,7 @@
 #include <optional>
 
 #include "host-common/logging.h"
-#include "vulkan/VkCommonOperations.h"
+#include "vulkan/vk_enum_string_helper.h"
 #include "vulkan/vk_util.h"
 
 using emugl::ABORT_REASON_OTHER;
@@ -18,150 +18,145 @@
 #include "vulkan/CompositorVertexShader.h"
 }  // namespace CompositorVkShader
 
-#define COMPOSITOR_VK_ERROR(fmt, ...)                                                         \
-    do {                                                                                      \
-        fprintf(stderr, "%s(%s:%d): " fmt "\n", __func__, __FILE__, __LINE__, ##__VA_ARGS__); \
-        fflush(stderr);                                                                       \
-    } while (0)
+namespace {
+
+constexpr const VkImageLayout kSourceImageInitialLayoutUsed =
+    VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+constexpr const VkImageLayout kSourceImageFinalLayoutUsed =
+    VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+
+constexpr const VkImageLayout kTargetImageInitialLayoutUsed = VK_IMAGE_LAYOUT_UNDEFINED;
+constexpr const VkImageLayout kTargetImageFinalLayoutUsed = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
+
+const BorrowedImageInfoVk* getInfoOrAbort(const std::unique_ptr<BorrowedImageInfo>& info) {
+    auto imageVk = static_cast<const BorrowedImageInfoVk*>(info.get());
+    if (imageVk != nullptr) {
+        return imageVk;
+    }
+
+    GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER))
+        << "CompositorVk did not find BorrowedImageInfoVk";
+}
+
+struct Vertex {
+    alignas(8) glm::vec2 pos;
+    alignas(8) glm::vec2 tex;
+
+    static VkVertexInputBindingDescription getBindingDescription() {
+        return VkVertexInputBindingDescription{
+            .binding = 0,
+            .stride = sizeof(struct Vertex),
+            .inputRate = VK_VERTEX_INPUT_RATE_VERTEX,
+        };
+    }
+
+    static std::array<VkVertexInputAttributeDescription, 2> getAttributeDescription() {
+        return {
+            VkVertexInputAttributeDescription{
+                .location = 0,
+                .binding = 0,
+                .format = VK_FORMAT_R32G32_SFLOAT,
+                .offset = offsetof(struct Vertex, pos),
+            },
+            VkVertexInputAttributeDescription{
+                .location = 1,
+                .binding = 0,
+                .format = VK_FORMAT_R32G32_SFLOAT,
+                .offset = offsetof(struct Vertex, tex),
+            },
+        };
+    }
+};
+
+static const std::vector<Vertex> k_vertices = {
+    // clang-format off
+    { .pos = {-1.0f, -1.0f}, .tex = {0.0f, 0.0f}},
+    { .pos = { 1.0f, -1.0f}, .tex = {1.0f, 0.0f}},
+    { .pos = { 1.0f,  1.0f}, .tex = {1.0f, 1.0f}},
+    { .pos = {-1.0f,  1.0f}, .tex = {0.0f, 1.0f}},
+    // clang-format on
+};
+
+static const std::vector<uint16_t> k_indices = {0, 1, 2, 2, 3, 0};
 
 static VkShaderModule createShaderModule(const goldfish_vk::VulkanDispatch& vk, VkDevice device,
                                          const std::vector<uint32_t>& code) {
-    VkShaderModuleCreateInfo shaderModuleCi = {
+    const VkShaderModuleCreateInfo shaderModuleCi = {
         .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
         .codeSize = static_cast<uint32_t>(code.size() * sizeof(uint32_t)),
-        .pCode = code.data()};
+        .pCode = code.data(),
+    };
     VkShaderModule res;
     VK_CHECK(vk.vkCreateShaderModule(device, &shaderModuleCi, nullptr, &res));
     return res;
 }
 
-ComposeLayerVk::ComposeLayerVk(VkSampler vkSampler, VkImageView vkImageView,
-                               const LayerTransform& layerTransform)
-    : m_vkSampler(vkSampler),
-      m_vkImageView(vkImageView),
-      m_layerTransform({.pos = layerTransform.pos, .texcoord = layerTransform.texcoord}) {}
+}  // namespace
 
-std::unique_ptr<ComposeLayerVk> ComposeLayerVk::createFromHwc2ComposeLayer(
-    VkSampler vkSampler, VkImageView vkImageView, const ComposeLayer& composeLayer,
-    uint32_t cbWidth, uint32_t cbHeight, uint32_t dstWidth, uint32_t dstHeight) {
-    // Calculate the posTransform and the texcoordTransform needed in the
-    // uniform of the Compositor.vert shader. The posTransform should transform
-    // the square(top = -1, bottom = 1, left = -1, right = 1) to the position
-    // where the layer should be drawn in NDC space given the composeLayer.
-    // texcoordTransform should transform the unit square(top = 0, bottom = 1,
-    // left = 0, right = 1) to where we should sample the layer in the
-    // normalized uv space given the composeLayer.
-    const hwc_rect_t& posRect = composeLayer.displayFrame;
-    const hwc_frect_t& texcoordRect = composeLayer.crop;
-
-    int posWidth = posRect.right - posRect.left;
-    int posHeight = posRect.bottom - posRect.top;
-
-    float posScaleX = float(posWidth) / dstWidth;
-    float posScaleY = float(posHeight) / dstHeight;
-
-    float posTranslateX = -1.0f + posScaleX + 2.0f * float(posRect.left) / dstWidth;
-    float posTranslateY = -1.0f + posScaleY + 2.0f * float(posRect.top) / dstHeight;
-
-    float texcoordScalX = (texcoordRect.right - texcoordRect.left) / float(cbWidth);
-    float texCoordScaleY = (texcoordRect.bottom - texcoordRect.top) / float(cbHeight);
-
-    float texCoordTranslateX = texcoordRect.left / float(cbWidth);
-    float texCoordTranslateY = texcoordRect.top / float(cbHeight);
-
-    float texcoordRotation = 0.0f;
-
-    const float pi = glm::pi<float>();
-
-    switch (composeLayer.transform) {
-        case HWC_TRANSFORM_ROT_90:
-            texcoordRotation = pi * 0.5f;
-            break;
-        case HWC_TRANSFORM_ROT_180:
-            texcoordRotation = pi;
-            break;
-        case HWC_TRANSFORM_ROT_270:
-            texcoordRotation = pi * 1.5f;
-            break;
-        case HWC_TRANSFORM_FLIP_H:
-            texcoordScalX *= -1.0f;
-            break;
-        case HWC_TRANSFORM_FLIP_V:
-            texCoordScaleY *= -1.0f;
-            break;
-        case HWC_TRANSFORM_FLIP_H_ROT_90:
-            texcoordRotation = pi * 0.5f;
-            texcoordScalX *= -1.0f;
-            break;
-        case HWC_TRANSFORM_FLIP_V_ROT_90:
-            texcoordRotation = pi * 0.5f;
-            texCoordScaleY *= -1.0f;
-            break;
-        default:
-            break;
+CompositorVk::RenderTarget::RenderTarget(const goldfish_vk::VulkanDispatch& vk, VkDevice vkDevice,
+                                         VkImage vkImage, VkImageView vkImageView, uint32_t width,
+                                         uint32_t height, VkRenderPass vkRenderPass)
+    : m_vk(vk),
+      m_vkDevice(vkDevice),
+      m_vkImage(vkImage),
+      m_vkFramebuffer(VK_NULL_HANDLE),
+      m_width(width),
+      m_height(height) {
+    if (vkImageView == VK_NULL_HANDLE) {
+        GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER))
+            << "CompositorVk found empty image view handle when creating RenderTarget.";
     }
 
-    ComposeLayerVk::LayerTransform layerTransform = {
-        .pos = glm::translate(glm::mat4(1.0f), glm::vec3(posTranslateX, posTranslateY, 0.0f)) *
-               glm::scale(glm::mat4(1.0f), glm::vec3(posScaleX, posScaleY, 1.0f)),
-        .texcoord = glm::translate(glm::mat4(1.0f),
-                                   glm::vec3(texCoordTranslateX, texCoordTranslateY, 0.0f)) *
-                    glm::scale(glm::mat4(1.0f), glm::vec3(texcoordScalX, texCoordScaleY, 1.0f)) *
-                    glm::rotate(glm::mat4(1.0f), texcoordRotation, glm::vec3(0.0f, 0.0f, 1.0f)),
+    const VkFramebufferCreateInfo framebufferCi = {
+        .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
+        .flags = 0,
+        .renderPass = vkRenderPass,
+        .attachmentCount = 1,
+        .pAttachments = &vkImageView,
+        .width = width,
+        .height = height,
+        .layers = 1,
     };
-
-    return std::unique_ptr<ComposeLayerVk>(
-        new ComposeLayerVk(vkSampler, vkImageView, layerTransform));
+    VK_CHECK(m_vk.vkCreateFramebuffer(vkDevice, &framebufferCi, nullptr, &m_vkFramebuffer));
 }
 
-Composition::Composition(std::vector<std::unique_ptr<ComposeLayerVk>> composeLayers)
-    : m_composeLayers(std::move(composeLayers)) {}
-
-const std::vector<CompositorVk::Vertex> CompositorVk::k_vertices = {
-    {{-1.0f, -1.0f}, {0.0f, 0.0f}},
-    {{1.0f, -1.0f}, {1.0f, 0.0f}},
-    {{1.0f, 1.0f}, {1.0f, 1.0f}},
-    {{-1.0f, 1.0f}, {0.0f, 1.0f}},
-};
-
-const std::vector<uint16_t> CompositorVk::k_indices = {0, 1, 2, 2, 3, 0};
+CompositorVk::RenderTarget::~RenderTarget() {
+    if (m_vkFramebuffer != VK_NULL_HANDLE) {
+        m_vk.vkDestroyFramebuffer(m_vkDevice, m_vkFramebuffer, nullptr);
+    }
+}
 
 std::unique_ptr<CompositorVk> CompositorVk::create(
     const goldfish_vk::VulkanDispatch& vk, VkDevice vkDevice, VkPhysicalDevice vkPhysicalDevice,
-    VkQueue vkQueue, std::shared_ptr<android::base::Lock> queueLock, VkFormat format,
-    VkImageLayout initialLayout, VkImageLayout finalLayout, uint32_t maxFramesInFlight,
-    VkCommandPool commandPool, VkSampler sampler) {
+    VkQueue vkQueue, std::shared_ptr<android::base::Lock> queueLock, uint32_t queueFamilyIndex,
+    uint32_t maxFramesInFlight) {
     auto res = std::unique_ptr<CompositorVk>(new CompositorVk(
-        vk, vkDevice, vkPhysicalDevice, vkQueue, queueLock, commandPool, maxFramesInFlight));
-    res->setUpGraphicsPipeline(format, initialLayout, finalLayout, sampler);
-    res->m_vkSampler = sampler;
+        vk, vkDevice, vkPhysicalDevice, vkQueue, queueLock, queueFamilyIndex, maxFramesInFlight));
+    res->setUpCommandPool();
+    res->setUpSampler();
+    res->setUpGraphicsPipeline();
     res->setUpVertexBuffers();
     res->setUpUniformBuffers();
     res->setUpDescriptorSets();
-    res->m_currentCompositions.resize(maxFramesInFlight);
-    for (auto i = 0; i < maxFramesInFlight; i++) {
-        std::vector<std::unique_ptr<ComposeLayerVk>> emptyCompositionLayers;
-        res->setComposition(i, std::make_unique<Composition>(std::move(emptyCompositionLayers)));
-    }
+    res->setUpFences();
+    res->setUpFrameResourceFutures();
     return res;
 }
 
 CompositorVk::CompositorVk(const goldfish_vk::VulkanDispatch& vk, VkDevice vkDevice,
                            VkPhysicalDevice vkPhysicalDevice, VkQueue vkQueue,
                            std::shared_ptr<android::base::Lock> queueLock,
-                           VkCommandPool commandPool, uint32_t maxFramesInFlight)
-    : CompositorVkBase(vk, vkDevice, vkPhysicalDevice, vkQueue, queueLock, commandPool),
+                           uint32_t queueFamilyIndex, uint32_t maxFramesInFlight)
+    : CompositorVkBase(vk, vkDevice, vkPhysicalDevice, vkQueue, queueLock, queueFamilyIndex,
+                       maxFramesInFlight),
       m_maxFramesInFlight(maxFramesInFlight),
-      m_vkSampler(VK_NULL_HANDLE),
-      m_currentCompositions(0),
-      m_uniformStorage({VK_NULL_HANDLE, VK_NULL_HANDLE, nullptr, 0}) {
-    VkPhysicalDeviceProperties physicalDeviceProperties;
-    m_vk.vkGetPhysicalDeviceProperties(m_vkPhysicalDevice, &physicalDeviceProperties);
-    auto alignment = physicalDeviceProperties.limits.minUniformBufferOffsetAlignment;
-    m_uniformStorage.m_stride = ((sizeof(UniformBufferObject) - 1) / alignment + 1) * alignment;
-}
+      m_renderTargetCache(k_renderTargetCacheSize) {}
 
 CompositorVk::~CompositorVk() {
+    {
+        android::base::AutoLock lock(*m_vkQueueLock);
+        VK_CHECK(vk_util::waitForVkQueueIdleWithRetry(m_vk, m_vkQueue));
+    }
     m_vk.vkDestroyDescriptorPool(m_vkDevice, m_vkDescriptorPool, nullptr);
     if (m_uniformStorage.m_vkDeviceMemory != VK_NULL_HANDLE) {
         m_vk.vkUnmapMemory(m_vkDevice, m_uniformStorage.m_vkDeviceMemory);
@@ -175,11 +170,15 @@
     m_vk.vkDestroyPipeline(m_vkDevice, m_graphicsVkPipeline, nullptr);
     m_vk.vkDestroyRenderPass(m_vkDevice, m_vkRenderPass, nullptr);
     m_vk.vkDestroyPipelineLayout(m_vkDevice, m_vkPipelineLayout, nullptr);
+    m_vk.vkDestroySampler(m_vkDevice, m_vkSampler, nullptr);
     m_vk.vkDestroyDescriptorSetLayout(m_vkDevice, m_vkDescriptorSetLayout, nullptr);
+    m_vk.vkDestroyCommandPool(m_vkDevice, m_vkCommandPool, nullptr);
+    for (PerFrameResources& frameResources : m_frameResources) {
+        m_vk.vkDestroyFence(m_vkDevice, frameResources.m_vkFence, nullptr);
+    }
 }
 
-void CompositorVk::setUpGraphicsPipeline(VkFormat renderTargetFormat, VkImageLayout initialLayout,
-                                         VkImageLayout finalLayout, VkSampler sampler) {
+void CompositorVk::setUpGraphicsPipeline() {
     const std::vector<uint32_t> vertSpvBuff(CompositorVkShader::compositorVertexShader,
                                             std::end(CompositorVkShader::compositorVertexShader));
     const std::vector<uint32_t> fragSpvBuff(CompositorVkShader::compositorFragmentShader,
@@ -187,31 +186,37 @@
     const auto vertShaderMod = createShaderModule(m_vk, m_vkDevice, vertSpvBuff);
     const auto fragShaderMod = createShaderModule(m_vk, m_vkDevice, fragSpvBuff);
 
-    VkPipelineShaderStageCreateInfo shaderStageCis[2] = {
-        {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
-         .stage = VK_SHADER_STAGE_VERTEX_BIT,
-         .module = vertShaderMod,
-         .pName = "main"},
-        {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
-         .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
-         .module = fragShaderMod,
-         .pName = "main"}};
+    const VkPipelineShaderStageCreateInfo shaderStageCis[2] = {
+        VkPipelineShaderStageCreateInfo{
+            .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+            .stage = VK_SHADER_STAGE_VERTEX_BIT,
+            .module = vertShaderMod,
+            .pName = "main",
+        },
+        VkPipelineShaderStageCreateInfo{
+            .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+            .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
+            .module = fragShaderMod,
+            .pName = "main",
+        },
+    };
 
-    auto bindingDescription = Vertex::getBindingDescription();
-    auto attributeDescription = Vertex::getAttributeDescription();
-    VkPipelineVertexInputStateCreateInfo vertexInputStateCi = {
+    const auto vertexAttributeDescription = Vertex::getAttributeDescription();
+    const auto vertexBindingDescription = Vertex::getBindingDescription();
+    const VkPipelineVertexInputStateCreateInfo vertexInputStateCi = {
         .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
         .vertexBindingDescriptionCount = 1,
-        .pVertexBindingDescriptions = &bindingDescription,
-        .vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescription.size()),
-        .pVertexAttributeDescriptions = attributeDescription.data()};
-    VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateCi = {
+        .pVertexBindingDescriptions = &vertexBindingDescription,
+        .vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexAttributeDescription.size()),
+        .pVertexAttributeDescriptions = vertexAttributeDescription.data(),
+    };
+    const VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateCi = {
         .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
         .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
         .primitiveRestartEnable = VK_FALSE,
     };
 
-    VkPipelineViewportStateCreateInfo viewportStateCi = {
+    const VkPipelineViewportStateCreateInfo viewportStateCi = {
         .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
         .viewportCount = 1,
         // The viewport state is dynamic.
@@ -221,7 +226,7 @@
         .pScissors = nullptr,
     };
 
-    VkPipelineRasterizationStateCreateInfo rasterizerStateCi = {
+    const VkPipelineRasterizationStateCreateInfo rasterizerStateCi = {
         .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
         .depthClampEnable = VK_FALSE,
         .rasterizerDiscardEnable = VK_FALSE,
@@ -232,18 +237,20 @@
         .depthBiasConstantFactor = 0.0f,
         .depthBiasClamp = 0.0f,
         .depthBiasSlopeFactor = 0.0f,
-        .lineWidth = 1.0f};
+        .lineWidth = 1.0f,
+    };
 
-    VkPipelineMultisampleStateCreateInfo multisampleStateCi = {
+    const VkPipelineMultisampleStateCreateInfo multisampleStateCi = {
         .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
         .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
         .sampleShadingEnable = VK_FALSE,
         .minSampleShading = 1.0f,
         .pSampleMask = nullptr,
         .alphaToCoverageEnable = VK_FALSE,
-        .alphaToOneEnable = VK_FALSE};
+        .alphaToOneEnable = VK_FALSE,
+    };
 
-    VkPipelineColorBlendAttachmentState colorBlendAttachment = {
+    const VkPipelineColorBlendAttachmentState colorBlendAttachment = {
         .blendEnable = VK_TRUE,
         .srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
         .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
@@ -252,90 +259,110 @@
         .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
         .alphaBlendOp = VK_BLEND_OP_ADD,
         .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
-                          VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT};
+                          VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT,
+    };
 
-    VkPipelineColorBlendStateCreateInfo colorBlendStateCi = {
+    const VkPipelineColorBlendStateCreateInfo colorBlendStateCi = {
         .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
         .logicOpEnable = VK_FALSE,
         .attachmentCount = 1,
-        .pAttachments = &colorBlendAttachment};
+        .pAttachments = &colorBlendAttachment,
+    };
 
-    VkDynamicState dynamicStates[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
-    VkPipelineDynamicStateCreateInfo dynamicStateCi = {
+    const VkDynamicState dynamicStates[] = {
+        VK_DYNAMIC_STATE_VIEWPORT,
+        VK_DYNAMIC_STATE_SCISSOR,
+    };
+    const VkPipelineDynamicStateCreateInfo dynamicStateCi = {
         .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
         .dynamicStateCount = std::size(dynamicStates),
         .pDynamicStates = dynamicStates,
     };
 
-    VkDescriptorSetLayoutBinding layoutBindings[2] = {
-        {.binding = 0,
-         .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
-         .descriptorCount = 1,
-         .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
-         .pImmutableSamplers = &sampler},
-        {.binding = 1,
-         .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
-         .descriptorCount = 1,
-         .stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
-         .pImmutableSamplers = nullptr}};
+    const VkDescriptorSetLayoutBinding layoutBindings[2] = {
+        VkDescriptorSetLayoutBinding{
+            .binding = 0,
+            .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+            .descriptorCount = 1,
+            .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
+            .pImmutableSamplers = &m_vkSampler,
+        },
+        VkDescriptorSetLayoutBinding{
+            .binding = 1,
+            .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+            .descriptorCount = 1,
+            .stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
+            .pImmutableSamplers = nullptr,
+        },
+    };
 
-    VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCi = {
+    const VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCi = {
         .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
         .pNext = nullptr,
         .flags = 0,
         .bindingCount = static_cast<uint32_t>(std::size(layoutBindings)),
-        .pBindings = layoutBindings};
+        .pBindings = layoutBindings,
+    };
     VK_CHECK(m_vk.vkCreateDescriptorSetLayout(m_vkDevice, &descriptorSetLayoutCi, nullptr,
                                               &m_vkDescriptorSetLayout));
 
-    VkPipelineLayoutCreateInfo pipelineLayoutCi = {
+    const VkPipelineLayoutCreateInfo pipelineLayoutCi = {
         .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
         .setLayoutCount = 1,
         .pSetLayouts = &m_vkDescriptorSetLayout,
-        .pushConstantRangeCount = 0};
+        .pushConstantRangeCount = 0,
+    };
 
     VK_CHECK(
         m_vk.vkCreatePipelineLayout(m_vkDevice, &pipelineLayoutCi, nullptr, &m_vkPipelineLayout));
 
-    VkAttachmentDescription colorAttachment = {.format = renderTargetFormat,
-                                               .samples = VK_SAMPLE_COUNT_1_BIT,
-                                               .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
-                                               .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
-                                               .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
-                                               .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
-                                               .initialLayout = initialLayout,
-                                               .finalLayout = finalLayout};
+    const VkAttachmentDescription colorAttachment = {
+        .format = VK_FORMAT_R8G8B8A8_UNORM,
+        .samples = VK_SAMPLE_COUNT_1_BIT,
+        .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
+        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
+        .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
+        .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
+        .initialLayout = kTargetImageInitialLayoutUsed,
+        .finalLayout = kTargetImageFinalLayoutUsed,
+    };
 
-    VkAttachmentReference colorAttachmentRef = {.attachment = 0,
-                                                .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
+    const VkAttachmentReference colorAttachmentRef = {
+        .attachment = 0,
+        .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+    };
 
-    VkSubpassDescription subpass = {.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
-                                    .colorAttachmentCount = 1,
-                                    .pColorAttachments = &colorAttachmentRef};
+    const VkSubpassDescription subpass = {
+        .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
+        .colorAttachmentCount = 1,
+        .pColorAttachments = &colorAttachmentRef,
+    };
 
     // TODO: to support multiple layer composition, we could run the same render
     // pass for multiple time. In that case, we should use explicit
     // VkImageMemoryBarriers to transform the image layout instead of relying on
     // renderpass to do it.
-    VkSubpassDependency subpassDependency = {
+    const VkSubpassDependency subpassDependency = {
         .srcSubpass = VK_SUBPASS_EXTERNAL,
         .dstSubpass = 0,
         .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
         .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
         .srcAccessMask = 0,
-        .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT};
+        .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+    };
 
-    VkRenderPassCreateInfo renderPassCi = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
-                                           .attachmentCount = 1,
-                                           .pAttachments = &colorAttachment,
-                                           .subpassCount = 1,
-                                           .pSubpasses = &subpass,
-                                           .dependencyCount = 1,
-                                           .pDependencies = &subpassDependency};
-
+    const VkRenderPassCreateInfo renderPassCi = {
+        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
+        .attachmentCount = 1,
+        .pAttachments = &colorAttachment,
+        .subpassCount = 1,
+        .pSubpasses = &subpass,
+        .dependencyCount = 1,
+        .pDependencies = &subpassDependency,
+    };
     VK_CHECK(m_vk.vkCreateRenderPass(m_vkDevice, &renderPassCi, nullptr, &m_vkRenderPass));
 
-    VkGraphicsPipelineCreateInfo graphicsPipelineCi = {
+    const VkGraphicsPipelineCreateInfo graphicsPipelineCi = {
         .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
         .stageCount = static_cast<uint32_t>(std::size(shaderStageCis)),
         .pStages = shaderStageCis,
@@ -351,8 +378,8 @@
         .renderPass = m_vkRenderPass,
         .subpass = 0,
         .basePipelineHandle = VK_NULL_HANDLE,
-        .basePipelineIndex = -1};
-
+        .basePipelineIndex = -1,
+    };
     VK_CHECK(m_vk.vkCreateGraphicsPipelines(m_vkDevice, VK_NULL_HANDLE, 1, &graphicsPipelineCi,
                                             nullptr, &m_graphicsVkPipeline));
 
@@ -360,15 +387,211 @@
     m_vk.vkDestroyShaderModule(m_vkDevice, fragShaderMod, nullptr);
 }
 
+void CompositorVk::setUpVertexBuffers() {
+    const VkDeviceSize vertexBufferSize = sizeof(Vertex) * k_vertices.size();
+    std::tie(m_vertexVkBuffer, m_vertexVkDeviceMemory) =
+        createBuffer(vertexBufferSize,
+                     VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
+                     VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
+            .value();
+    auto [vertexStagingBuffer, vertexStagingBufferMemory] =
+        createStagingBufferWithData(k_vertices.data(), vertexBufferSize);
+    copyBuffer(vertexStagingBuffer, m_vertexVkBuffer, vertexBufferSize);
+    m_vk.vkDestroyBuffer(m_vkDevice, vertexStagingBuffer, nullptr);
+    m_vk.vkFreeMemory(m_vkDevice, vertexStagingBufferMemory, nullptr);
+
+    VkDeviceSize indexBufferSize = sizeof(k_indices[0]) * k_indices.size();
+    auto [indexStagingBuffer, indexStagingBufferMemory] =
+        createStagingBufferWithData(k_indices.data(), indexBufferSize);
+    std::tie(m_indexVkBuffer, m_indexVkDeviceMemory) =
+        createBuffer(indexBufferSize,
+                     VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
+                     VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
+            .value();
+
+    copyBuffer(indexStagingBuffer, m_indexVkBuffer, indexBufferSize);
+    m_vk.vkDestroyBuffer(m_vkDevice, indexStagingBuffer, nullptr);
+    m_vk.vkFreeMemory(m_vkDevice, indexStagingBufferMemory, nullptr);
+}
+
+void CompositorVk::setUpDescriptorSets() {
+    const uint32_t descriptorSetsPerFrame = kMaxLayersPerFrame;
+    const uint32_t descriptorSetsTotal = descriptorSetsPerFrame * m_maxFramesInFlight;
+
+    const uint32_t descriptorsOfEachTypePerSet = 1;
+    const uint32_t descriptorsOfEachTypePerFrame =
+        descriptorSetsPerFrame * descriptorsOfEachTypePerSet;
+    const uint32_t descriptorsOfEachTypeTotal = descriptorsOfEachTypePerFrame * m_maxFramesInFlight;
+
+    const VkDescriptorPoolSize descriptorPoolSizes[2] = {
+        VkDescriptorPoolSize{
+            .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+            .descriptorCount = descriptorsOfEachTypeTotal,
+        },
+        VkDescriptorPoolSize{
+            .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+            .descriptorCount = descriptorsOfEachTypeTotal,
+        }};
+    const VkDescriptorPoolCreateInfo descriptorPoolCi = {
+        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
+        .flags = 0,
+        .maxSets = descriptorSetsTotal,
+        .poolSizeCount = static_cast<uint32_t>(std::size(descriptorPoolSizes)),
+        .pPoolSizes = descriptorPoolSizes,
+    };
+    VK_CHECK(
+        m_vk.vkCreateDescriptorPool(m_vkDevice, &descriptorPoolCi, nullptr, &m_vkDescriptorPool));
+
+    const std::vector<VkDescriptorSetLayout> frameDescriptorSetLayouts(descriptorSetsPerFrame,
+                                                                       m_vkDescriptorSetLayout);
+    const VkDescriptorSetAllocateInfo frameDescriptorSetAllocInfo = {
+        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
+        .descriptorPool = m_vkDescriptorPool,
+        .descriptorSetCount = descriptorSetsPerFrame,
+        .pSetLayouts = frameDescriptorSetLayouts.data(),
+    };
+
+    VkDeviceSize uniformBufferOffset = 0;
+    for (uint32_t frameIndex = 0; frameIndex < m_maxFramesInFlight; ++frameIndex) {
+        PerFrameResources& frameResources = m_frameResources[frameIndex];
+        frameResources.m_layerDescriptorSets.resize(descriptorSetsPerFrame);
+
+        VK_CHECK(m_vk.vkAllocateDescriptorSets(m_vkDevice, &frameDescriptorSetAllocInfo,
+                                               frameResources.m_layerDescriptorSets.data()));
+
+        for (uint32_t layerIndex = 0; layerIndex < kMaxLayersPerFrame; ++layerIndex) {
+            const VkDescriptorBufferInfo bufferInfo = {
+                .buffer = m_uniformStorage.m_vkBuffer,
+                .offset = uniformBufferOffset,
+                .range = sizeof(UniformBufferBinding),
+            };
+            const VkWriteDescriptorSet descriptorSetWrite = {
+                .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+                .dstSet = frameResources.m_layerDescriptorSets[layerIndex],
+                .dstBinding = 1,
+                .dstArrayElement = 0,
+                .descriptorCount = 1,
+                .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+                .pBufferInfo = &bufferInfo,
+            };
+            m_vk.vkUpdateDescriptorSets(m_vkDevice, 1, &descriptorSetWrite, 0, nullptr);
+
+            uniformBufferOffset += m_uniformStorage.m_stride;
+        }
+    }
+}
+
+void CompositorVk::setUpCommandPool() {
+    const VkCommandPoolCreateInfo commandPoolCreateInfo = {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+        .flags = 0,
+        .queueFamilyIndex = m_queueFamilyIndex,
+    };
+
+    VkCommandPool commandPool = VK_NULL_HANDLE;
+    VK_CHECK(m_vk.vkCreateCommandPool(m_vkDevice, &commandPoolCreateInfo, nullptr, &commandPool));
+    m_vkCommandPool = commandPool;
+}
+
+void CompositorVk::setUpFences() {
+    for (uint32_t frameIndex = 0; frameIndex < m_maxFramesInFlight; ++frameIndex) {
+        PerFrameResources& frameResources = m_frameResources[frameIndex];
+
+        const VkFenceCreateInfo fenceCi = {
+            .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+        };
+
+        VkFence fence;
+        VK_CHECK(m_vk.vkCreateFence(m_vkDevice, &fenceCi, nullptr, &fence));
+
+        frameResources.m_vkFence = fence;
+    }
+}
+
+void CompositorVk::setUpFrameResourceFutures() {
+    for (uint32_t frameIndex = 0; frameIndex < m_maxFramesInFlight; ++frameIndex) {
+        std::shared_future<PerFrameResources*> availableFrameResourceFuture =
+            std::async(std::launch::deferred, [this, frameIndex] {
+                return &m_frameResources[frameIndex];
+            }).share();
+
+        m_availableFrameResources.push_back(std::move(availableFrameResourceFuture));
+    }
+}
+
+void CompositorVk::setUpUniformBuffers() {
+    VkPhysicalDeviceProperties physicalDeviceProperties;
+    m_vk.vkGetPhysicalDeviceProperties(m_vkPhysicalDevice, &physicalDeviceProperties);
+    const VkDeviceSize alignment = physicalDeviceProperties.limits.minUniformBufferOffsetAlignment;
+    m_uniformStorage.m_stride = ((sizeof(UniformBufferBinding) - 1) / alignment + 1) * alignment;
+
+    VkDeviceSize size = m_uniformStorage.m_stride * m_maxFramesInFlight * kMaxLayersPerFrame;
+    auto maybeBuffer =
+        createBuffer(size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
+                     VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT |
+                         VK_MEMORY_PROPERTY_HOST_CACHED_BIT);
+    auto buffer = std::make_tuple<VkBuffer, VkDeviceMemory>(VK_NULL_HANDLE, VK_NULL_HANDLE);
+    if (maybeBuffer.has_value()) {
+        buffer = maybeBuffer.value();
+    } else {
+        buffer =
+            createBuffer(size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
+                         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)
+                .value();
+    }
+    std::tie(m_uniformStorage.m_vkBuffer, m_uniformStorage.m_vkDeviceMemory) = buffer;
+
+    void* mapped = nullptr;
+    VK_CHECK(m_vk.vkMapMemory(m_vkDevice, m_uniformStorage.m_vkDeviceMemory, 0, size, 0, &mapped));
+
+    uint8_t* data = reinterpret_cast<uint8_t*>(mapped);
+    for (uint32_t frameIndex = 0; frameIndex < m_maxFramesInFlight; ++frameIndex) {
+        PerFrameResources& frameResources = m_frameResources[frameIndex];
+        for (uint32_t layerIndex = 0; layerIndex < kMaxLayersPerFrame; ++layerIndex) {
+            auto* layerUboStorage = reinterpret_cast<UniformBufferBinding*>(data);
+            frameResources.m_layerUboStorages.push_back(layerUboStorage);
+            data += m_uniformStorage.m_stride;
+        }
+    }
+}
+
+void CompositorVk::setUpSampler() {
+    // The texture coordinate transformation matrices for flip/rotate/etc
+    // currently depends on this being repeat.
+    constexpr const VkSamplerAddressMode kSamplerMode = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+
+    const VkSamplerCreateInfo samplerCi = {
+        .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
+        .magFilter = VK_FILTER_LINEAR,
+        .minFilter = VK_FILTER_LINEAR,
+        .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
+        .addressModeU = kSamplerMode,
+        .addressModeV = kSamplerMode,
+        .addressModeW = kSamplerMode,
+        .mipLodBias = 0.0f,
+        .anisotropyEnable = VK_FALSE,
+        .maxAnisotropy = 1.0f,
+        .compareEnable = VK_FALSE,
+        .compareOp = VK_COMPARE_OP_ALWAYS,
+        .minLod = 0.0f,
+        .maxLod = 0.0f,
+        .borderColor = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK,
+        .unnormalizedCoordinates = VK_FALSE,
+    };
+    VK_CHECK(m_vk.vkCreateSampler(m_vkDevice, &samplerCi, nullptr, &m_vkSampler));
+}
+
 // Create a VkBuffer and a bound VkDeviceMemory. When the specified memory type
 // can't be found, return std::nullopt. When Vulkan call fails, terminate the
 // program.
 std::optional<std::tuple<VkBuffer, VkDeviceMemory>> CompositorVk::createBuffer(
     VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags memProperty) const {
-    VkBufferCreateInfo bufferCi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
-                                   .size = size,
-                                   .usage = usage,
-                                   .sharingMode = VK_SHARING_MODE_EXCLUSIVE};
+    const VkBufferCreateInfo bufferCi = {
+        .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
+        .size = size,
+        .usage = usage,
+        .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+    };
     VkBuffer resBuffer;
     VK_CHECK(m_vk.vkCreateBuffer(m_vkDevice, &bufferCi, nullptr, &resBuffer));
     VkMemoryRequirements memRequirements;
@@ -377,12 +600,15 @@
     m_vk.vkGetPhysicalDeviceMemoryProperties(m_vkPhysicalDevice, &physicalMemProperties);
     auto maybeMemoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, memProperty);
     if (!maybeMemoryTypeIndex.has_value()) {
+        ERR("Failed to find memory type for creating buffer.");
         m_vk.vkDestroyBuffer(m_vkDevice, resBuffer, nullptr);
         return std::nullopt;
     }
-    VkMemoryAllocateInfo memAllocInfo = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
-                                         .allocationSize = memRequirements.size,
-                                         .memoryTypeIndex = maybeMemoryTypeIndex.value()};
+    const VkMemoryAllocateInfo memAllocInfo = {
+        .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+        .allocationSize = memRequirements.size,
+        .memoryTypeIndex = maybeMemoryTypeIndex.value(),
+    };
     VkDeviceMemory resMemory;
     VK_CHECK(m_vk.vkAllocateMemory(m_vkDevice, &memAllocInfo, nullptr, &resMemory));
     VK_CHECK(m_vk.vkBindBufferMemory(m_vkDevice, resBuffer, resMemory, 0));
@@ -412,236 +638,465 @@
     });
 }
 
-void CompositorVk::setUpVertexBuffers() {
-    const VkDeviceSize vertexBufferSize = sizeof(k_vertices[0]) * k_vertices.size();
-    std::tie(m_vertexVkBuffer, m_vertexVkDeviceMemory) =
-        createBuffer(vertexBufferSize,
-                     VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
-                     VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
-            .value();
-    auto [vertexStagingBuffer, vertexStagingBufferMemory] =
-        createStagingBufferWithData(k_vertices.data(), vertexBufferSize);
-    copyBuffer(vertexStagingBuffer, m_vertexVkBuffer, vertexBufferSize);
-    m_vk.vkDestroyBuffer(m_vkDevice, vertexStagingBuffer, nullptr);
-    m_vk.vkFreeMemory(m_vkDevice, vertexStagingBufferMemory, nullptr);
-
-    VkDeviceSize indexBufferSize = sizeof(k_indices[0]) * k_indices.size();
-    auto [indexStagingBuffer, indexStagingBufferMemory] =
-        createStagingBufferWithData(k_indices.data(), indexBufferSize);
-    std::tie(m_indexVkBuffer, m_indexVkDeviceMemory) =
-        createBuffer(indexBufferSize,
-                     VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
-                     VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
-            .value();
-    copyBuffer(indexStagingBuffer, m_indexVkBuffer, indexBufferSize);
-    m_vk.vkDestroyBuffer(m_vkDevice, indexStagingBuffer, nullptr);
-    m_vk.vkFreeMemory(m_vkDevice, indexStagingBufferMemory, nullptr);
+// TODO: move this to another common CRTP helper class in vk_util.h.
+VkFormatFeatureFlags CompositorVk::getFormatFeatures(VkFormat format, VkImageTiling tiling) {
+    auto i = m_vkFormatProperties.find(format);
+    if (i == m_vkFormatProperties.end()) {
+        VkFormatProperties formatProperties;
+        m_vk.vkGetPhysicalDeviceFormatProperties(m_vkPhysicalDevice, format, &formatProperties);
+        i = m_vkFormatProperties.emplace(format, formatProperties).first;
+    }
+    const VkFormatProperties& formatProperties = i->second;
+    VkFormatFeatureFlags formatFeatures = 0;
+    if (tiling == VK_IMAGE_TILING_LINEAR) {
+        formatFeatures = formatProperties.linearTilingFeatures;
+    } else if (tiling == VK_IMAGE_TILING_OPTIMAL) {
+        formatFeatures = formatProperties.optimalTilingFeatures;
+    } else {
+        ERR("Unknown tiling:%#" PRIx64 ".", static_cast<uint64_t>(tiling));
+    }
+    return formatFeatures;
 }
 
-// We do see a composition requests with 12 layers. (b/222700096)
-// Inside hwc2, we will ask for surfaceflinger to
-// do the composition, if the layers more than 16.
-// If we see rendering error or significant time spent on updating
-// descriptors in setComposition, we should tune this number.
-static const uint32_t kMaxLayersPerFrame = 16;
+CompositorVk::RenderTarget* CompositorVk::getOrCreateRenderTargetInfo(
+    const BorrowedImageInfoVk& imageInfo) {
+    auto* renderTargetPtr = m_renderTargetCache.get(imageInfo.id);
+    if (renderTargetPtr != nullptr) {
+        return renderTargetPtr->get();
+    }
 
-void CompositorVk::setUpDescriptorSets() {
-    uint32_t setsPerDescriptorType = m_maxFramesInFlight * kMaxLayersPerFrame;
+    auto* renderTarget = new RenderTarget(m_vk, m_vkDevice, imageInfo.image, imageInfo.imageView,
+                                          imageInfo.imageCreateInfo.extent.width,
+                                          imageInfo.imageCreateInfo.extent.height, m_vkRenderPass);
 
-    VkDescriptorPoolSize descriptorPoolSizes[2] = {
-        {.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
-         .descriptorCount = setsPerDescriptorType},
-        {.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .descriptorCount = setsPerDescriptorType}};
+    m_renderTargetCache.set(imageInfo.id, std::unique_ptr<RenderTarget>(renderTarget));
 
-    VkDescriptorPoolCreateInfo descriptorPoolCi = {
-        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
-        .flags = 0,
-        .maxSets = static_cast<uint32_t>(setsPerDescriptorType),
-        .poolSizeCount = static_cast<uint32_t>(std::size(descriptorPoolSizes)),
-        .pPoolSizes = descriptorPoolSizes};
-    VK_CHECK(
-        m_vk.vkCreateDescriptorPool(m_vkDevice, &descriptorPoolCi, nullptr, &m_vkDescriptorPool));
-    std::vector<VkDescriptorSetLayout> layouts(setsPerDescriptorType, m_vkDescriptorSetLayout);
-    VkDescriptorSetAllocateInfo descriptorSetAllocInfo = {
-        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
-        .descriptorPool = m_vkDescriptorPool,
-        .descriptorSetCount = setsPerDescriptorType,
-        .pSetLayouts = layouts.data()};
-    m_vkDescriptorSets.resize(setsPerDescriptorType);
-    VK_CHECK(m_vk.vkAllocateDescriptorSets(m_vkDevice, &descriptorSetAllocInfo,
-                                           m_vkDescriptorSets.data()));
-    for (size_t i = 0; i < setsPerDescriptorType; i++) {
-        VkDescriptorBufferInfo bufferInfo = {.buffer = m_uniformStorage.m_vkBuffer,
-                                             .offset = i * m_uniformStorage.m_stride,
-                                             .range = sizeof(UniformBufferObject)};
-        VkWriteDescriptorSet descriptorSetWrite = {
-            .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
-            .dstSet = m_vkDescriptorSets[i],
-            .dstBinding = 1,
-            .dstArrayElement = 0,
-            .descriptorCount = 1,
-            .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
-            .pBufferInfo = &bufferInfo};
-        m_vk.vkUpdateDescriptorSets(m_vkDevice, 1, &descriptorSetWrite, 0, nullptr);
+    return renderTarget;
+}
+
+bool CompositorVk::canCompositeFrom(const VkImageCreateInfo& imageCi) {
+    VkFormatFeatureFlags formatFeatures = getFormatFeatures(imageCi.format, imageCi.tiling);
+    if (!(formatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) {
+        ERR("The format, %s, with tiling, %s, doesn't support the "
+            "VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT feature. All supported features are %s.",
+            string_VkFormat(imageCi.format), string_VkImageTiling(imageCi.tiling),
+            string_VkFormatFeatureFlags(formatFeatures).c_str());
+        return false;
+    }
+    return true;
+}
+
+bool CompositorVk::canCompositeTo(const VkImageCreateInfo& imageCi) {
+    VkFormatFeatureFlags formatFeatures = getFormatFeatures(imageCi.format, imageCi.tiling);
+    if (!(formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
+        ERR("The format, %s, with tiling, %s, doesn't support the "
+            "VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT feature. All supported features are %s.",
+            string_VkFormat(imageCi.format), string_VkImageTiling(imageCi.tiling),
+            string_VkFormatFeatureFlags(formatFeatures).c_str());
+        return false;
+    }
+    if (!(imageCi.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)) {
+        ERR("The VkImage is not created with the VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT usage flag. "
+            "The usage flags are %s.",
+            string_VkImageUsageFlags(imageCi.usage).c_str());
+        return false;
+    }
+    if (imageCi.format != k_renderTargetFormat) {
+        ERR("The format of the image, %s, is not supported by the CompositorVk as the render "
+            "target.",
+            string_VkFormat(imageCi.format));
+        return false;
+    }
+    return true;
+}
+
+void CompositorVk::buildCompositionVk(const CompositionRequest& compositionRequest,
+                                      CompositionVk* compositionVk) {
+    const BorrowedImageInfoVk* targetImage = getInfoOrAbort(compositionRequest.target);
+    RenderTarget* targetImageRenderTarget = getOrCreateRenderTargetInfo(*targetImage);
+
+    const uint32_t targetWidth = targetImage->width;
+    const uint32_t targetHeight = targetImage->height;
+
+    compositionVk->targetImage = targetImage;
+    compositionVk->targetFramebuffer = targetImageRenderTarget->m_vkFramebuffer;
+
+    for (const CompositionRequestLayer& layer : compositionRequest.layers) {
+        const BorrowedImageInfoVk* sourceImage = getInfoOrAbort(layer.source);
+        if (!canCompositeFrom(sourceImage->imageCreateInfo)) {
+            continue;
+        }
+
+        const uint32_t sourceImageWidth = sourceImage->width;
+        const uint32_t sourceImageHeight = sourceImage->height;
+
+        // Calculate the posTransform and the texcoordTransform needed in the
+        // uniform of the Compositor.vert shader. The posTransform should transform
+        // the square(top = -1, bottom = 1, left = -1, right = 1) to the position
+        // where the layer should be drawn in NDC space given the layer.
+        // texcoordTransform should transform the unit square(top = 0, bottom = 1,
+        // left = 0, right = 1) to where we should sample the layer in the
+        // normalized uv space given the composeLayer.
+        const hwc_rect_t& posRect = layer.props.displayFrame;
+        const hwc_frect_t& texcoordRect = layer.props.crop;
+
+        const int posWidth = posRect.right - posRect.left;
+        const int posHeight = posRect.bottom - posRect.top;
+
+        const float posScaleX = float(posWidth) / targetWidth;
+        const float posScaleY = float(posHeight) / targetHeight;
+
+        const float posTranslateX = -1.0f + posScaleX + 2.0f * float(posRect.left) / targetWidth;
+        const float posTranslateY = -1.0f + posScaleY + 2.0f * float(posRect.top) / targetHeight;
+
+        float texCoordScaleX = (texcoordRect.right - texcoordRect.left) / float(sourceImageWidth);
+        float texCoordScaleY = (texcoordRect.bottom - texcoordRect.top) / float(sourceImageHeight);
+
+        const float texCoordTranslateX = texcoordRect.left / float(sourceImageWidth);
+        const float texCoordTranslateY = texcoordRect.top / float(sourceImageHeight);
+
+        float texcoordRotation = 0.0f;
+
+        const float pi = glm::pi<float>();
+
+        switch (layer.props.transform) {
+            case HWC_TRANSFORM_NONE:
+                break;
+            case HWC_TRANSFORM_ROT_90:
+                texcoordRotation = pi * 0.5f;
+                break;
+            case HWC_TRANSFORM_ROT_180:
+                texcoordRotation = pi;
+                break;
+            case HWC_TRANSFORM_ROT_270:
+                texcoordRotation = pi * 1.5f;
+                break;
+            case HWC_TRANSFORM_FLIP_H:
+                texCoordScaleX *= -1.0f;
+                break;
+            case HWC_TRANSFORM_FLIP_V:
+                texCoordScaleY *= -1.0f;
+                break;
+            case HWC_TRANSFORM_FLIP_H_ROT_90:
+                texcoordRotation = pi * 0.5f;
+                texCoordScaleX *= -1.0f;
+                break;
+            case HWC_TRANSFORM_FLIP_V_ROT_90:
+                texcoordRotation = pi * 0.5f;
+                texCoordScaleY *= -1.0f;
+                break;
+            default:
+                ERR("Unknown transform:%d", static_cast<int>(layer.props.transform));
+                break;
+        }
+
+        const DescriptorSetContents descriptorSetContents = {
+            .binding0 =
+                {
+                    .sampledImageView = sourceImage->imageView,
+                },
+            .binding1 = {
+                .positionTransform =
+                    glm::translate(glm::mat4(1.0f), glm::vec3(posTranslateX, posTranslateY, 0.0f)) *
+                    glm::scale(glm::mat4(1.0f), glm::vec3(posScaleX, posScaleY, 1.0f)),
+                .texCoordTransform =
+                    glm::translate(glm::mat4(1.0f),
+                                   glm::vec3(texCoordTranslateX, texCoordTranslateY, 0.0f)) *
+                    glm::scale(glm::mat4(1.0f), glm::vec3(texCoordScaleX, texCoordScaleY, 1.0f)) *
+                    glm::rotate(glm::mat4(1.0f), texcoordRotation, glm::vec3(0.0f, 0.0f, 1.0f)),
+            }};
+        compositionVk->layersDescriptorSets.descriptorSets.emplace_back(descriptorSetContents);
+        compositionVk->layersSourceImages.emplace_back(sourceImage);
     }
 }
 
-void CompositorVk::recordCommandBuffers(uint32_t renderTargetIndex, VkCommandBuffer cmdBuffer,
-                                        const CompositorVkRenderTarget& renderTarget) {
-    VkClearValue clearColor = {.color = {.float32 = {0.0f, 0.0f, 0.0f, 1.0f}}};
-    VkRenderPassBeginInfo renderPassBeginInfo = {
-        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
-        .renderPass = m_vkRenderPass,
-        .framebuffer = renderTarget.m_vkFramebuffer,
-        .renderArea = {.offset = {0, 0}, .extent = {renderTarget.m_width, renderTarget.m_height}},
-        .clearValueCount = 1,
-        .pClearValues = &clearColor};
-    m_vk.vkCmdBeginRenderPass(cmdBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
-    m_vk.vkCmdBindPipeline(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_graphicsVkPipeline);
-    VkRect2D scissor = {
-        .offset = {0, 0},
-        .extent =
+CompositorVk::CompositionFinishedWaitable CompositorVk::compose(
+    const CompositionRequest& compositionRequest) {
+    CompositionVk compositionVk;
+    buildCompositionVk(compositionRequest, &compositionVk);
+
+    // Grab and wait for the next available resources.
+    if (m_availableFrameResources.empty()) {
+        GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER))
+            << "CompositorVk failed to get PerFrameResources.";
+    }
+    auto frameResourceFuture = std::move(m_availableFrameResources.front());
+    m_availableFrameResources.pop_front();
+    PerFrameResources* frameResources = frameResourceFuture.get();
+
+    updateDescriptorSetsIfChanged(compositionVk.layersDescriptorSets, frameResources);
+
+    std::vector<VkImageMemoryBarrier> preCompositionQueueTransferBarriers;
+    std::vector<VkImageMemoryBarrier> preCompositionLayoutTransitionBarriers;
+    std::vector<VkImageMemoryBarrier> postCompositionLayoutTransitionBarriers;
+    std::vector<VkImageMemoryBarrier> postCompositionQueueTransferBarriers;
+    addNeededBarriersToUseBorrowedImage(
+        *compositionVk.targetImage, m_queueFamilyIndex, kTargetImageInitialLayoutUsed,
+        kTargetImageFinalLayoutUsed, &preCompositionQueueTransferBarriers,
+        &preCompositionLayoutTransitionBarriers, &postCompositionLayoutTransitionBarriers,
+        &postCompositionQueueTransferBarriers);
+    for (const BorrowedImageInfoVk* sourceImage : compositionVk.layersSourceImages) {
+        addNeededBarriersToUseBorrowedImage(
+            *sourceImage, m_queueFamilyIndex, kSourceImageInitialLayoutUsed,
+            kSourceImageFinalLayoutUsed, &preCompositionQueueTransferBarriers,
+            &preCompositionLayoutTransitionBarriers, &postCompositionLayoutTransitionBarriers,
+            &postCompositionQueueTransferBarriers);
+    }
+
+    VkCommandBuffer& commandBuffer = frameResources->m_vkCommandBuffer;
+    if (commandBuffer != VK_NULL_HANDLE) {
+        m_vk.vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, 1, &commandBuffer);
+    }
+
+    const VkCommandBufferAllocateInfo commandBufferAllocInfo = {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+        .commandPool = m_vkCommandPool,
+        .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+        .commandBufferCount = 1,
+    };
+    VK_CHECK(m_vk.vkAllocateCommandBuffers(m_vkDevice, &commandBufferAllocInfo, &commandBuffer));
+
+    const VkCommandBufferBeginInfo beginInfo = {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
+    };
+    VK_CHECK(m_vk.vkBeginCommandBuffer(commandBuffer, &beginInfo));
+
+    if (!preCompositionQueueTransferBarriers.empty()) {
+        m_vk.vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+                                  VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr,
+                                  static_cast<uint32_t>(preCompositionQueueTransferBarriers.size()),
+                                  preCompositionQueueTransferBarriers.data());
+    }
+    if (!preCompositionLayoutTransitionBarriers.empty()) {
+        m_vk.vkCmdPipelineBarrier(
+            commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+            0, 0, nullptr, 0, nullptr,
+            static_cast<uint32_t>(preCompositionLayoutTransitionBarriers.size()),
+            preCompositionLayoutTransitionBarriers.data());
+    }
+
+    const VkClearValue renderTargetClearColor = {
+        .color =
             {
-                .width = renderTarget.m_width,
-                .height = renderTarget.m_height,
+                .float32 = {0.0f, 0.0f, 0.0f, 1.0f},
             },
     };
-    m_vk.vkCmdSetScissor(cmdBuffer, 0, 1, &scissor);
-    VkViewport viewport = {
+    const VkRenderPassBeginInfo renderPassBeginInfo = {
+        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
+        .renderPass = m_vkRenderPass,
+        .framebuffer = compositionVk.targetFramebuffer,
+        .renderArea =
+            {
+                .offset =
+                    {
+                        .x = 0,
+                        .y = 0,
+                    },
+                .extent =
+                    {
+                        .width = compositionVk.targetImage->imageCreateInfo.extent.width,
+                        .height = compositionVk.targetImage->imageCreateInfo.extent.height,
+                    },
+            },
+        .clearValueCount = 1,
+        .pClearValues = &renderTargetClearColor,
+    };
+    m_vk.vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
+
+    m_vk.vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_graphicsVkPipeline);
+
+    const VkRect2D scissor = {
+        .offset =
+            {
+                .x = 0,
+                .y = 0,
+            },
+        .extent =
+            {
+                .width = compositionVk.targetImage->imageCreateInfo.extent.width,
+                .height = compositionVk.targetImage->imageCreateInfo.extent.height,
+            },
+    };
+    m_vk.vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
+
+    const VkViewport viewport = {
         .x = 0.0f,
         .y = 0.0f,
-        .width = static_cast<float>(renderTarget.m_width),
-        .height = static_cast<float>(renderTarget.m_height),
+        .width = static_cast<float>(compositionVk.targetImage->imageCreateInfo.extent.width),
+        .height = static_cast<float>(compositionVk.targetImage->imageCreateInfo.extent.height),
         .minDepth = 0.0f,
         .maxDepth = 1.0f,
     };
-    m_vk.vkCmdSetViewport(cmdBuffer, 0, 1, &viewport);
-    VkDeviceSize offsets[] = {0};
-    m_vk.vkCmdBindVertexBuffers(cmdBuffer, 0, 1, &m_vertexVkBuffer, offsets);
-    m_vk.vkCmdBindIndexBuffer(cmdBuffer, m_indexVkBuffer, 0, VK_INDEX_TYPE_UINT16);
-    for (uint32_t j = 0; j < m_currentCompositions[renderTargetIndex]->m_composeLayers.size();
-         ++j) {
-        m_vk.vkCmdBindDescriptorSets(
-            cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_vkPipelineLayout, 0, 1,
-            &m_vkDescriptorSets[renderTargetIndex * kMaxLayersPerFrame + j], 0, nullptr);
-        m_vk.vkCmdDrawIndexed(cmdBuffer, static_cast<uint32_t>(k_indices.size()), 1, 0, 0, 0);
+    m_vk.vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
+
+    const VkDeviceSize offsets[] = {0};
+    m_vk.vkCmdBindVertexBuffers(commandBuffer, 0, 1, &m_vertexVkBuffer, offsets);
+
+    m_vk.vkCmdBindIndexBuffer(commandBuffer, m_indexVkBuffer, 0, VK_INDEX_TYPE_UINT16);
+
+    for (int layerIndex = 0; layerIndex < compositionVk.layersSourceImages.size(); ++layerIndex) {
+        VkDescriptorSet layerDescriptorSet = frameResources->m_layerDescriptorSets[layerIndex];
+
+        m_vk.vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
+                                     m_vkPipelineLayout,
+                                     /*firstSet=*/0,
+                                     /*descriptorSetCount=*/1, &layerDescriptorSet,
+                                     /*dynamicOffsetCount=*/0,
+                                     /*pDynamicOffsets=*/nullptr);
+
+        m_vk.vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(k_indices.size()), 1, 0, 0, 0);
     }
-    m_vk.vkCmdEndRenderPass(cmdBuffer);
-}
 
-void CompositorVk::setUpUniformBuffers() {
-    VkDeviceSize size = m_uniformStorage.m_stride * m_maxFramesInFlight * kMaxLayersPerFrame;
-    auto maybeBuffer =
-        createBuffer(size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
-                     VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT |
-                         VK_MEMORY_PROPERTY_HOST_CACHED_BIT);
-    auto buffer = std::make_tuple<VkBuffer, VkDeviceMemory>(VK_NULL_HANDLE, VK_NULL_HANDLE);
-    if (maybeBuffer.has_value()) {
-        buffer = maybeBuffer.value();
-    } else {
-        buffer =
-            createBuffer(size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
-                         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)
-                .value();
+    m_vk.vkCmdEndRenderPass(commandBuffer);
+
+    // Insert a VkImageMemoryBarrier so that the vkCmdBlitImage in post will wait for the rendering
+    // to the render target to complete.
+    const VkImageMemoryBarrier renderTargetBarrier = {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+        .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
+        .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT,
+        .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+        .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+        .image = compositionVk.targetImage->image,
+        .subresourceRange =
+            {
+                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                .baseMipLevel = 0,
+                .levelCount = 1,
+                .baseArrayLayer = 0,
+                .layerCount = 1,
+            },
+    };
+    m_vk.vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
+                              VK_PIPELINE_STAGE_TRANSFER_BIT,
+                              /*dependencyFlags=*/0,
+                              /*memoryBarrierCount=*/0,
+                              /*pMemoryBarriers=*/nullptr,
+                              /*bufferMemoryBarrierCount=*/0,
+                              /*pBufferMemoryBarriers=*/nullptr, 1, &renderTargetBarrier);
+
+    if (!postCompositionLayoutTransitionBarriers.empty()) {
+        m_vk.vkCmdPipelineBarrier(
+            commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+            0, 0, nullptr, 0, nullptr,
+            static_cast<uint32_t>(postCompositionLayoutTransitionBarriers.size()),
+            postCompositionLayoutTransitionBarriers.data());
     }
-    std::tie(m_uniformStorage.m_vkBuffer, m_uniformStorage.m_vkDeviceMemory) = buffer;
-    VK_CHECK(m_vk.vkMapMemory(m_vkDevice, m_uniformStorage.m_vkDeviceMemory, 0, size, 0,
-                              reinterpret_cast<void**>(&m_uniformStorage.m_data)));
+    if (!postCompositionQueueTransferBarriers.empty()) {
+        m_vk.vkCmdPipelineBarrier(
+            commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+            0, 0, nullptr, 0, nullptr,
+            static_cast<uint32_t>(postCompositionQueueTransferBarriers.size()),
+            postCompositionQueueTransferBarriers.data());
+    }
+
+    VK_CHECK(m_vk.vkEndCommandBuffer(commandBuffer));
+
+    VkFence composeCompleteFence = frameResources->m_vkFence;
+    VK_CHECK(m_vk.vkResetFences(m_vkDevice, 1, &composeCompleteFence));
+
+    const VkPipelineStageFlags submitWaitStages[] = {
+        VK_PIPELINE_STAGE_TRANSFER_BIT,
+    };
+    const VkSubmitInfo submitInfo = {
+        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+        .waitSemaphoreCount = 0,
+        .pWaitSemaphores = nullptr,
+        .pWaitDstStageMask = submitWaitStages,
+        .commandBufferCount = 1,
+        .pCommandBuffers = &commandBuffer,
+        .signalSemaphoreCount = 0,
+        .pSignalSemaphores = nullptr,
+    };
+
+    {
+        android::base::AutoLock lock(*m_vkQueueLock);
+        VK_CHECK(m_vk.vkQueueSubmit(m_vkQueue, 1, &submitInfo, composeCompleteFence));
+    }
+
+    // Create a future that will return the PerFrameResources to the next
+    // iteration of CompostiorVk::compose() once this current composition
+    // completes.
+    std::shared_future<PerFrameResources*> composeCompleteFutureForResources =
+        std::async(std::launch::deferred, [composeCompleteFence, frameResources, this]() mutable {
+            VK_CHECK(
+                m_vk.vkWaitForFences(m_vkDevice, 1, &composeCompleteFence, VK_TRUE, UINT64_MAX));
+            return frameResources;
+        }).share();
+    m_availableFrameResources.push_back(composeCompleteFutureForResources);
+
+    // Create a future that will return once this current composition
+    // completes that can be shared outside of CompositorVk.
+    std::shared_future<void> composeCompleteFuture =
+        std::async(std::launch::deferred, [composeCompleteFutureForResources]() {
+            composeCompleteFutureForResources.get();
+        }).share();
+
+    return composeCompleteFuture;
 }
 
-bool CompositorVk::validateQueueFamilyProperties(const VkQueueFamilyProperties& properties) {
-    return properties.queueFlags & VK_QUEUE_GRAPHICS_BIT;
+void CompositorVk::onImageDestroyed(uint32_t imageId) { m_renderTargetCache.remove(imageId); }
+
+bool operator==(const CompositorVkBase::DescriptorSetContents& lhs,
+                const CompositorVkBase::DescriptorSetContents& rhs) {
+    return std::tie(lhs.binding0.sampledImageView, lhs.binding1.positionTransform,
+                    lhs.binding1.texCoordTransform) == std::tie(rhs.binding0.sampledImageView,
+                                                                rhs.binding1.positionTransform,
+                                                                rhs.binding1.texCoordTransform);
 }
 
-void CompositorVk::setComposition(uint32_t rtIndex, std::unique_ptr<Composition>&& composition) {
-    m_currentCompositions[rtIndex] = std::move(composition);
-    const auto& currentComposition = *m_currentCompositions[rtIndex];
-    if (currentComposition.m_composeLayers.size() > kMaxLayersPerFrame) {
+bool operator==(const CompositorVkBase::FrameDescriptorSetsContents& lhs,
+                const CompositorVkBase::FrameDescriptorSetsContents& rhs) {
+    return lhs.descriptorSets == rhs.descriptorSets;
+}
+
+void CompositorVk::updateDescriptorSetsIfChanged(
+    const FrameDescriptorSetsContents& descriptorSetsContents, PerFrameResources* frameResources) {
+    if (frameResources->m_vkDescriptorSetsContents == descriptorSetsContents) {
+        return;
+    }
+
+    const uint32_t numRequestedLayers =
+        static_cast<uint32_t>(descriptorSetsContents.descriptorSets.size());
+    if (numRequestedLayers > kMaxLayersPerFrame) {
         GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER))
             << "CompositorVk can't compose more than " << kMaxLayersPerFrame
-            << " layers. layers asked: "
-            << static_cast<uint32_t>(currentComposition.m_composeLayers.size());
+            << " layers. layers asked: " << numRequestedLayers;
+        return;
     }
 
-    memset(reinterpret_cast<uint8_t*>(m_uniformStorage.m_data) +
-               (rtIndex * kMaxLayersPerFrame + 0) * m_uniformStorage.m_stride,
-           0, sizeof(ComposeLayerVk::LayerTransform) * kMaxLayersPerFrame);
-
-    std::vector<VkDescriptorImageInfo> imageInfos(currentComposition.m_composeLayers.size());
+    std::vector<VkDescriptorImageInfo> descriptorImageInfos(numRequestedLayers);
     std::vector<VkWriteDescriptorSet> descriptorWrites;
-    for (size_t i = 0; i < currentComposition.m_composeLayers.size(); ++i) {
-        const auto& layer = currentComposition.m_composeLayers[i];
-        if (m_vkSampler != layer->m_vkSampler) {
-            GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER))
-                << "Unsupported sampler(" << reinterpret_cast<uintptr_t>(layer->m_vkSampler)
-                << ").";
-        }
-        imageInfos[i] =
-            VkDescriptorImageInfo({.sampler = VK_NULL_HANDLE,
-                                   .imageView = layer->m_vkImageView,
-                                   .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL});
-        const VkDescriptorImageInfo& imageInfo = imageInfos[i];
-        descriptorWrites.emplace_back(
-            VkWriteDescriptorSet{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
-                                 .dstSet = m_vkDescriptorSets[rtIndex * kMaxLayersPerFrame + i],
-                                 .dstBinding = 0,
-                                 .dstArrayElement = 0,
-                                 .descriptorCount = 1,
-                                 .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
-                                 .pImageInfo = &imageInfo});
-        memcpy(reinterpret_cast<uint8_t*>(m_uniformStorage.m_data) +
-                   (rtIndex * kMaxLayersPerFrame + i) * m_uniformStorage.m_stride,
-               &layer->m_layerTransform, sizeof(ComposeLayerVk::LayerTransform));
+    for (uint32_t layerIndex = 0; layerIndex < numRequestedLayers; ++layerIndex) {
+        const DescriptorSetContents& layerDescriptorSetContents =
+            descriptorSetsContents.descriptorSets[layerIndex];
+
+        descriptorImageInfos[layerIndex] = VkDescriptorImageInfo{
+            // Empty as we only use immutable samplers.
+            .sampler = VK_NULL_HANDLE,
+            .imageView = layerDescriptorSetContents.binding0.sampledImageView,
+            .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+        };
+
+        descriptorWrites.emplace_back(VkWriteDescriptorSet{
+            .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+            .dstSet = frameResources->m_layerDescriptorSets[layerIndex],
+            .dstBinding = 0,
+            .dstArrayElement = 0,
+            .descriptorCount = 1,
+            .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+            .pImageInfo = &descriptorImageInfos[layerIndex],
+        });
+
+        UniformBufferBinding* layerUboStorage = frameResources->m_layerUboStorages[layerIndex];
+        *layerUboStorage = layerDescriptorSetContents.binding1;
     }
+
     m_vk.vkUpdateDescriptorSets(m_vkDevice, descriptorWrites.size(), descriptorWrites.data(), 0,
                                 nullptr);
-}
 
-std::unique_ptr<CompositorVkRenderTarget> CompositorVk::createRenderTarget(VkImageView vkImageView,
-                                                                           uint32_t width,
-                                                                           uint32_t height) {
-    return std::unique_ptr<CompositorVkRenderTarget>(
-        new CompositorVkRenderTarget(m_vk, m_vkDevice, vkImageView, width, height, m_vkRenderPass));
+    frameResources->m_vkDescriptorSetsContents = descriptorSetsContents;
 }
-
-VkVertexInputBindingDescription CompositorVk::Vertex::getBindingDescription() {
-    return {
-        .binding = 0, .stride = sizeof(struct Vertex), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX};
-}
-
-std::array<VkVertexInputAttributeDescription, 2> CompositorVk::Vertex::getAttributeDescription() {
-    return {VkVertexInputAttributeDescription{.location = 0,
-                                              .binding = 0,
-                                              .format = VK_FORMAT_R32G32_SFLOAT,
-                                              .offset = offsetof(struct Vertex, pos)},
-            VkVertexInputAttributeDescription{.location = 1,
-                                              .binding = 0,
-                                              .format = VK_FORMAT_R32G32_SFLOAT,
-                                              .offset = offsetof(struct Vertex, texPos)}};
-}
-
-CompositorVkRenderTarget::CompositorVkRenderTarget(const goldfish_vk::VulkanDispatch& vk,
-                                                   VkDevice vkDevice, VkImageView vkImageView,
-                                                   uint32_t width, uint32_t height,
-                                                   VkRenderPass vkRenderPass)
-    : m_vk(vk),
-      m_vkDevice(vkDevice),
-      m_vkFramebuffer(VK_NULL_HANDLE),
-      m_width(width),
-      m_height(height) {
-    VkFramebufferCreateInfo framebufferCi = {
-        .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
-        .flags = 0,
-        .renderPass = vkRenderPass,
-        .attachmentCount = 1,
-        .pAttachments = &vkImageView,
-        .width = width,
-        .height = height,
-        .layers = 1,
-    };
-    VK_CHECK(m_vk.vkCreateFramebuffer(vkDevice, &framebufferCi, nullptr, &m_vkFramebuffer));
-}
-
-CompositorVkRenderTarget::~CompositorVkRenderTarget() {
-    m_vk.vkDestroyFramebuffer(m_vkDevice, m_vkFramebuffer, nullptr);
-}
\ No newline at end of file
diff --git a/stream-servers/vulkan/CompositorVk.h b/stream-servers/vulkan/CompositorVk.h
index 2216c61..182076f 100644
--- a/stream-servers/vulkan/CompositorVk.h
+++ b/stream-servers/vulkan/CompositorVk.h
@@ -2,57 +2,41 @@
 #define COMPOSITOR_VK_H
 
 #include <array>
+#include <deque>
+#include <future>
 #include <glm/glm.hpp>
+#include <list>
 #include <memory>
 #include <optional>
 #include <tuple>
-#include <variant>
+#include <unordered_map>
 #include <vector>
 
+#include "BorrowedImage.h"
+#include "BorrowedImageVk.h"
+#include "Compositor.h"
 #include "Hwc2.h"
 #include "base/Lock.h"
+#include "base/LruCache.h"
 #include "vulkan/cereal/common/goldfish_vk_dispatch.h"
 #include "vulkan/vk_util.h"
 
-class ComposeLayerVk {
-   public:
-    VkSampler m_vkSampler;
-    VkImageView m_vkImageView;
-    struct LayerTransform {
-        glm::mat4 pos;
-        glm::mat4 texcoord;
-    } m_layerTransform;
+// We do see a composition requests with 12 layers. (b/222700096)
+// Inside hwc2, we will ask for surfaceflinger to
+// do the composition, if the layers more than 16.
+// If we see rendering error or significant time spent on updating
+// descriptors in setComposition, we should tune this number.
+static constexpr const uint32_t kMaxLayersPerFrame = 16;
 
-    static std::unique_ptr<ComposeLayerVk> createFromHwc2ComposeLayer(
-        VkSampler, VkImageView, const ComposeLayer&, uint32_t cbWidth, uint32_t cbHeight,
-        uint32_t dstWidth, uint32_t dstHeight);
-
-   private:
-    ComposeLayerVk() = delete;
-    explicit ComposeLayerVk(VkSampler, VkImageView, const LayerTransform&);
-};
-
-// If we want to apply transform to all layers to rotate/clip/position the
-// virtual display, we should add that functionality here.
-class Composition {
-   public:
-    std::vector<std::unique_ptr<ComposeLayerVk>> m_composeLayers;
-
-    Composition() = delete;
-    explicit Composition(std::vector<std::unique_ptr<ComposeLayerVk>> composeLayers);
-};
-
-class CompositorVkRenderTarget;
-
+// Base used to grant visibility to members to the vk_util::* helper classes.
 struct CompositorVkBase
-    : public vk_util::RunSingleTimeCommand<
-          CompositorVkBase,
-          vk_util::FindMemoryType<CompositorVkBase,
-                                  vk_util::RecordImageLayoutTransformCommands<CompositorVkBase>>> {
+    : public vk_util::RunSingleTimeCommand<CompositorVkBase,
+                                           vk_util::FindMemoryType<CompositorVkBase>> {
     const goldfish_vk::VulkanDispatch& m_vk;
     const VkDevice m_vkDevice;
     const VkPhysicalDevice m_vkPhysicalDevice;
     const VkQueue m_vkQueue;
+    const uint32_t m_queueFamilyIndex;
     std::shared_ptr<android::base::Lock> m_vkQueueLock;
     VkDescriptorSetLayout m_vkDescriptorSetLayout;
     VkPipelineLayout m_vkPipelineLayout;
@@ -63,19 +47,66 @@
     VkBuffer m_indexVkBuffer;
     VkDeviceMemory m_indexVkDeviceMemory;
     VkDescriptorPool m_vkDescriptorPool;
-    std::vector<VkDescriptorSet> m_vkDescriptorSets;
-
     VkCommandPool m_vkCommandPool;
+    // TODO: create additional VkSampler-s for YCbCr layers.
+    VkSampler m_vkSampler;
+
+    // The underlying storage for all of the uniform buffer objects.
+    struct UniformBufferStorage {
+        VkBuffer m_vkBuffer = VK_NULL_HANDLE;
+        VkDeviceMemory m_vkDeviceMemory = VK_NULL_HANDLE;
+        VkDeviceSize m_stride = 0;
+    } m_uniformStorage;
+
+    // Keep in sync with vulkan/Compositor.frag.
+    struct SamplerBinding {
+        VkImageView sampledImageView = VK_NULL_HANDLE;
+    };
+
+    // Keep in sync with vulkan/Compositor.vert.
+    struct UniformBufferBinding {
+        alignas(16) glm::mat4 positionTransform;
+        alignas(16) glm::mat4 texCoordTransform;
+    };
+
+    // The cached contents of a given descriptor set.
+    struct DescriptorSetContents {
+        SamplerBinding binding0;
+        UniformBufferBinding binding1;
+    };
+
+    // The cached contents of all descriptors sets of a given frame.
+    struct FrameDescriptorSetsContents {
+        std::vector<DescriptorSetContents> descriptorSets;
+    };
+
+    friend bool operator==(const DescriptorSetContents& lhs, const DescriptorSetContents& rhs);
+
+    friend bool operator==(const FrameDescriptorSetsContents& lhs,
+                           const FrameDescriptorSetsContents& rhs);
+
+    struct PerFrameResources {
+        VkFence m_vkFence = VK_NULL_HANDLE;
+        VkCommandBuffer m_vkCommandBuffer = VK_NULL_HANDLE;
+        std::vector<VkDescriptorSet> m_layerDescriptorSets;
+        // Pointers into the underlying uniform buffer storage for the uniform
+        // buffer of part of each descriptor set for each layer.
+        std::vector<UniformBufferBinding*> m_layerUboStorages;
+        std::optional<FrameDescriptorSetsContents> m_vkDescriptorSetsContents;
+    };
+    std::vector<PerFrameResources> m_frameResources;
+    std::deque<std::shared_future<PerFrameResources*>> m_availableFrameResources;
 
     explicit CompositorVkBase(const goldfish_vk::VulkanDispatch& vk, VkDevice device,
                               VkPhysicalDevice physicalDevice, VkQueue queue,
                               std::shared_ptr<android::base::Lock> queueLock,
-                              VkCommandPool commandPool)
+                              uint32_t queueFamilyIndex, uint32_t maxFramesInFlight)
         : m_vk(vk),
           m_vkDevice(device),
           m_vkPhysicalDevice(physicalDevice),
           m_vkQueue(queue),
           m_vkQueueLock(queueLock),
+          m_queueFamilyIndex(queueFamilyIndex),
           m_vkDescriptorSetLayout(VK_NULL_HANDLE),
           m_vkPipelineLayout(VK_NULL_HANDLE),
           m_vkRenderPass(VK_NULL_HANDLE),
@@ -85,35 +116,43 @@
           m_indexVkBuffer(VK_NULL_HANDLE),
           m_indexVkDeviceMemory(VK_NULL_HANDLE),
           m_vkDescriptorPool(VK_NULL_HANDLE),
-          m_vkDescriptorSets(0),
-          m_vkCommandPool(commandPool) {}
+          m_vkCommandPool(VK_NULL_HANDLE),
+          m_vkSampler(VK_NULL_HANDLE),
+          m_frameResources(maxFramesInFlight) {}
 };
 
-class CompositorVk : protected CompositorVkBase {
+class CompositorVk : protected CompositorVkBase, public Compositor {
    public:
-    static std::unique_ptr<CompositorVk> create(
-        const goldfish_vk::VulkanDispatch& vk, VkDevice, VkPhysicalDevice, VkQueue,
-        std::shared_ptr<android::base::Lock> queueLock, VkFormat, VkImageLayout initialLayout,
-        VkImageLayout finalLayout, uint32_t maxFramesInFlight, VkCommandPool, VkSampler);
-    static bool validateQueueFamilyProperties(const VkQueueFamilyProperties& properties);
+    static std::unique_ptr<CompositorVk> create(const goldfish_vk::VulkanDispatch& vk,
+                                                VkDevice vkDevice,
+                                                VkPhysicalDevice vkPhysicalDevice, VkQueue vkQueue,
+                                                std::shared_ptr<android::base::Lock> queueLock,
+                                                uint32_t queueFamilyIndex,
+                                                uint32_t maxFramesInFlight);
 
     ~CompositorVk();
-    void recordCommandBuffers(uint32_t renderTargetIndex, VkCommandBuffer,
-                              const CompositorVkRenderTarget&);
-    void setComposition(uint32_t i, std::unique_ptr<Composition>&& composition);
-    std::unique_ptr<CompositorVkRenderTarget> createRenderTarget(VkImageView, uint32_t width,
-                                                                 uint32_t height);
+
+    CompositionFinishedWaitable compose(const CompositionRequest& compositionRequest) override;
+
+    void onImageDestroyed(uint32_t imageId) override;
+
+    static bool queueSupportsComposition(const VkQueueFamilyProperties& properties) {
+        return properties.queueFlags & VK_QUEUE_GRAPHICS_BIT;
+    }
 
    private:
     explicit CompositorVk(const goldfish_vk::VulkanDispatch&, VkDevice, VkPhysicalDevice, VkQueue,
-                          std::shared_ptr<android::base::Lock> queueLock, VkCommandPool,
+                          std::shared_ptr<android::base::Lock> queueLock, uint32_t queueFamilyIndex,
                           uint32_t maxFramesInFlight);
-    void setUpGraphicsPipeline(VkFormat renderTargetFormat, VkImageLayout initialLayout,
-                               VkImageLayout finalLayout, VkSampler);
+
+    void setUpGraphicsPipeline();
     void setUpVertexBuffers();
+    void setUpSampler();
     void setUpDescriptorSets();
-    void setUpEmptyComposition(VkFormat);
     void setUpUniformBuffers();
+    void setUpCommandPool();
+    void setUpFences();
+    void setUpFrameResourceFutures();
 
     std::optional<std::tuple<VkBuffer, VkDeviceMemory>> createBuffer(VkDeviceSize,
                                                                      VkBufferUsageFlags,
@@ -122,47 +161,73 @@
                                                                      VkDeviceSize size) const;
     void copyBuffer(VkBuffer src, VkBuffer dst, VkDeviceSize) const;
 
-    struct UniformBufferObject {
-        alignas(16) glm::mat4 pos_transform;
-        alignas(16) glm::mat4 texcoord_transform;
+    VkFormatFeatureFlags getFormatFeatures(VkFormat format, VkImageTiling tiling);
+
+    // Check if the ColorBuffer can be used as a compose layer to be sampled from.
+    bool canCompositeFrom(const VkImageCreateInfo& info);
+
+    // Check if the ColorBuffer can be used as a render target of a composition.
+    bool canCompositeTo(const VkImageCreateInfo& info);
+
+    // A consolidated view of a `Compositor::CompositionRequestLayer` with only
+    // the Vulkan components needed for command recording and submission.
+    struct CompositionLayerVk {
+        VkImage image = VK_NULL_HANDLE;
+        VkImageView imageView = VK_NULL_HANDLE;
+        VkImageLayout preCompositionLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+        uint32_t preCompositionQueueFamilyIndex = 0;
+        VkImageLayout postCompositionLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+        uint32_t postCompositionQueueFamilyIndex = 0;
     };
 
-    struct Vertex {
-        alignas(8) glm::vec2 pos;
-        alignas(8) glm::vec2 texPos;
+    // A consolidated view of a `Compositor::CompositionRequest` with only
+    // the Vulkan components needed for command recording and submission.
+    struct CompositionVk {
+        const BorrowedImageInfoVk* targetImage = nullptr;
+        VkFramebuffer targetFramebuffer = VK_NULL_HANDLE;
+        std::vector<const BorrowedImageInfoVk*> layersSourceImages;
+        FrameDescriptorSetsContents layersDescriptorSets;
+    };
+    void buildCompositionVk(const CompositionRequest& compositionRequest,
+                            CompositionVk* compositionVk);
 
-        static VkVertexInputBindingDescription getBindingDescription();
-        static std::array<VkVertexInputAttributeDescription, 2> getAttributeDescription();
+    void updateDescriptorSetsIfChanged(const FrameDescriptorSetsContents& contents,
+                                       PerFrameResources* frameResources);
+
+    class RenderTarget {
+       public:
+        ~RenderTarget();
+
+        DISALLOW_COPY_ASSIGN_AND_MOVE(RenderTarget);
+
+       private:
+        friend class CompositorVk;
+        RenderTarget(const goldfish_vk::VulkanDispatch& vk, VkDevice vkDevice, VkImage vkImage,
+                     VkImageView vkImageView, uint32_t width, uint32_t height,
+                     VkRenderPass vkRenderPass);
+
+        const goldfish_vk::VulkanDispatch& m_vk;
+        VkDevice m_vkDevice;
+        VkImage m_vkImage;
+        VkFramebuffer m_vkFramebuffer;
+        uint32_t m_width;
+        uint32_t m_height;
     };
 
-    static const std::vector<Vertex> k_vertices;
-    static const std::vector<uint16_t> k_indices;
+    // Gets the RenderTarget used for composing into the given image if it already exists,
+    // otherwise creates it.
+    RenderTarget* getOrCreateRenderTargetInfo(const BorrowedImageInfoVk& info);
 
-    uint32_t m_maxFramesInFlight;
-    VkSampler m_vkSampler;
+    // Cached format properties used for checking if composition is supported with a given
+    // format.
+    std::unordered_map<VkFormat, VkFormatProperties> m_vkFormatProperties;
 
-    std::vector<std::unique_ptr<Composition>> m_currentCompositions;
-    struct UniformStorage {
-        VkBuffer m_vkBuffer;
-        VkDeviceMemory m_vkDeviceMemory;
-        void* m_data;
-        VkDeviceSize m_stride;
-    } m_uniformStorage;
-};
+    uint32_t m_maxFramesInFlight = 0;
 
-class CompositorVkRenderTarget {
-   public:
-    ~CompositorVkRenderTarget();
-
-   private:
-    const goldfish_vk::VulkanDispatch& m_vk;
-    VkDevice m_vkDevice;
-    VkFramebuffer m_vkFramebuffer;
-    uint32_t m_width;
-    uint32_t m_height;
-    CompositorVkRenderTarget(const goldfish_vk::VulkanDispatch&, VkDevice, VkImageView,
-                             uint32_t width, uint32_t height, VkRenderPass);
-    friend class CompositorVk;
+    static constexpr const VkFormat k_renderTargetFormat = VK_FORMAT_R8G8B8A8_UNORM;
+    static constexpr const uint32_t k_renderTargetCacheSize = 128;
+    // Maps from borrowed image ids to render target info.
+    android::base::LruCache<uint32_t, std::unique_ptr<RenderTarget>> m_renderTargetCache;
 };
 
 #endif /* COMPOSITOR_VK_H */
diff --git a/stream-servers/vulkan/DisplayVk.cpp b/stream-servers/vulkan/DisplayVk.cpp
index 0550c08..5c240b1 100644
--- a/stream-servers/vulkan/DisplayVk.cpp
+++ b/stream-servers/vulkan/DisplayVk.cpp
@@ -6,7 +6,6 @@
 
 #include "host-common/GfxstreamFatalError.h"
 #include "host-common/logging.h"
-#include "vulkan/VkCommonOperations.h"
 #include "vulkan/VkFormatUtils.h"
 #include "vulkan/vk_enum_string_helper.h"
 
@@ -63,7 +62,6 @@
       m_swapChainVkQueueLock(swapChainVkQueueLock),
       m_vkCommandPool(VK_NULL_HANDLE),
       m_swapChainStateVk(nullptr),
-      m_compositorVk(nullptr),
       m_surfaceState(nullptr) {
     // TODO(kaiyili): validate the capabilites of the passed in Vulkan
     // components.
@@ -73,24 +71,6 @@
         .queueFamilyIndex = m_compositorQueueFamilyIndex,
     };
     VK_CHECK(m_vk.vkCreateCommandPool(m_vkDevice, &commandPoolCi, nullptr, &m_vkCommandPool));
-
-    VkSamplerCreateInfo samplerCi = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
-                                     .magFilter = VK_FILTER_LINEAR,
-                                     .minFilter = VK_FILTER_LINEAR,
-                                     .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
-                                     .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
-                                     .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
-                                     .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
-                                     .mipLodBias = 0.0f,
-                                     .anisotropyEnable = VK_FALSE,
-                                     .maxAnisotropy = 1.0f,
-                                     .compareEnable = VK_FALSE,
-                                     .compareOp = VK_COMPARE_OP_ALWAYS,
-                                     .minLod = 0.0f,
-                                     .maxLod = 0.0f,
-                                     .borderColor = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK,
-                                     .unnormalizedCoordinates = VK_FALSE};
-    VK_CHECK(m_vk.vkCreateSampler(m_vkDevice, &samplerCi, nullptr, &m_compositionVkSampler));
 }
 
 DisplayVk::~DisplayVk() {
@@ -103,11 +83,7 @@
         VK_CHECK(vk_util::waitForVkQueueIdleWithRetry(m_vk, m_compositorVkQueue));
     }
     m_postResourceFuture = std::nullopt;
-    m_composeResourceFuture = std::nullopt;
-    m_compositorVkRenderTargets.clear();
-    m_vk.vkDestroySampler(m_vkDevice, m_compositionVkSampler, nullptr);
     m_surfaceState.reset();
-    m_compositorVk.reset();
     m_swapChainStateVk.reset();
     m_vk.vkDestroyCommandPool(m_vkDevice, m_vkCommandPool, nullptr);
 }
@@ -122,10 +98,6 @@
         VK_CHECK(vk_util::waitForVkQueueIdleWithRetry(m_vk, m_swapChainVkQueue));
     }
     m_postResourceFuture = std::nullopt;
-    m_composeResourceFuture = std::nullopt;
-    m_compositorVkRenderTargets = std::deque<std::shared_ptr<CompositorVkRenderTarget>>(
-        k_compositorVkRenderTargetCacheSize, nullptr);
-    m_compositorVk.reset();
     m_swapChainStateVk.reset();
 
     if (!SwapChainStateVk::validateQueueFamilyProperties(m_vk, m_vkPhysicalDevice, surface,
@@ -150,11 +122,6 @@
     }
     m_swapChainStateVk =
         std::make_unique<SwapChainStateVk>(m_vk, m_vkDevice, swapChainCi->mCreateInfo);
-    m_compositorVk = CompositorVk::create(
-        m_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue, m_compositorVkQueueLock,
-        k_compositorVkRenderTargetFormat, VK_IMAGE_LAYOUT_UNDEFINED,
-        VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_swapChainStateVk->getVkImageViews().size(),
-        m_vkCommandPool, m_compositionVkSampler);
 
     int numSwapChainImages = m_swapChainStateVk->getVkImages().size();
 
@@ -165,35 +132,24 @@
 
     m_inFlightFrameIndex = 0;
 
-    m_composeResourceFuture = std::async(std::launch::deferred, [this] {
-        return ComposeResource::create(m_vk, m_vkDevice, m_vkCommandPool);
-    });
-    m_composeResourceFuture.value().wait();
     auto surfaceState = std::make_unique<SurfaceState>();
     surfaceState->m_height = height;
     surfaceState->m_width = width;
     m_surfaceState = std::move(surfaceState);
 }
 
-std::shared_ptr<DisplayVk::DisplayBufferInfo> DisplayVk::createDisplayBuffer(
-    VkImage image, const VkImageCreateInfo& vkImageCreateInfo) {
-    return std::shared_ptr<DisplayBufferInfo>(
-        new DisplayBufferInfo(m_vk, m_vkDevice, vkImageCreateInfo, image));
-}
-
 std::tuple<bool, std::shared_future<void>> DisplayVk::post(
-    std::shared_ptr<DisplayBufferInfo> displayBufferPtr) {
+    const BorrowedImageInfo* sourceImageInfo) {
     auto completedFuture = std::async(std::launch::deferred, [] {}).share();
     completedFuture.wait();
-    if (!displayBufferPtr) {
-        fprintf(stderr, "%s: warning: null ptr passed to post buffer\n", __func__);
-        return std::make_tuple(true, std::move(completedFuture));
-    }
+
     if (!m_swapChainStateVk || !m_surfaceState) {
         DISPLAY_VK_ERROR("Haven't bound to a surface, can't post ColorBuffer.");
         return std::make_tuple(true, std::move(completedFuture));
     }
-    if (!canPost(displayBufferPtr->m_vkImageCreateInfo)) {
+
+    const auto* sourceImageInfoVk = static_cast<const BorrowedImageInfoVk*>(sourceImageInfo);
+    if (!canPost(sourceImageInfoVk->imageCreateInfo)) {
         DISPLAY_VK_ERROR("Can't post ColorBuffer.");
         return std::make_tuple(true, std::move(completedFuture));
     }
@@ -212,37 +168,81 @@
 
     VkCommandBuffer cmdBuff = postResource->m_vkCommandBuffer;
     VK_CHECK(m_vk.vkResetCommandBuffer(cmdBuff, 0));
-    VkCommandBufferBeginInfo beginInfo = {
+
+    const VkCommandBufferBeginInfo beginInfo = {
         .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
         .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
     };
     VK_CHECK(m_vk.vkBeginCommandBuffer(cmdBuff, &beginInfo));
-    VkImageMemoryBarrier presentToXferDstBarrier = {
+
+    std::vector<VkImageMemoryBarrier> preBlitQueueTransferBarriers;
+    std::vector<VkImageMemoryBarrier> preBlitLayoutTransitionBarriers;
+    std::vector<VkImageMemoryBarrier> postBlitLayoutTransitionBarriers;
+    std::vector<VkImageMemoryBarrier> postBlitQueueTransferBarriers;
+    addNeededBarriersToUseBorrowedImage(
+        *sourceImageInfoVk, m_compositorQueueFamilyIndex,
+        /*usedInitialImageLayout=*/VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+        /*usedFinalImageLayout=*/VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+        &preBlitQueueTransferBarriers, &preBlitLayoutTransitionBarriers,
+        &postBlitLayoutTransitionBarriers, &postBlitQueueTransferBarriers);
+    preBlitLayoutTransitionBarriers.push_back(
+        VkImageMemoryBarrier{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+                             .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
+                             .dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
+                             .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+                             .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                             .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+                             .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+                             .image = m_swapChainStateVk->getVkImages()[imageIndex],
+                             .subresourceRange = {
+                                 .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                                 .baseMipLevel = 0,
+                                 .levelCount = 1,
+                                 .baseArrayLayer = 0,
+                                 .layerCount = 1,
+                             }});
+    postBlitLayoutTransitionBarriers.push_back(VkImageMemoryBarrier{
         .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
         .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
-        .dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
-        .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
-        .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+        .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT,
+        .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+        .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
         .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
         .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
         .image = m_swapChainStateVk->getVkImages()[imageIndex],
-        .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
-                             .baseMipLevel = 0,
-                             .levelCount = 1,
-                             .baseArrayLayer = 0,
-                             .layerCount = 1}};
-    m_vk.vkCmdPipelineBarrier(cmdBuff, VK_PIPELINE_STAGE_TRANSFER_BIT,
-                              VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1,
-                              &presentToXferDstBarrier);
-    VkImageBlit region = {
+        .subresourceRange =
+            {
+                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                .baseMipLevel = 0,
+                .levelCount = 1,
+                .baseArrayLayer = 0,
+                .layerCount = 1,
+            },
+    });
+
+    if (!preBlitQueueTransferBarriers.empty()) {
+        m_vk.vkCmdPipelineBarrier(
+            cmdBuff, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
+            VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr,
+            static_cast<uint32_t>(preBlitQueueTransferBarriers.size()),
+            preBlitQueueTransferBarriers.data());
+    }
+    if (!preBlitLayoutTransitionBarriers.empty()) {
+        m_vk.vkCmdPipelineBarrier(
+            cmdBuff, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
+            VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr,
+            static_cast<uint32_t>(preBlitLayoutTransitionBarriers.size()),
+            preBlitLayoutTransitionBarriers.data());
+    }
+
+    const VkImageBlit region = {
         .srcSubresource = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                            .mipLevel = 0,
                            .baseArrayLayer = 0,
                            .layerCount = 1},
         .srcOffsets = {{0, 0, 0},
-                       {static_cast<int32_t>(displayBufferPtr->m_vkImageCreateInfo.extent.width),
-                        static_cast<int32_t>(displayBufferPtr->m_vkImageCreateInfo.extent.height),
-                        1}},
+                       {static_cast<int32_t>(sourceImageInfoVk->imageCreateInfo.extent.width),
+                        static_cast<int32_t>(sourceImageInfoVk->imageCreateInfo.extent.height), 1}},
         .dstSubresource = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                            .mipLevel = 0,
                            .baseArrayLayer = 0,
@@ -251,8 +251,8 @@
                        {static_cast<int32_t>(m_surfaceState->m_width),
                         static_cast<int32_t>(m_surfaceState->m_height), 1}},
     };
-    VkFormat displayBufferFormat = displayBufferPtr->m_vkImageCreateInfo.format;
-    VkImageTiling displayBufferTiling = displayBufferPtr->m_vkImageCreateInfo.tiling;
+    VkFormat displayBufferFormat = sourceImageInfoVk->imageCreateInfo.format;
+    VkImageTiling displayBufferTiling = sourceImageInfoVk->imageCreateInfo.tiling;
     VkFilter filter = VK_FILTER_NEAREST;
     VkFormatFeatureFlags displayBufferFormatFeatures =
         getFormatFeatures(displayBufferFormat, displayBufferTiling);
@@ -274,26 +274,23 @@
     } else {
         filter = VK_FILTER_LINEAR;
     }
-    m_vk.vkCmdBlitImage(cmdBuff, displayBufferPtr->m_vkImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+    m_vk.vkCmdBlitImage(cmdBuff, sourceImageInfoVk->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                         m_swapChainStateVk->getVkImages()[imageIndex],
                         VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region, filter);
-    VkImageMemoryBarrier xferDstToPresentBarrier = {
-        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
-        .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
-        .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT,
-        .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-        .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
-        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
-        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
-        .image = m_swapChainStateVk->getVkImages()[imageIndex],
-        .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
-                             .baseMipLevel = 0,
-                             .levelCount = 1,
-                             .baseArrayLayer = 0,
-                             .layerCount = 1}};
-    m_vk.vkCmdPipelineBarrier(cmdBuff, VK_PIPELINE_STAGE_TRANSFER_BIT,
-                              VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1,
-                              &xferDstToPresentBarrier);
+
+    if (!postBlitLayoutTransitionBarriers.empty()) {
+        m_vk.vkCmdPipelineBarrier(cmdBuff, VK_PIPELINE_STAGE_TRANSFER_BIT,
+                                  VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr,
+                                  static_cast<uint32_t>(postBlitLayoutTransitionBarriers.size()),
+                                  postBlitLayoutTransitionBarriers.data());
+    }
+    if (!postBlitQueueTransferBarriers.empty()) {
+        m_vk.vkCmdPipelineBarrier(cmdBuff, VK_PIPELINE_STAGE_TRANSFER_BIT,
+                                  VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr,
+                                  static_cast<uint32_t>(postBlitQueueTransferBarriers.size()),
+                                  postBlitQueueTransferBarriers.data());
+    }
+
     VK_CHECK(m_vk.vkEndCommandBuffer(cmdBuff));
 
     VkFence postCompleteFence = postResource->m_swapchainImageReleaseFence;
@@ -313,13 +310,8 @@
         VK_CHECK(m_vk.vkQueueSubmit(m_compositorVkQueue, 1, &submitInfo, postCompleteFence));
     }
     std::shared_future<std::shared_ptr<PostResource>> postResourceFuture =
-        std::async(std::launch::deferred, [postCompleteFence, postResource, displayBufferPtr,
-                                           this]() mutable {
+        std::async(std::launch::deferred, [postCompleteFence, postResource, this]() mutable {
             VK_CHECK(m_vk.vkWaitForFences(m_vkDevice, 1, &postCompleteFence, VK_TRUE, UINT64_MAX));
-            // Explicitly reset displayBufferPtr here to make sure the lambda actually capture
-            // displayBufferPtr to correctly extend the lifetime of displayBufferPtr until the
-            // rendering completes.
-            displayBufferPtr.reset();
             return postResource;
         }).share();
     m_postResourceFuture = postResourceFuture;
@@ -351,152 +343,6 @@
                                  }).share());
 }
 
-std::tuple<bool, std::shared_future<void>> DisplayVk::compose(
-    const std::vector<ComposeLayer>& composeLayers,
-    std::vector<std::shared_ptr<DisplayBufferInfo>> composeBuffers,
-    std::shared_ptr<DisplayBufferInfo> targetBuffer) {
-    std::shared_future<void> completedFuture = std::async(std::launch::deferred, [] {}).share();
-    completedFuture.wait();
-
-    if (!m_swapChainStateVk || !m_compositorVk) {
-        DISPLAY_VK_ERROR("Haven't bound to a surface, can't compose color buffer.");
-        // The surface hasn't been created yet, hence we don't return
-        // std::nullopt to request rebinding.
-        return std::make_tuple(true, std::move(completedFuture));
-    }
-
-    std::vector<std::unique_ptr<ComposeLayerVk>> composeLayerVks;
-    for (int i = 0; i < composeLayers.size(); ++i) {
-        if (composeLayers[i].cbHandle == 0) {
-            // When ColorBuffer handle is 0, it's expected that no ColorBuffer
-            // is not found.
-            continue;
-        }
-        if (!composeBuffers[i]) {
-            DISPLAY_VK_ERROR("warning: null ptr passed to compose buffer for layer %d.", i);
-            continue;
-        }
-        const auto& db = *composeBuffers[i];
-        if (!canCompositeFrom(db.m_vkImageCreateInfo)) {
-            DISPLAY_VK_ERROR("Can't composite from a display buffer. Skip the layer.");
-            continue;
-        }
-        auto composeLayerVk = ComposeLayerVk::createFromHwc2ComposeLayer(
-            m_compositionVkSampler, composeBuffers[i]->m_vkImageView, composeLayers[i],
-            composeBuffers[i]->m_vkImageCreateInfo.extent.width,
-            composeBuffers[i]->m_vkImageCreateInfo.extent.height,
-            targetBuffer->m_vkImageCreateInfo.extent.width,
-            targetBuffer->m_vkImageCreateInfo.extent.height);
-        composeLayerVks.emplace_back(std::move(composeLayerVk));
-    }
-
-    if (composeLayerVks.empty()) {
-        return std::make_tuple(true, std::move(completedFuture));
-    }
-
-    if (!targetBuffer) {
-        DISPLAY_VK_ERROR("warning null ptr passed to compose target.");
-        return std::make_tuple(true, std::move(completedFuture));
-    }
-    if (!canCompositeTo(targetBuffer->m_vkImageCreateInfo)) {
-        DISPLAY_VK_ERROR("Can't write the result of the composition to the display buffer.");
-        return std::make_tuple(true, std::move(completedFuture));
-    }
-
-    std::shared_ptr<CompositorVkRenderTarget> compositorVkRenderTarget =
-        targetBuffer->m_compositorVkRenderTarget.lock();
-    if (!compositorVkRenderTarget) {
-        compositorVkRenderTarget = m_compositorVk->createRenderTarget(
-            targetBuffer->m_vkImageView, targetBuffer->m_vkImageCreateInfo.extent.width,
-            targetBuffer->m_vkImageCreateInfo.extent.height);
-        if (!compositorVkRenderTarget) {
-            GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER))
-                << "Failed to create CompositorVkRenderTarget for the target display buffer.";
-        }
-        m_compositorVkRenderTargets.pop_back();
-        m_compositorVkRenderTargets.push_front(compositorVkRenderTarget);
-        targetBuffer->m_compositorVkRenderTarget = compositorVkRenderTarget;
-    }
-
-    std::future<std::unique_ptr<ComposeResource>> composeResourceFuture =
-        std::move(m_composeResourceFuture.value());
-    if (!composeResourceFuture.valid()) {
-        GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER))
-            << "Invalid composeResourceFuture in m_postResourceFutures.";
-    }
-    std::unique_ptr<ComposeResource> composeResource = composeResourceFuture.get();
-
-    if (compareAndSaveComposition(m_inFlightFrameIndex, composeLayers, composeBuffers)) {
-        auto composition = std::make_unique<Composition>(std::move(composeLayerVks));
-        m_compositorVk->setComposition(m_inFlightFrameIndex, std::move(composition));
-    }
-
-    VkCommandBuffer cmdBuff = composeResource->m_vkCommandBuffer;
-    VK_CHECK(m_vk.vkResetCommandBuffer(cmdBuff, 0));
-
-    VkCommandBufferBeginInfo beginInfo = {
-        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
-        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
-    };
-    VK_CHECK(m_vk.vkBeginCommandBuffer(cmdBuff, &beginInfo));
-    m_compositorVk->recordCommandBuffers(m_inFlightFrameIndex, cmdBuff, *compositorVkRenderTarget);
-    // Insert a VkImageMemoryBarrier so that the vkCmdBlitImage in post will wait for the rendering
-    // to the render target to complete.
-    VkImageMemoryBarrier renderTargetBarrier = {
-        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
-        .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
-        .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT,
-        .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-        .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
-        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
-        .image = targetBuffer->m_vkImage,
-        .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
-                             .baseMipLevel = 0,
-                             .levelCount = 1,
-                             .baseArrayLayer = 0,
-                             .layerCount = 1}};
-    m_vk.vkCmdPipelineBarrier(cmdBuff, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
-                              VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1,
-                              &renderTargetBarrier);
-    VK_CHECK(m_vk.vkEndCommandBuffer(cmdBuff));
-
-    VkFence composeCompleteFence = composeResource->m_composeCompleteFence;
-    VK_CHECK(m_vk.vkResetFences(m_vkDevice, 1, &composeCompleteFence));
-    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_TRANSFER_BIT};
-    VkSubmitInfo submitInfo = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
-                               .waitSemaphoreCount = 0,
-                               .pWaitSemaphores = nullptr,
-                               .pWaitDstStageMask = waitStages,
-                               .commandBufferCount = 1,
-                               .pCommandBuffers = &cmdBuff,
-                               .signalSemaphoreCount = 0,
-                               .pSignalSemaphores = nullptr};
-    {
-        android::base::AutoLock lock(*m_compositorVkQueueLock);
-        VK_CHECK(m_vk.vkQueueSubmit(m_compositorVkQueue, 1, &submitInfo, composeCompleteFence));
-    }
-
-    m_composeResourceFuture =
-        std::async(std::launch::deferred,
-                   [composeCompleteFence, composeResource = std::move(composeResource),
-                    composeBuffers = std::move(composeBuffers), targetBuffer, this]() mutable {
-                       VK_CHECK(m_vk.vkWaitForFences(m_vkDevice, 1, &composeCompleteFence, VK_TRUE,
-                                                     UINT64_MAX));
-                       // Explicitly clear the composeBuffers here to ensure the lambda does
-                       // caputure composeBuffers and correctly extend the lifetime of related
-                       // DisplayBufferInfo until the render completes.
-                       composeBuffers.clear();
-                       // Explicitly reset the targetBuffer here to ensure the lambda does caputure
-                       // targetBuffer and correctly extend the lifetime of the DisplayBufferInfo
-                       // until the render completes.
-                       targetBuffer.reset();
-                       return std::move(composeResource);
-                   });
-    m_inFlightFrameIndex = (m_inFlightFrameIndex + 1) % m_swapChainStateVk->getVkImages().size();
-    return post(targetBuffer);
-}
-
 VkFormatFeatureFlags DisplayVk::getFormatFeatures(VkFormat format, VkImageTiling tiling) {
     auto i = m_vkFormatProperties.find(format);
     if (i == m_vkFormatProperties.end()) {
@@ -607,165 +453,6 @@
     return true;
 }
 
-bool DisplayVk::canCompositeFrom(const VkImageCreateInfo& imageCi) {
-    VkFormatFeatureFlags formatFeatures = getFormatFeatures(imageCi.format, imageCi.tiling);
-    if (!(formatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) {
-        DISPLAY_VK_ERROR(
-            "The format, %s, with tiling, %s, doesn't support the "
-            "VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT feature. All supported features are %s.",
-            string_VkFormat(imageCi.format), string_VkImageTiling(imageCi.tiling),
-            string_VkFormatFeatureFlags(formatFeatures).c_str());
-        return false;
-    }
-    return true;
-}
-
-bool DisplayVk::canCompositeTo(const VkImageCreateInfo& imageCi) {
-    VkFormatFeatureFlags formatFeatures = getFormatFeatures(imageCi.format, imageCi.tiling);
-    if (!(formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
-        DISPLAY_VK_ERROR(
-            "The format, %s, with tiling, %s, doesn't support the "
-            "VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT feature. All supported features are %s.",
-            string_VkFormat(imageCi.format), string_VkImageTiling(imageCi.tiling),
-            string_VkFormatFeatureFlags(formatFeatures).c_str());
-        return false;
-    }
-    if (!(imageCi.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)) {
-        DISPLAY_VK_ERROR(
-            "The VkImage is not created with the VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT usage flag. "
-            "The usage flags are %s.",
-            string_VkImageUsageFlags(imageCi.usage).c_str());
-        return false;
-    }
-    if (imageCi.format != k_compositorVkRenderTargetFormat) {
-        DISPLAY_VK_ERROR(
-            "The format of the image, %s, is not supported by the CompositorVk as the render "
-            "target.",
-            string_VkFormat(imageCi.format));
-        return false;
-    }
-    return true;
-}
-
-bool DisplayVk::compareAndSaveComposition(
-    uint32_t renderTargetIndex, const std::vector<ComposeLayer>& composeLayers,
-    const std::vector<std::shared_ptr<DisplayBufferInfo>>& composeBuffers) {
-    if (!m_surfaceState) {
-        GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER))
-            << "Haven't bound to a surface, can't compare and save composition.";
-    }
-    auto [iPrevComposition, compositionNotFound] =
-        m_surfaceState->m_prevCompositions.emplace(renderTargetIndex, 0);
-    auto& prevComposition = iPrevComposition->second;
-    bool compositionChanged = false;
-    if (composeLayers.size() == prevComposition.size()) {
-        for (int i = 0; i < composeLayers.size(); i++) {
-            if (composeBuffers[i] == nullptr) {
-                // If the display buffer of the current layer doesn't exist, we
-                // check if the layer at the same index in the previous
-                // composition doesn't exist either.
-                if (prevComposition[i] == nullptr) {
-                    continue;
-                } else {
-                    compositionChanged = true;
-                    break;
-                }
-            }
-            if (prevComposition[i] == nullptr) {
-                // If the display buffer of the current layer exists but the
-                // layer at the same index in the previous composition doesn't
-                // exist, the composition is changed.
-                compositionChanged = true;
-                break;
-            }
-            const auto& prevLayer = *prevComposition[i];
-            const auto prevDisplayBufferPtr = prevLayer.m_displayBuffer.lock();
-            // prevLayer.m_displayBuffer is a weak pointer, so if
-            // prevDisplayBufferPtr is null, the color buffer
-            // prevDisplayBufferPtr pointed to should have been released or
-            // re-allocated, and we should consider the composition is changed.
-            // If prevDisplayBufferPtr exists and it points to the same display
-            // buffer as the input composeBuffers[i] we consider the composition
-            // not changed.
-            if (!prevDisplayBufferPtr || prevDisplayBufferPtr != composeBuffers[i]) {
-                compositionChanged = true;
-                break;
-            }
-            const auto& prevHwc2Layer = prevLayer.m_hwc2Layer;
-            const auto& hwc2Layer = composeLayers[i];
-            compositionChanged =
-                (prevHwc2Layer.cbHandle != hwc2Layer.cbHandle) ||
-                (prevHwc2Layer.composeMode != hwc2Layer.composeMode) ||
-                (prevHwc2Layer.displayFrame.left != hwc2Layer.displayFrame.left) ||
-                (prevHwc2Layer.displayFrame.top != hwc2Layer.displayFrame.top) ||
-                (prevHwc2Layer.displayFrame.right != hwc2Layer.displayFrame.right) ||
-                (prevHwc2Layer.displayFrame.bottom != hwc2Layer.displayFrame.bottom) ||
-                (prevHwc2Layer.crop.left != hwc2Layer.crop.left) ||
-                (prevHwc2Layer.crop.top != hwc2Layer.crop.top) ||
-                (prevHwc2Layer.crop.right != hwc2Layer.crop.right) ||
-                (prevHwc2Layer.crop.bottom != hwc2Layer.crop.bottom) ||
-                (prevHwc2Layer.blendMode != hwc2Layer.blendMode) ||
-                (prevHwc2Layer.alpha != hwc2Layer.alpha) ||
-                (prevHwc2Layer.color.r != hwc2Layer.color.r) ||
-                (prevHwc2Layer.color.g != hwc2Layer.color.g) ||
-                (prevHwc2Layer.color.b != hwc2Layer.color.b) ||
-                (prevHwc2Layer.color.a != hwc2Layer.color.a) ||
-                (prevHwc2Layer.transform != hwc2Layer.transform);
-            if (compositionChanged) {
-                break;
-            }
-        }
-    } else {
-        compositionChanged = true;
-    }
-    bool needsSave = compositionNotFound || compositionChanged;
-    if (needsSave) {
-        prevComposition.clear();
-        for (int i = 0; i < composeLayers.size(); i++) {
-            if (composeBuffers[i] == nullptr) {
-                prevComposition.emplace_back(nullptr);
-                continue;
-            }
-            auto layer = std::make_unique<SurfaceState::Layer>();
-            layer->m_hwc2Layer = composeLayers[i];
-            layer->m_displayBuffer = composeBuffers[i];
-            prevComposition.emplace_back(std::move(layer));
-        }
-    }
-    return needsSave;
-}
-
-DisplayVk::DisplayBufferInfo::DisplayBufferInfo(const goldfish_vk::VulkanDispatch& vk,
-                                                VkDevice vkDevice,
-                                                const VkImageCreateInfo& vkImageCreateInfo,
-                                                VkImage image)
-    : m_vk(vk),
-      m_vkDevice(vkDevice),
-      m_vkImageCreateInfo(vk_make_orphan_copy(vkImageCreateInfo)),
-      m_vkImage(image),
-      m_vkImageView(VK_NULL_HANDLE),
-      m_compositorVkRenderTarget() {
-    VkImageViewCreateInfo imageViewCi = {
-        .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
-        .image = image,
-        .viewType = VK_IMAGE_VIEW_TYPE_2D,
-        .format = m_vkImageCreateInfo.format,
-        .components = {.r = VK_COMPONENT_SWIZZLE_IDENTITY,
-                       .g = VK_COMPONENT_SWIZZLE_IDENTITY,
-                       .b = VK_COMPONENT_SWIZZLE_IDENTITY,
-                       .a = VK_COMPONENT_SWIZZLE_IDENTITY},
-        .subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
-                             .baseMipLevel = 0,
-                             .levelCount = 1,
-                             .baseArrayLayer = 0,
-                             .layerCount = 1}};
-    VK_CHECK(m_vk.vkCreateImageView(m_vkDevice, &imageViewCi, nullptr, &m_vkImageView));
-}
-
-DisplayVk::DisplayBufferInfo::~DisplayBufferInfo() {
-    m_vk.vkDestroyImageView(m_vkDevice, m_vkImageView, nullptr);
-}
-
 std::shared_ptr<DisplayVk::PostResource> DisplayVk::PostResource::create(
     const goldfish_vk::VulkanDispatch& vk, VkDevice vkDevice, VkCommandPool vkCommandPool) {
     VkFenceCreateInfo fenceCi = {
@@ -812,39 +499,3 @@
       m_vk(vk),
       m_vkDevice(vkDevice),
       m_vkCommandPool(vkCommandPool) {}
-
-std::unique_ptr<DisplayVk::ComposeResource> DisplayVk::ComposeResource::create(
-    const goldfish_vk::VulkanDispatch& vk, VkDevice vkDevice, VkCommandPool vkCommandPool) {
-    VkFenceCreateInfo fenceCi = {
-        .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
-    };
-    VkFence fence;
-    VK_CHECK(vk.vkCreateFence(vkDevice, &fenceCi, nullptr, &fence));
-
-    VkCommandBuffer commandBuffer;
-    VkCommandBufferAllocateInfo commandBufferAllocInfo = {
-        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
-        .commandPool = vkCommandPool,
-        .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
-        .commandBufferCount = 1,
-    };
-    VK_CHECK(vk.vkAllocateCommandBuffers(vkDevice, &commandBufferAllocInfo, &commandBuffer));
-
-    return std::unique_ptr<ComposeResource>(
-        new ComposeResource(vk, vkDevice, vkCommandPool, fence, commandBuffer));
-}
-
-DisplayVk::ComposeResource::~ComposeResource() {
-    m_vk.vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, 1, &m_vkCommandBuffer);
-    m_vk.vkDestroyFence(m_vkDevice, m_composeCompleteFence, nullptr);
-}
-
-DisplayVk::ComposeResource::ComposeResource(const goldfish_vk::VulkanDispatch& vk,
-                                            VkDevice vkDevice, VkCommandPool vkCommandPool,
-                                            VkFence composeCompleteFence,
-                                            VkCommandBuffer vkCommandBuffer)
-    : m_composeCompleteFence(composeCompleteFence),
-      m_vkCommandBuffer(vkCommandBuffer),
-      m_vk(vk),
-      m_vkDevice(vkDevice),
-      m_vkCommandPool(vkCommandPool) {}
diff --git a/stream-servers/vulkan/DisplayVk.h b/stream-servers/vulkan/DisplayVk.h
index cae840b..89cfa5b 100644
--- a/stream-servers/vulkan/DisplayVk.h
+++ b/stream-servers/vulkan/DisplayVk.h
@@ -10,6 +10,7 @@
 #include <unordered_map>
 #include <unordered_set>
 
+#include "BorrowedImage.h"
 #include "CompositorVk.h"
 #include "Hwc2.h"
 #include "RenderContext.h"
@@ -22,70 +23,22 @@
 
 class DisplayVk {
    public:
-    class DisplayBufferInfo {
-       public:
-        ~DisplayBufferInfo();
-
-       private:
-        DisplayBufferInfo(const goldfish_vk::VulkanDispatch&, VkDevice, const VkImageCreateInfo&,
-                          VkImage);
-
-        const goldfish_vk::VulkanDispatch& m_vk;
-        VkDevice m_vkDevice;
-        VkImageCreateInfo m_vkImageCreateInfo;
-
-        VkImage m_vkImage;
-        VkImageView m_vkImageView;
-        // m_compositorVkRenderTarget will be created when the first time the ColorBuffer is used as
-        // the render target of DisplayVk::compose. DisplayVk owns the m_compositorVkRenderTarget so
-        // that when the CompositorVk is recreated m_compositorVkRenderTarget can be restored to
-        // nullptr.
-        std::weak_ptr<CompositorVkRenderTarget> m_compositorVkRenderTarget;
-
-        friend class DisplayVk;
-    };
     DisplayVk(const goldfish_vk::VulkanDispatch&, VkPhysicalDevice,
               uint32_t swapChainQueueFamilyIndex, uint32_t compositorQueueFamilyIndex, VkDevice,
               VkQueue compositorVkQueue, std::shared_ptr<android::base::Lock> compositorVkQueueLock,
               VkQueue swapChainVkQueue, std::shared_ptr<android::base::Lock> swapChainVkQueueLock);
     ~DisplayVk();
     void bindToSurface(VkSurfaceKHR, uint32_t width, uint32_t height);
-    // The caller is responsible to make sure the VkImage lives longer than the DisplayBufferInfo
-    // created here.
-    std::shared_ptr<DisplayBufferInfo> createDisplayBuffer(VkImage, const VkImageCreateInfo&);
     // The first component of the returned tuple is false when the swapchain is no longer valid and
     // bindToSurface() needs to be called again. When the first component is true, the second
     // component of the returned tuple is a/ future that will complete when the GPU side of work
     // completes. The caller is responsible to guarantee the synchronization and the layout of
-    // DisplayBufferInfo::m_vkImage is VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL.
-    std::tuple<bool, std::shared_future<void>> post(std::shared_ptr<DisplayBufferInfo>);
-
-    // dstWidth and dstHeight describe the size of the render target the guest
-    // "thinks" it composes to, essentially, the virtual display size. Note that
-    // this can be different from the actual window size. The first component of
-    // the returned tuple is false when the swapchain is no longer valid and
-    // bindToSurface() needs to be called again. When the first component is
-    // true, the second component of the returned tuple is a future that will
-    // complete when the GPU side of work completes.
-    std::tuple<bool, std::shared_future<void>> compose(
-        const std::vector<ComposeLayer>& composeLayers,
-        std::vector<std::shared_ptr<DisplayBufferInfo>> composeBuffers,
-        std::shared_ptr<DisplayBufferInfo> renderTarget);
+    // ColorBufferCompositionInfo::m_vkImage is VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL.
+    std::tuple<bool, std::shared_future<void>> post(const BorrowedImageInfo* info);
 
    private:
     VkFormatFeatureFlags getFormatFeatures(VkFormat, VkImageTiling);
     bool canPost(const VkImageCreateInfo&);
-    // Check if the VkImage can be used as the compose layer to be sampled from.
-    bool canCompositeFrom(const VkImageCreateInfo&);
-    // Check if the VkImage can be used as the render target of the composition.
-    bool canCompositeTo(const VkImageCreateInfo&);
-    // Returns if the composition specified by the parameter is different from
-    // the previous composition. If the composition is different, update the
-    // previous composition stored in m_surfaceState. Must be called after
-    // bindToSurface() is called.
-    bool compareAndSaveComposition(
-        uint32_t renderTargetIndex, const std::vector<ComposeLayer>& composeLayers,
-        const std::vector<std::shared_ptr<DisplayBufferInfo>>& composeBuffers);
 
     const goldfish_vk::VulkanDispatch& m_vk;
     VkPhysicalDevice m_vkPhysicalDevice;
@@ -97,7 +50,6 @@
     VkQueue m_swapChainVkQueue;
     std::shared_ptr<android::base::Lock> m_swapChainVkQueueLock;
     VkCommandPool m_vkCommandPool;
-    VkSampler m_compositionVkSampler;
 
     class PostResource {
        public:
@@ -122,40 +74,13 @@
     std::deque<std::shared_ptr<PostResource>> m_freePostResources;
     std::optional<std::shared_future<std::shared_ptr<PostResource>>> m_postResourceFuture;
 
-    class ComposeResource {
-       public:
-        const VkFence m_composeCompleteFence;
-        const VkCommandBuffer m_vkCommandBuffer;
-        static std::unique_ptr<ComposeResource> create(const goldfish_vk::VulkanDispatch&, VkDevice,
-                                                       VkCommandPool);
-        ~ComposeResource();
-        DISALLOW_COPY_ASSIGN_AND_MOVE(ComposeResource);
-
-       private:
-        ComposeResource(const goldfish_vk::VulkanDispatch&, VkDevice, VkCommandPool, VkFence,
-                        VkCommandBuffer);
-        const goldfish_vk::VulkanDispatch& m_vk;
-        const VkDevice m_vkDevice;
-        const VkCommandPool m_vkCommandPool;
-    };
-
     int m_inFlightFrameIndex;
-    std::optional<std::future<std::unique_ptr<ComposeResource>>> m_composeResourceFuture;
 
     std::unique_ptr<SwapChainStateVk> m_swapChainStateVk;
-    std::unique_ptr<CompositorVk> m_compositorVk;
-    static constexpr uint32_t k_compositorVkRenderTargetCacheSize = 128;
-    std::deque<std::shared_ptr<CompositorVkRenderTarget>> m_compositorVkRenderTargets;
-    static constexpr VkFormat k_compositorVkRenderTargetFormat = VK_FORMAT_R8G8B8A8_UNORM;
-    struct SurfaceState {
-        struct Layer {
-            ComposeLayer m_hwc2Layer;
-            std::weak_ptr<DisplayBufferInfo> m_displayBuffer;
-        };
 
+    struct SurfaceState {
         uint32_t m_width = 0;
         uint32_t m_height = 0;
-        std::unordered_map<uint32_t, std::vector<std::unique_ptr<Layer>>> m_prevCompositions;
     };
     std::unique_ptr<SurfaceState> m_surfaceState;
 
diff --git a/stream-servers/vulkan/VkAndroidNativeBuffer.cpp b/stream-servers/vulkan/VkAndroidNativeBuffer.cpp
index 2cc9e54..d04a78f 100644
--- a/stream-servers/vulkan/VkAndroidNativeBuffer.cpp
+++ b/stream-servers/vulkan/VkAndroidNativeBuffer.cpp
@@ -144,7 +144,7 @@
     if (colorBufferVulkanCompatible && externalMemoryCompatible &&
         setupVkColorBuffer(out->colorBufferHandle, false /* not Vulkan only */,
                            0u /* memoryProperty */, &out->useVulkanNativeImage)) {
-        releaseColorBufferFromHostComposingSync({out->colorBufferHandle});
+        releaseColorBufferForGuestUse(out->colorBufferHandle);
         out->externallyBacked = true;
     }
 
diff --git a/stream-servers/vulkan/VkCommonOperations.cpp b/stream-servers/vulkan/VkCommonOperations.cpp
index 6f5808a..0f5ca0c 100644
--- a/stream-servers/vulkan/VkCommonOperations.cpp
+++ b/stream-servers/vulkan/VkCommonOperations.cpp
@@ -1123,6 +1123,7 @@
     INFO("    useDeferredCommands: %s", features->deferredCommands ? "true" : "false");
     INFO("    createResourceWithRequirements: %s",
          features->createResourceWithRequirements ? "true" : "false");
+    INFO("    useVulkanComposition: %s", features->useVulkanComposition ? "true" : "false");
     INFO("    useVulkanNativeSwapchain: %s", features->useVulkanNativeSwapchain ? "true" : "false");
     INFO("    enable guestRenderDoc: %s", features->guestRenderDoc ? "true" : "false");
     sVkEmulation->deviceInfo.glInteropSupported = features->glInteropSupported;
@@ -1130,6 +1131,15 @@
     sVkEmulation->useCreateResourcesWithRequirements = features->createResourceWithRequirements;
     sVkEmulation->guestRenderDoc = std::move(features->guestRenderDoc);
 
+    if (features->useVulkanComposition) {
+        if (sVkEmulation->compositorVk) {
+            ERR("Reset VkEmulation::compositorVk.");
+        }
+        sVkEmulation->compositorVk = CompositorVk::create(
+            *sVkEmulation->ivk, sVkEmulation->device, sVkEmulation->physdev, sVkEmulation->queue,
+            sVkEmulation->queueLock, sVkEmulation->queueFamilyIndex, 3);
+    }
+
     if (features->useVulkanNativeSwapchain) {
         if (sVkEmulation->displayVk) {
             ERR("Reset VkEmulation::displayVk.");
@@ -1152,6 +1162,7 @@
     // Don't try to tear down something that did not set up completely; too risky
     if (!sVkEmulation->live) return;
 
+    sVkEmulation->compositorVk.reset();
     sVkEmulation->displayVk.reset();
 
     freeExternalMemoryLocked(sVkEmulation->dvk, &sVkEmulation->staging.memory);
@@ -1666,6 +1677,8 @@
     }
 
     res.imageCreateInfoShallow = vk_make_orphan_copy(*imageCi);
+    res.currentLayout = res.imageCreateInfoShallow.initialLayout;
+    res.currentQueueFamilyIndex = sVkEmulation->queueFamilyIndex;
 
     vk->vkGetImageMemoryRequirements(sVkEmulation->device, res.image, &res.memReqs);
 
@@ -1716,6 +1729,36 @@
         return false;
     }
 
+    const VkImageViewCreateInfo imageViewCi = {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+        .pNext = nullptr,
+        .flags = 0,
+        .image = res.image,
+        .viewType = VK_IMAGE_VIEW_TYPE_2D,
+        .format = res.imageCreateInfoShallow.format,
+        .components =
+            {
+                .r = VK_COMPONENT_SWIZZLE_IDENTITY,
+                .g = VK_COMPONENT_SWIZZLE_IDENTITY,
+                .b = VK_COMPONENT_SWIZZLE_IDENTITY,
+                .a = VK_COMPONENT_SWIZZLE_IDENTITY,
+            },
+        .subresourceRange =
+            {
+                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                .baseMipLevel = 0,
+                .levelCount = 1,
+                .baseArrayLayer = 0,
+                .layerCount = 1,
+            },
+    };
+    createRes = vk->vkCreateImageView(sVkEmulation->device, &imageViewCi, nullptr, &res.imageView);
+    if (createRes != VK_SUCCESS) {
+        // LOG(VERBOSE) << "Failed to create Vulkan image for ColorBuffer "
+        //              << colorBufferHandle;
+        return false;
+    }
+
     if (sVkEmulation->instanceSupportsMoltenVK) {
         sVkEmulation->getMTLTextureFunc(res.image, &res.mtlTexture);
         if (!res.mtlTexture) {
@@ -1740,8 +1783,6 @@
     if (typeIndex) *typeIndex = res.memory.typeIndex;
     if (mappedPtr) *mappedPtr = res.memory.mappedPtr;
 
-    res.ownedByHost = std::make_shared<std::atomic_bool>(true);
-
     sVkEmulation->colorBuffers[colorBufferHandle] = res;
     return true;
 }
@@ -1762,6 +1803,7 @@
         android::base::AutoLock lock(*sVkEmulation->queueLock);
         VK_CHECK(vk->vkQueueWaitIdle(sVkEmulation->queue));
     }
+    vk->vkDestroyImageView(sVkEmulation->device, info.imageView, nullptr);
     vk->vkDestroyImage(sVkEmulation->device, info.image, nullptr);
     freeExternalMemoryLocked(vk, &info.memory);
 
@@ -2467,6 +2509,17 @@
     return res;
 }
 
+void setColorBufferCurrentLayout(uint32_t colorBufferHandle, VkImageLayout layout) {
+    AutoLock lock(sVkEmulationLock);
+
+    auto infoPtr = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
+    if (!infoPtr) {
+        VK_COMMON_ERROR("Invalid ColorBuffer handle %d.", static_cast<int>(colorBufferHandle));
+        return;
+    }
+    infoPtr->currentLayout = layout;
+}
+
 // Allocate a ready to use VkCommandBuffer for queue transfer. The caller needs
 // to signal the returned VkFence when the VkCommandBuffer completes.
 static std::tuple<VkCommandBuffer, VkFence> allocateQueueTransferCommandBuffer_locked() {
@@ -2521,179 +2574,31 @@
     return std::make_tuple(commandBuffer, fence);
 }
 
-void acquireColorBuffersForHostComposing(const std::vector<uint32_t>& layerColorBuffers,
-                                         uint32_t renderTargetColorBuffer) {
-    if (!sVkEmulation || !sVkEmulation->live) {
-        GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER)) << "Host Vulkan device lost";
-    }
+const VkImageLayout kGuestUseDefaultImageLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
 
-    std::vector<std::tuple<uint32_t, VkImageLayout>> colorBuffersAndLayouts;
-    for (uint32_t layerColorBuffer : layerColorBuffers) {
-        colorBuffersAndLayouts.emplace_back(
-            layerColorBuffer, FrameBuffer::getFB()->getVkImageLayoutForComposeLayer());
-    }
-    colorBuffersAndLayouts.emplace_back(renderTargetColorBuffer,
-                                        VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
-    AutoLock lock(sVkEmulationLock);
-    auto vk = sVkEmulation->dvk;
-
-    std::vector<std::tuple<VkEmulation::ColorBufferInfo*, VkImageLayout>>
-        colorBufferInfosAndLayouts;
-    for (auto [colorBufferHandle, newLayout] : colorBuffersAndLayouts) {
-        VkEmulation::ColorBufferInfo* infoPtr =
-            android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
-        if (!infoPtr) {
-            VK_COMMON_ERROR("Invalid ColorBuffer handle %d.", static_cast<int>(colorBufferHandle));
-            continue;
-        }
-        colorBufferInfosAndLayouts.emplace_back(infoPtr, newLayout);
-    }
-
-    std::vector<VkImageMemoryBarrier> queueTransferBarriers;
-    std::stringstream transferredColorBuffers;
-    for (auto [infoPtr, _] : colorBufferInfosAndLayouts) {
-        if (infoPtr->ownedByHost->load()) {
-            VK_COMMON_VERBOSE("Skipping queue transfer from guest to host for ColorBuffer(id = %d)",
-                              static_cast<int>(infoPtr->handle));
-            continue;
-        }
-        VkImageMemoryBarrier queueTransferBarrier = {
-            .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
-            .pNext = nullptr,
-            .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT,
-            // VK_ACCESS_SHADER_READ_BIT for the compose layers, and VK_ACCESS_TRANSFER_READ_BIT for
-            // the render target/post source.
-            .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT,
-            .oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
-            .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
-            .srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL,
-            .dstQueueFamilyIndex = sVkEmulation->queueFamilyIndex,
-            .image = infoPtr->image,
-            .subresourceRange =
-                {
-                    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
-                    .baseMipLevel = 0,
-                    .levelCount = 1,
-                    .baseArrayLayer = 0,
-                    .layerCount = 1,
-                },
-        };
-        queueTransferBarriers.emplace_back(queueTransferBarrier);
-        transferredColorBuffers << infoPtr->handle << " ";
-        infoPtr->ownedByHost->store(true);
-        infoPtr->currentLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
-    }
-
-    std::vector<VkImageMemoryBarrier> layoutTransitionBarriers;
-    for (auto [infoPtr, newLayout] : colorBufferInfosAndLayouts) {
-        if (newLayout == VK_IMAGE_LAYOUT_UNDEFINED || infoPtr->currentLayout == newLayout) {
-            continue;
-        }
-        VkImageMemoryBarrier layoutTransitionBarrier = {
-            .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
-            .pNext = nullptr,
-            .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT,
-            // VK_ACCESS_SHADER_READ_BIT for the compose layers, and VK_ACCESS_TRANSFER_READ_BIT for
-            // the render target/post source.
-            .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT,
-            .oldLayout = infoPtr->currentLayout,
-            .newLayout = newLayout,
-            .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
-            .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
-            .image = infoPtr->image,
-            .subresourceRange =
-                {
-                    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
-                    .baseMipLevel = 0,
-                    .levelCount = 1,
-                    .baseArrayLayer = 0,
-                    .layerCount = 1,
-                },
-        };
-        layoutTransitionBarriers.emplace_back(layoutTransitionBarrier);
-        infoPtr->currentLayout = newLayout;
-    }
-
-    auto [commandBuffer, fence] = allocateQueueTransferCommandBuffer_locked();
-
-    VkCommandBufferBeginInfo beginInfo = {
-        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
-        .pNext = nullptr,
-        .flags = 0,
-        .pInheritanceInfo = nullptr,
-    };
-    VK_CHECK(vk->vkBeginCommandBuffer(commandBuffer, &beginInfo));
-    if (!queueTransferBarriers.empty()) {
-        vk->vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
-                                 VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr,
-                                 static_cast<uint32_t>(queueTransferBarriers.size()),
-                                 queueTransferBarriers.data());
-    }
-    if (!layoutTransitionBarriers.empty()) {
-        vk->vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
-                                 VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr,
-                                 static_cast<uint32_t>(layoutTransitionBarriers.size()),
-                                 layoutTransitionBarriers.data());
-    }
-    VK_CHECK(vk->vkEndCommandBuffer(commandBuffer));
-
-    // We assume the host Vulkan compositor lives on the same queue, so we don't
-    // need to use semaphore to synchronize with the host compositor.
-    VkSubmitInfo submitInfo = {
-        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
-        .pNext = nullptr,
-        .waitSemaphoreCount = 0,
-        .pWaitSemaphores = nullptr,
-        .pWaitDstStageMask = nullptr,
-        .commandBufferCount = 1,
-        .pCommandBuffers = &commandBuffer,
-        .signalSemaphoreCount = 0,
-        .pSignalSemaphores = nullptr,
-    };
-    {
-        std::stringstream ss;
-        ss << __func__
-           << ": submitting commands to issue acquire queue transfer from "
-              "guest to host for ColorBuffer("
-           << transferredColorBuffers.str() << ")";
-        AEMU_SCOPED_TRACE(ss.str().c_str());
-        android::base::AutoLock lock(*sVkEmulation->queueLock);
-        VK_CHECK(vk->vkQueueSubmit(sVkEmulation->queue, 1, &submitInfo, fence));
-    }
-}
-
-static VkFence doReleaseColorBufferForGuestRendering(
-    const std::vector<uint32_t>& colorBufferHandles) {
+void releaseColorBufferForGuestUse(uint32_t colorBufferHandle) {
     if (!sVkEmulation || !sVkEmulation->live) {
         GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER)) << "Host Vulkan device lost";
     }
 
     AutoLock lock(sVkEmulationLock);
-    auto vk = sVkEmulation->dvk;
 
-    std::stringstream transferredColorBuffers;
-    std::vector<VkImageMemoryBarrier> layoutTransitionBarriers;
-    std::vector<VkImageMemoryBarrier> queueTransferBarriers;
-    for (uint32_t colorBufferHandle : colorBufferHandles) {
-        auto infoPtr = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
-        if (!infoPtr) {
-            VK_COMMON_ERROR("Invalid ColorBuffer handle %d.", static_cast<int>(colorBufferHandle));
-            continue;
-        }
-        if (!infoPtr->ownedByHost->load()) {
-            VK_COMMON_VERBOSE(
-                "Skipping queue transfer from host to guest for "
-                "ColorBuffer(id = %d)",
-                static_cast<int>(colorBufferHandle));
-            continue;
-        }
-        VkImageMemoryBarrier layoutTransitionBarrier = {
+    auto infoPtr = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
+    if (!infoPtr) {
+        VK_COMMON_ERROR("Failed to find ColorBuffer handle %d.",
+                        static_cast<int>(colorBufferHandle));
+        return;
+    }
+
+    std::optional<VkImageMemoryBarrier> layoutTransitionBarrier;
+    if (infoPtr->currentLayout != kGuestUseDefaultImageLayout) {
+        layoutTransitionBarrier = VkImageMemoryBarrier{
             .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
             .pNext = nullptr,
             .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
             .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
             .oldLayout = infoPtr->currentLayout,
-            .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
+            .newLayout = kGuestUseDefaultImageLayout,
             .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
             .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
             .image = infoPtr->image,
@@ -2706,17 +2611,19 @@
                     .layerCount = 1,
                 },
         };
-        layoutTransitionBarriers.emplace_back(layoutTransitionBarrier);
-        infoPtr->currentLayout = layoutTransitionBarrier.newLayout;
+        infoPtr->currentLayout = kGuestUseDefaultImageLayout;
+    }
 
-        VkImageMemoryBarrier queueTransferBarrier = {
+    std::optional<VkImageMemoryBarrier> queueTransferBarrier;
+    if (infoPtr->currentQueueFamilyIndex != VK_QUEUE_FAMILY_EXTERNAL) {
+        queueTransferBarrier = VkImageMemoryBarrier{
             .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
             .pNext = nullptr,
             .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
             .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
             .oldLayout = infoPtr->currentLayout,
             .newLayout = infoPtr->currentLayout,
-            .srcQueueFamilyIndex = sVkEmulation->queueFamilyIndex,
+            .srcQueueFamilyIndex = infoPtr->currentQueueFamilyIndex,
             .dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL,
             .image = infoPtr->image,
             .subresourceRange =
@@ -2728,33 +2635,40 @@
                     .layerCount = 1,
                 },
         };
-        queueTransferBarriers.emplace_back(queueTransferBarrier);
-        transferredColorBuffers << colorBufferHandle << " ";
-        infoPtr->ownedByHost->store(false);
+        infoPtr->currentQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
     }
 
+    if (!layoutTransitionBarrier && !queueTransferBarrier) {
+        return;
+    }
+
+    auto vk = sVkEmulation->dvk;
     auto [commandBuffer, fence] = allocateQueueTransferCommandBuffer_locked();
 
     VK_CHECK(vk->vkResetCommandBuffer(commandBuffer, 0));
-    VkCommandBufferBeginInfo beginInfo = {
+
+    const VkCommandBufferBeginInfo beginInfo = {
         .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
         .pNext = nullptr,
         .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
         .pInheritanceInfo = nullptr,
     };
     VK_CHECK(vk->vkBeginCommandBuffer(commandBuffer, &beginInfo));
-    vk->vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
-                             VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr,
-                             static_cast<uint32_t>(layoutTransitionBarriers.size()),
-                             layoutTransitionBarriers.data());
-    vk->vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
-                             VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr,
-                             static_cast<uint32_t>(queueTransferBarriers.size()),
-                             queueTransferBarriers.data());
+
+    if (layoutTransitionBarrier) {
+        vk->vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+                                 VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1,
+                                 &layoutTransitionBarrier.value());
+    }
+    if (queueTransferBarrier) {
+        vk->vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+                                 VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1,
+                                 &queueTransferBarrier.value());
+    }
+
     VK_CHECK(vk->vkEndCommandBuffer(commandBuffer));
-    // We assume the host Vulkan compositor lives on the same queue, so we don't
-    // need to use semaphore to synchronize with the host compositor.
-    VkSubmitInfo submitInfo = {
+
+    const VkSubmitInfo submitInfo = {
         .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
         .pNext = nullptr,
         .waitSemaphoreCount = 0,
@@ -2765,45 +2679,84 @@
         .signalSemaphoreCount = 0,
         .pSignalSemaphores = nullptr,
     };
-
     {
-        std::stringstream ss;
-        ss << __func__
-           << ": submitting commands to issue release queue transfer from host "
-              "to guest for ColorBuffer("
-           << transferredColorBuffers.str() << ")";
-        AEMU_SCOPED_TRACE(ss.str().c_str());
         android::base::AutoLock lock(*sVkEmulation->queueLock);
         VK_CHECK(vk->vkQueueSubmit(sVkEmulation->queue, 1, &submitInfo, fence));
     }
-    return fence;
-}
 
-void releaseColorBufferFromHostComposing(const std::vector<uint32_t>& colorBufferHandles) {
-    doReleaseColorBufferForGuestRendering(colorBufferHandles);
-}
-
-void releaseColorBufferFromHostComposingSync(const std::vector<uint32_t>& colorBufferHandles) {
-    VkFence fence = doReleaseColorBufferForGuestRendering(colorBufferHandles);
-    if (!sVkEmulation || !sVkEmulation->live) {
-        GFXSTREAM_ABORT(FatalError(ABORT_REASON_OTHER)) << "Host Vulkan device lost";
-    }
-
-    AutoLock lock(sVkEmulationLock);
-    auto vk = sVkEmulation->dvk;
     static constexpr uint64_t ANB_MAX_WAIT_NS = 5ULL * 1000ULL * 1000ULL * 1000ULL;
     VK_CHECK(vk->vkWaitForFences(sVkEmulation->device, 1, &fence, VK_TRUE, ANB_MAX_WAIT_NS));
 }
 
-void setColorBufferCurrentLayout(uint32_t colorBufferHandle, VkImageLayout layout) {
+std::unique_ptr<BorrowedImageInfoVk> borrowColorBufferForComposition(uint32_t colorBufferHandle,
+                                                                     bool colorBufferIsTarget) {
     AutoLock lock(sVkEmulationLock);
 
-    auto infoPtr = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
-    if (!infoPtr) {
+    auto colorBufferInfo = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
+    if (!colorBufferInfo) {
         VK_COMMON_ERROR("Invalid ColorBuffer handle %d.", static_cast<int>(colorBufferHandle));
-        return;
+        return nullptr;
     }
-    infoPtr->currentLayout = layout;
+
+    auto compositorInfo = std::make_unique<BorrowedImageInfoVk>();
+    compositorInfo->id = colorBufferInfo->handle;
+    compositorInfo->width = colorBufferInfo->imageCreateInfoShallow.extent.width;
+    compositorInfo->height = colorBufferInfo->imageCreateInfoShallow.extent.height;
+    compositorInfo->image = colorBufferInfo->image;
+    compositorInfo->imageView = colorBufferInfo->imageView;
+    compositorInfo->imageCreateInfo = colorBufferInfo->imageCreateInfoShallow;
+    compositorInfo->preBorrowLayout = colorBufferInfo->currentLayout;
+    compositorInfo->preBorrowQueueFamilyIndex = colorBufferInfo->currentQueueFamilyIndex;
+    if (colorBufferIsTarget && sVkEmulation->displayVk) {
+        // Instruct the compositor to perform the layout transition after use so
+        // that it is ready to be blitted to the display.
+        compositorInfo->postBorrowQueueFamilyIndex = sVkEmulation->queueFamilyIndex;
+        compositorInfo->postBorrowLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
+    } else {
+        // Instruct the compositor to perform the queue transfer release after use
+        // so that the color buffer can be acquired by the guest.
+        compositorInfo->postBorrowQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
+        compositorInfo->postBorrowLayout = colorBufferInfo->currentLayout;
+
+        if (compositorInfo->postBorrowLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
+            compositorInfo->postBorrowLayout = kGuestUseDefaultImageLayout;
+        }
+    }
+
+    colorBufferInfo->currentLayout = compositorInfo->postBorrowLayout;
+    colorBufferInfo->currentQueueFamilyIndex = compositorInfo->postBorrowQueueFamilyIndex;
+
+    return compositorInfo;
+}
+
+std::unique_ptr<BorrowedImageInfoVk> borrowColorBufferForDisplay(uint32_t colorBufferHandle) {
+    AutoLock lock(sVkEmulationLock);
+
+    auto colorBufferInfo = android::base::find(sVkEmulation->colorBuffers, colorBufferHandle);
+    if (!colorBufferInfo) {
+        VK_COMMON_ERROR("Invalid ColorBuffer handle %d.", static_cast<int>(colorBufferHandle));
+        return nullptr;
+    }
+
+    auto compositorInfo = std::make_unique<BorrowedImageInfoVk>();
+    compositorInfo->id = colorBufferInfo->handle;
+    compositorInfo->width = colorBufferInfo->imageCreateInfoShallow.extent.width;
+    compositorInfo->height = colorBufferInfo->imageCreateInfoShallow.extent.height;
+    compositorInfo->image = colorBufferInfo->image;
+    compositorInfo->imageView = colorBufferInfo->imageView;
+    compositorInfo->imageCreateInfo = colorBufferInfo->imageCreateInfoShallow;
+    compositorInfo->preBorrowLayout = colorBufferInfo->currentLayout;
+    compositorInfo->preBorrowQueueFamilyIndex = sVkEmulation->queueFamilyIndex;
+
+    // Instruct the display to perform the queue transfer release after use so
+    // that the color buffer can be acquired by the guest.
+    compositorInfo->postBorrowQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
+    compositorInfo->postBorrowLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
+
+    colorBufferInfo->currentLayout = compositorInfo->postBorrowLayout;
+    colorBufferInfo->currentQueueFamilyIndex = compositorInfo->postBorrowQueueFamilyIndex;
+
+    return compositorInfo;
 }
 
 }  // namespace goldfish_vk
diff --git a/stream-servers/vulkan/VkCommonOperations.h b/stream-servers/vulkan/VkCommonOperations.h
index e987df2..a2a1bd5 100644
--- a/stream-servers/vulkan/VkCommonOperations.h
+++ b/stream-servers/vulkan/VkCommonOperations.h
@@ -22,6 +22,8 @@
 #include <unordered_set>
 #include <vector>
 
+#include "BorrowedImageVk.h"
+#include "CompositorVk.h"
 #include "DisplayVk.h"
 #include "base/Lock.h"
 #include "base/Optional.h"
@@ -250,21 +252,18 @@
         int frameworkStride;
 
         VkImage image = VK_NULL_HANDLE;
+        VkImageView imageView = VK_NULL_HANDLE;
         VkImageCreateInfo imageCreateInfoShallow = {};
         VkMemoryRequirements memReqs;
 
         VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+        uint32_t currentQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
 
         bool glExported = false;
 
         VulkanMode vulkanMode = VulkanMode::Default;
 
         MTLTextureRef mtlTexture = nullptr;
-        // shared_ptr is required so that ColorBufferInfo::ownedByHost can have
-        // an uninitialized default value that is neither true or false. The
-        // actual value will be set by setupVkColorBuffer when creating
-        // ColorBufferInfo.
-        std::shared_ptr<std::atomic_bool> ownedByHost = nullptr;
     };
 
     struct BufferInfo {
@@ -337,6 +336,8 @@
     // signaled only if the command buffer completes.
     std::vector<std::tuple<VkCommandBuffer, VkFence>> transferQueueCommandBufferPool;
 
+    std::unique_ptr<CompositorVk> compositorVk;
+
     // The implementation for Vulkan native swapchain. Only initialized in initVkEmulationFeatures
     // if useVulkanNativeSwapchain is set.
     std::unique_ptr<DisplayVk> displayVk;
@@ -347,6 +348,7 @@
     bool glInteropSupported = false;
     bool deferredCommands = false;
     bool createResourceWithRequirements = false;
+    bool useVulkanComposition = false;
     bool useVulkanNativeSwapchain = false;
     std::unique_ptr<emugl::RenderDocWithMultipleVkInstances> guestRenderDoc = nullptr;
 };
@@ -409,13 +411,12 @@
 VkExternalMemoryProperties transformExternalMemoryProperties_fromhost(
     VkExternalMemoryProperties props, VkExternalMemoryHandleTypeFlags wantedGuestHandleType);
 
-void acquireColorBuffersForHostComposing(const std::vector<uint32_t>& layerColorBuffers,
-                                         uint32_t renderTargetColorBuffer);
-
-void releaseColorBufferFromHostComposing(const std::vector<uint32_t>& colorBufferHandles);
-
-void releaseColorBufferFromHostComposingSync(const std::vector<uint32_t>& colorBufferHandles);
-
 void setColorBufferCurrentLayout(uint32_t colorBufferHandle, VkImageLayout);
 
+void releaseColorBufferForGuestUse(uint32_t colorBufferHandle);
+
+std::unique_ptr<BorrowedImageInfoVk> borrowColorBufferForComposition(uint32_t colorBufferHandle,
+                                                                     bool colorBufferIsTarget);
+std::unique_ptr<BorrowedImageInfoVk> borrowColorBufferForDisplay(uint32_t colorBufferHandle);
+
 }  // namespace goldfish_vk
diff --git a/stream-servers/vulkan/VkDecoderGlobalState.cpp b/stream-servers/vulkan/VkDecoderGlobalState.cpp
index d2d44c8..70c7212 100644
--- a/stream-servers/vulkan/VkDecoderGlobalState.cpp
+++ b/stream-servers/vulkan/VkDecoderGlobalState.cpp
@@ -3234,6 +3234,17 @@
         auto imageInfo = android::base::find(mImageInfo, image);
         auto anbInfo = imageInfo->anbInfo;
 
+        if (anbInfo->useVulkanNativeImage) {
+            // vkQueueSignalReleaseImageANDROID() is only called by the Android framework's
+            // implementation of vkQueuePresentKHR(). The guest application is responsible for
+            // transitioning the image layout of the image passed to vkQueuePresentKHR() to
+            // VK_IMAGE_LAYOUT_PRESENT_SRC_KHR before the call. If the host is using native
+            // Vulkan images where `image` is backed with the same memory as its ColorBuffer,
+            // then we need to update the tracked layout for that ColorBuffer.
+            setColorBufferCurrentLayout(anbInfo->colorBufferHandle,
+                                        VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
+        }
+
         return syncImageToColorBuffer(vk, queueInfo->queueFamilyIndex, queue, queueInfo->lock,
                                       waitSemaphoreCount, pWaitSemaphores, pNativeFenceFd, anbInfo);
     }