/* Copyright (c) 2026 The Khronos Group Inc.
 * Copyright (c) 2026 Valve Corporation
 * Copyright (c) 2026 LunarG, Inc.
 *
 * 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 "sync_val_tests.h"
#include <vulkan/utility/vk_format_utils.h>
#include <thread>

struct PositiveSyncValWsi : public VkSyncValTest {};

TEST_F(PositiveSyncValWsi, PresentAfterSubmit2AutomaticVisibility) {
    TEST_DESCRIPTION("Waiting on the semaphore makes available image accesses visible to the presentation engine.");
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);
    RETURN_IF_SKIP(InitSyncValFramework());
    RETURN_IF_SKIP(InitState());
    RETURN_IF_SKIP(InitSwapchain());
    vkt::Semaphore acquire_semaphore(*m_device);
    vkt::Semaphore submit_semaphore(*m_device);
    const auto swapchain_images = m_swapchain.GetImages();
    const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);

    VkImageMemoryBarrier2 layout_transition = vku::InitStructHelper();
    // this creates execution dependency with submit's wait semaphore, so layout
    // transition does not start before image is acquired.
    layout_transition.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition.srcAccessMask = 0;

    // this creates execution dependency with submit's signal operation, so layout
    // transition finishes before presentation starts.
    layout_transition.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;

    // dstAccessMask makes accesses visible only to the device.
    // Also, any writes to swapchain images that are made available, are
    // automatically made visible to the presentation engine reads.
    // This test checks that presentation engine accesses are not reported as hazards.
    layout_transition.dstAccessMask = 0;

    layout_transition.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    layout_transition.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    layout_transition.image = swapchain_images[image_index];
    layout_transition.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    m_command_buffer.Begin();
    m_command_buffer.Barrier(layout_transition);
    m_command_buffer.End();

    m_default_queue->Submit2(m_command_buffer, vkt::Wait(acquire_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT),
                             vkt::Signal(submit_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore);
    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, PresentAfterSubmitAutomaticVisibility) {
    TEST_DESCRIPTION("Waiting on the semaphore makes available image accesses visible to the presentation engine.");
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncValFramework());
    RETURN_IF_SKIP(InitState());
    RETURN_IF_SKIP(InitSwapchain());
    vkt::Semaphore acquire_semaphore(*m_device);
    vkt::Semaphore submit_semaphore(*m_device);
    const auto swapchain_images = m_swapchain.GetImages();
    const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);

    VkImageMemoryBarrier layout_transition = vku::InitStructHelper();
    layout_transition.srcAccessMask = 0;

    // dstAccessMask makes accesses visible only to the device.
    // Also, any writes to swapchain images that are made available, are
    // automatically made visible to the presentation engine reads.
    // This test checks that presentation engine accesses are not reported as hazards.
    layout_transition.dstAccessMask = 0;

    layout_transition.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    layout_transition.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    layout_transition.image = swapchain_images[image_index];
    layout_transition.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    m_command_buffer.Begin();
    vk::CmdPipelineBarrier(m_command_buffer, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
                           VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &layout_transition);
    m_command_buffer.End();

    m_default_queue->Submit(m_command_buffer, vkt::Wait(acquire_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT),
                            vkt::Signal(submit_semaphore));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore);
    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, PresentAfterSubmitNoneDstStage) {
    TEST_DESCRIPTION("Test that QueueSubmit's signal semaphore behaves the same way as QueueSubmit2 with ALL_COMMANDS signal.");
    AddSurfaceExtension();
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddRequiredFeature(vkt::Feature::synchronization2);
    RETURN_IF_SKIP(InitSyncValFramework());
    RETURN_IF_SKIP(InitState());
    RETURN_IF_SKIP(InitSwapchain());
    vkt::Semaphore acquire_semaphore(*m_device);
    vkt::Semaphore submit_semaphore(*m_device);
    const auto swapchain_images = m_swapchain.GetImages();
    const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);

    VkImageMemoryBarrier2 layout_transition = vku::InitStructHelper();
    layout_transition.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition.srcAccessMask = 0;
    // Specify NONE as destination stage to detect issues during conversion SubmitInfo -> SubmitInfo2
    layout_transition.dstStageMask = VK_PIPELINE_STAGE_2_NONE;
    layout_transition.dstAccessMask = 0;
    layout_transition.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    layout_transition.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    layout_transition.image = swapchain_images[image_index];
    layout_transition.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    m_command_buffer.Begin();
    m_command_buffer.Barrier(layout_transition);
    m_command_buffer.End();

    // The goal of this test is to use QueueSubmit API (not QueueSubmit2) to
    // ensure syncval correctly converts SubmitInfo to SubmitInfo2 with ALL_COMMANDS signal semaphore.
    m_default_queue->Submit(m_command_buffer, vkt::Wait(acquire_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT),
                            vkt::Signal(submit_semaphore));

    m_default_queue->Present(m_swapchain, image_index, submit_semaphore);
    m_device->Wait();
}

TEST_F(PositiveSyncValWsi, ThreadedSubmitAndFenceWaitAndPresent) {
    TEST_DESCRIPTION("https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/7250");
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    const auto swapchain_images = m_swapchain.GetImages();
    if (!m_swapchain.TryTransitionToPresentLayout(*m_device, *m_default_queue, m_command_pool)) {
        GTEST_SKIP() << "Failed to pre-transition swapchain images";
    }

    constexpr int N = 100;  // Initially 1000 for higher repro rate
    std::mutex queue_mutex;

    // Worker thread submits accesses and waits on the fence.
    std::thread thread([&] {
        const int size = 1024 * 128;
        vkt::Buffer src(*m_device, size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
        vkt::Buffer dst(*m_device, size, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
        VkBufferCopy copy_info{};
        copy_info.size = size;

        vkt::Fence fence(*m_device);
        for (int i = 0; i < N; i++) {
            m_command_buffer.Begin();
            vk::CmdCopyBuffer(m_command_buffer, src, dst, 1, &copy_info);
            m_command_buffer.End();
            {
                std::unique_lock<std::mutex> lock(queue_mutex);
                m_default_queue->Submit(m_command_buffer, fence);
            }
            vk::WaitForFences(device(), 1, &fence.handle(), VK_TRUE, kWaitTimeout);
            vk::ResetFences(device(), 1, &fence.handle());
        }
    });

    // Main thread submits empty batches and presents images
    {
        vkt::Semaphore acquire_semaphore(*m_device);

        std::vector<vkt::Semaphore> submit_semaphores;
        for (size_t i = 0; i < swapchain_images.size(); i++) {
            submit_semaphores.emplace_back(*m_device);
        }

        vkt::Fence fence(*m_device);

        for (int i = 0; i < N; i++) {
            const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);
            {
                std::unique_lock<std::mutex> lock(queue_mutex);
                m_default_queue->Submit(vkt::no_cmd, vkt::Wait(acquire_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT),
                                        vkt::Signal(submit_semaphores[image_index]), fence);
                m_default_queue->Present(m_swapchain, image_index, submit_semaphores[image_index]);
            }
            fence.Wait(kWaitTimeout);
            fence.Reset();
        }
        {
            // We did not synchronize with the presentation request from the last iteration.
            // Wait on the queue to ensure submit semaphore used by presentation request is not in use.
            std::unique_lock<std::mutex> lock(queue_mutex);
            m_default_queue->Wait();
        }
    }
    thread.join();
}

TEST_F(PositiveSyncValWsi, WaitForFencesWithPresentBatches) {
    TEST_DESCRIPTION("Check that WaitForFences applies tagged waits to present batches");
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());
    const auto swapchain_images = m_swapchain.GetImages();

    vkt::CommandBuffer frame1_cb(*m_device, m_command_pool);

    vkt::Semaphore acquire_semaphore(*m_device);
    vkt::Semaphore submit_semaphore(*m_device);

    vkt::Semaphore acquire_semaphore2(*m_device);
    vkt::Semaphore submit_semaphore2(*m_device);

    vkt::Fence fence(*m_device);

    vkt::Buffer buffer(*m_device, 256, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
    vkt::Buffer src_buffer(*m_device, 256, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
    vkt::Buffer dst_buffer(*m_device, 256, VK_BUFFER_USAGE_TRANSFER_DST_BIT);

    // Frame 0
    {
        const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);

        m_command_buffer.Begin();
        m_command_buffer.TransitionLayout(swapchain_images[image_index], VK_IMAGE_LAYOUT_UNDEFINED,
                                          VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
        m_command_buffer.Copy(src_buffer, buffer);
        m_command_buffer.End();

        m_default_queue->Submit(m_command_buffer, vkt::Wait(acquire_semaphore), vkt::Signal(submit_semaphore), fence);
        m_default_queue->Present(m_swapchain, image_index, submit_semaphore);
    }
    // Frame 1
    {
        const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore2, kWaitTimeout);

        frame1_cb.Begin();
        frame1_cb.TransitionLayout(swapchain_images[image_index], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
        frame1_cb.End();

        m_default_queue->Submit(frame1_cb, vkt::Wait(acquire_semaphore2), vkt::Signal(submit_semaphore2));
        m_default_queue->Present(m_swapchain, image_index, submit_semaphore2);
    }
    // Frame 2
    {
        // The goal of this test is to ensure that this wait is applied to the
        // batches resulted from queue presentation operations. Those batches
        // import accesses from regular submits.
        vk::WaitForFences(*m_device, 1, &fence.handle(), VK_TRUE, kWaitTimeout);

        m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);  // do not need to keep result

        // If WaitForFences leaks accesses from present batches the following copy will cause submit time hazard.
        m_command_buffer.Begin();
        m_command_buffer.Copy(buffer, dst_buffer);
        m_command_buffer.End();
        m_default_queue->Submit(m_command_buffer, vkt::Wait(acquire_semaphore));
    }
    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, RecreateBuffer) {
    TEST_DESCRIPTION("Recreate buffer on each simulation iteration. Use acquire fence synchronization approach.");
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    const auto swapchain_images = m_swapchain.GetImages();

    std::vector<vkt::Fence> acquire_fences;
    vkt::Fence current_fence(*m_device);
    std::vector<vkt::CommandBuffer> command_buffers;
    std::vector<vkt::Semaphore> submit_semaphores;

    std::vector<vkt::Buffer> src_buffers(swapchain_images.size());
    std::vector<vkt::Buffer> dst_buffers(swapchain_images.size());

    for (size_t i = 0; i < swapchain_images.size(); i++) {
        acquire_fences.emplace_back(*m_device);
        command_buffers.emplace_back(*m_device, m_command_pool);
        submit_semaphores.emplace_back(*m_device);
    }

    // NOTE: This test can be used for manual inspection of memory usage.
    // Increase frame count and observe that the test does not continuously allocate memory.
    // Syncval should not track ranges of deleted resources.
    const int frame_count = 100;

    for (int i = 0; i < frame_count; i++) {
        const uint32_t image_index = m_swapchain.AcquireNextImage(current_fence, kWaitTimeout);
        current_fence.Wait(kWaitTimeout);
        current_fence.Reset();

        auto& src_buffer = src_buffers[image_index];
        src_buffer.Destroy();
        src_buffer = vkt::Buffer(*m_device, 1024, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);

        auto& dst_buffer = dst_buffers[image_index];
        dst_buffer.Destroy();
        dst_buffer = vkt::Buffer(*m_device, 1024, VK_BUFFER_USAGE_TRANSFER_DST_BIT);

        auto& command_buffer = command_buffers[image_index];
        command_buffer.Begin();
        command_buffer.TransitionLayout(swapchain_images[image_index], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
        command_buffer.Copy(src_buffer, dst_buffer);
        command_buffer.End();

        auto& submit_semaphore = submit_semaphores[image_index];
        m_default_queue->Submit(command_buffer, vkt::Signal(submit_semaphore));
        m_default_queue->Present(m_swapchain, image_index, submit_semaphore);
        std::swap(acquire_fences[image_index], current_fence);
    }
    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, RecreateImage) {
    TEST_DESCRIPTION("Recreate image on each simulation iteration. Use acquire fence synchronization approach.");
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    constexpr uint32_t width = 256;
    constexpr uint32_t height = 128;
    constexpr VkFormat format = VK_FORMAT_B8G8R8A8_UNORM;

    const auto swapchain_images = m_swapchain.GetImages();

    std::vector<vkt::Fence> acquire_fences;
    vkt::Fence current_fence(*m_device);
    std::vector<vkt::CommandBuffer> command_buffers;
    std::vector<vkt::Semaphore> submit_semaphores;

    const vkt::Buffer src_buffer(*m_device, width * height * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
    std::vector<vkt::Image> dst_images(swapchain_images.size());

    for (size_t i = 0; i < swapchain_images.size(); i++) {
        acquire_fences.emplace_back(*m_device);
        command_buffers.emplace_back(*m_device, m_command_pool);
        submit_semaphores.emplace_back(*m_device);
    }

    // NOTE: This test can be used for manual inspection of memory usage.
    // Increase frame count and observe that the test does not continuously allocate memory.
    // Syncval should not track ranges of deleted resources.
    const int frame_count = 100;

    for (int i = 0; i < frame_count; i++) {
        const uint32_t image_index = m_swapchain.AcquireNextImage(current_fence, kWaitTimeout);
        current_fence.Wait(kWaitTimeout);
        current_fence.Reset();

        auto& dst_image = dst_images[image_index];
        dst_image.Destroy();
        dst_image = vkt::Image(*m_device, width, height, format, VK_IMAGE_USAGE_TRANSFER_DST_BIT);

        VkBufferImageCopy region = {};
        region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
        region.imageExtent = {width, height, 1};

        auto& command_buffer = command_buffers[image_index];
        command_buffer.Begin();
        command_buffer.TransitionLayout(swapchain_images[image_index], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
        command_buffer.TransitionLayout(dst_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
        vk::CmdCopyBufferToImage(command_buffer, src_buffer, dst_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
        command_buffer.End();

        auto& submit_semaphore = submit_semaphores[image_index];
        m_default_queue->Submit(command_buffer, vkt::Signal(submit_semaphore));
        m_default_queue->Present(m_swapchain, image_index, submit_semaphore);
        std::swap(acquire_fences[image_index], current_fence);
    }
    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, ResyncWithSwapchain) {
    // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/10586
    // Semaphore wait should not introduce unsynchronized swapchain accesses from internal
    // queue access contexts if those acceses were properly synchronized.
    TEST_DESCRIPTION("Try to introduce unsynchronized swapchain accesses after proper swapchain synchronization");
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);

    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT));

    const auto swapchain_images = m_swapchain.GetImages();
    if (swapchain_images.size() != 2) {
        GTEST_SKIP() << "The test requires swapchain with 2 images";
    }
    if (!m_swapchain.TryTransitionToPresentLayout(*m_device, *m_default_queue, m_command_pool)) {
        GTEST_SKIP() << "Failed to pre-transition swapchain images";
    }
    const VkImage swapchain_image0 = swapchain_images[0];

    vkt::Semaphore acquire_semaphore0(*m_device);
    vkt::Semaphore acquire_semaphore1(*m_device);
    vkt::Semaphore acquire_semaphore2(*m_device);
    vkt::Semaphore submit_semaphore0(*m_device);
    vkt::Semaphore submit_semaphore1(*m_device);

    // This semaphore is signaled when swapchain still uses image0
    vkt::Semaphore semaphore(*m_device);

    VkImageMemoryBarrier2 transition_swapchain_image0 = vku::InitStructHelper();
    transition_swapchain_image0.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT;
    transition_swapchain_image0.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
    transition_swapchain_image0.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    transition_swapchain_image0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    transition_swapchain_image0.image = swapchain_image0;
    transition_swapchain_image0.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    const SurfaceInformation info = GetSwapchainInfo(m_surface);
    const uint32_t width = GetSwapchainExtent(info.surface_capabilities).width;
    const uint32_t height = GetSwapchainExtent(info.surface_capabilities).height;
    const uint32_t format_size = vkuFormatTexelBlockSize(info.surface_formats[0].format);
    vkt::Buffer buffer(*m_device, width * height * format_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);

    VkBufferImageCopy copy_region{};
    copy_region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
    copy_region.imageExtent = {width, height, 1};

    // Frame 0
    uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore0, kWaitTimeout);
    if (image_index != 0) {
        GTEST_SKIP() << "This test requires the first acquired image index is 0";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore0), vkt::Signal(submit_semaphore0));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore0);

    // Signal semaphore when swapchain image0 can still be in use by the swapchain.
    // If we immediately synchronize with this semaphore image0 can still be in use.
    // If at first we synchronize with swapchain image0 and only then wait on this
    // semaphore (maybe redundantly), then it changes nothing, image0 remains synchronized
    // and it is safe to access it. This test recreates a regression scenario when binary
    // semaphore wait imported unsynchronized swapchain accesses even though swapchain
    // accesses were already synchronized.
    m_default_queue->Submit2(vkt::no_cmd, vkt::Signal(semaphore));

    // Frame 1
    image_index = m_swapchain.AcquireNextImage(acquire_semaphore1, kWaitTimeout);
    if (image_index != 1) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the second acquired image index is 1";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore1), vkt::Signal(submit_semaphore1));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore1);

    // Frame 2. Re-acquire image0 that was presented in Frame0
    image_index = m_swapchain.AcquireNextImage(acquire_semaphore2, kWaitTimeout);
    if (image_index != 0) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the third acquired image index is 0";
    }

    // Explanation of what the following sequence does.
    // Waiting on acquire_semaphore2 imports synchronized swapchain image0 accesses into queue context.
    // The important point is that imported accesses are synchronized due to acquire semaphore wait
    // In the next step we import image0 layout transition accesses from command buffer and this replaces
    // synchronized swapchain accesses with image layout write accesses. Then Wait() synchronized all
    // accesses on default queue. In our case this removes layout transition accesses from queue context
    // (queue context gets empty).
    m_command_buffer.Begin();
    m_command_buffer.Barrier(transition_swapchain_image0);
    m_command_buffer.End();
    m_default_queue->Submit2(m_command_buffer, vkt::Wait(acquire_semaphore2));
    // QueueWaitIdle filters synchronized swapchain accesses across queue contexts (including context associated with 'semaphore')
    m_default_queue->Wait();

    // The second sequence starts by importing accesses associated with semaphore wait (from queue context
    // where this semaphore was signaled). Because this semaphore was signaled when swapchain image0
    // accesses were not synchronized yet, there is a danger (with buggy implementation) that those
    // unsynchronized accesses can be imported into queue context and they will hazard with subsequent copy
    // operation. The goal of this test is to check that implementation correctly handles this.
    // Please note, it is important the previous sequence clears queue context by doing Wait(). If queue context
    // is not cleared and, for example, still contains layout transition accesses, then even in the case of
    // regression the buggy unsynchronized accesses won't overwrite layout transition accesses (the latter have
    // newer tags), so without Wait() the test won't be able to detect regression.
    m_command_buffer.Begin();
    vk::CmdCopyBufferToImage(m_command_buffer, buffer, swapchain_image0, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy_region);
    m_command_buffer.End();
    // Test semaphore wait does not import unsynchronized swapchain accesses
    m_default_queue->Submit2(m_command_buffer, vkt::Wait(semaphore));

    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, ResyncWithSwapchain2) {
    // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/10586
    // This test is a variation of PositiveSyncValWsi.ResyncWithSwapchain that uses DeviceWaitIdle instead of QueueWaitIdle.
    // Check comments in ResyncWithSwapchain for additional details.
    TEST_DESCRIPTION("Try to introduce unsynchronized swapchain accesses after proper swapchain synchronization");
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);

    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT));

    const auto swapchain_images = m_swapchain.GetImages();
    if (swapchain_images.size() != 2) {
        GTEST_SKIP() << "The test requires swapchain with 2 images";
    }
    if (!m_swapchain.TryTransitionToPresentLayout(*m_device, *m_default_queue, m_command_pool)) {
        GTEST_SKIP() << "Failed to pre-transition swapchain images";
    }
    const VkImage swapchain_image0 = swapchain_images[0];

    vkt::Semaphore acquire_semaphore0(*m_device);
    vkt::Semaphore acquire_semaphore1(*m_device);
    vkt::Semaphore acquire_semaphore2(*m_device);
    vkt::Semaphore submit_semaphore0(*m_device);
    vkt::Semaphore submit_semaphore1(*m_device);

    // This semaphore is signaled when swapchain still uses image0
    vkt::Semaphore semaphore(*m_device);

    VkImageMemoryBarrier2 transition_swapchain_image0 = vku::InitStructHelper();
    transition_swapchain_image0.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT;
    transition_swapchain_image0.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
    transition_swapchain_image0.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    transition_swapchain_image0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    transition_swapchain_image0.image = swapchain_image0;
    transition_swapchain_image0.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    const SurfaceInformation info = GetSwapchainInfo(m_surface);
    const uint32_t width = GetSwapchainExtent(info.surface_capabilities).width;
    const uint32_t height = GetSwapchainExtent(info.surface_capabilities).height;
    const uint32_t format_size = vkuFormatTexelBlockSize(info.surface_formats[0].format);
    vkt::Buffer buffer(*m_device, width * height * format_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);

    VkBufferImageCopy copy_region{};
    copy_region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
    copy_region.imageExtent = {width, height, 1};

    // Frame 0
    uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore0, kWaitTimeout);
    if (image_index != 0) {
        GTEST_SKIP() << "This test requires the first acquired image index is 0";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore0), vkt::Signal(submit_semaphore0));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore0);
    m_default_queue->Submit2(vkt::no_cmd, vkt::Signal(semaphore));
    // Frame 1
    image_index = m_swapchain.AcquireNextImage(acquire_semaphore1, kWaitTimeout);
    if (image_index != 1) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the second acquired image index is 1";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore1), vkt::Signal(submit_semaphore1));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore1);
    // Frame 2. Re-acquire image0 that was presented in Frame0
    image_index = m_swapchain.AcquireNextImage(acquire_semaphore2, kWaitTimeout);
    if (image_index != 0) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the third acquired image index is 0";
    }

    m_command_buffer.Begin();
    m_command_buffer.Barrier(transition_swapchain_image0);
    m_command_buffer.End();
    m_default_queue->Submit2(m_command_buffer, vkt::Wait(acquire_semaphore2));
    // DeviceWaitIdle filters synchronized swapchain accesses across queue contexts
    m_device->Wait();

    m_command_buffer.Begin();
    vk::CmdCopyBufferToImage(m_command_buffer, buffer, swapchain_image0, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy_region);
    m_command_buffer.End();
    // Test semaphore wait does not import unsynchronized swapchain accesses
    m_default_queue->Submit2(m_command_buffer, vkt::Wait(semaphore));

    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, ResyncWithSwapchain3) {
    // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/10586
    // This test is a variation of PositiveSyncValWsi.ResyncWithSwapchain that uses timeline semaphore to sync queue.
    // Check comments in ResyncWithSwapchain for additional details.
    TEST_DESCRIPTION("Try to introduce unsynchronized swapchain accesses after proper swapchain synchronization");
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);
    AddRequiredFeature(vkt::Feature::timelineSemaphore);

    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT));

    const auto swapchain_images = m_swapchain.GetImages();
    if (swapchain_images.size() != 2) {
        GTEST_SKIP() << "The test requires swapchain with 2 images";
    }
    if (!m_swapchain.TryTransitionToPresentLayout(*m_device, *m_default_queue, m_command_pool)) {
        GTEST_SKIP() << "Failed to pre-transition swapchain images";
    }

    const VkImage swapchain_image0 = swapchain_images[0];

    vkt::Semaphore acquire_semaphore0(*m_device);
    vkt::Semaphore acquire_semaphore1(*m_device);
    vkt::Semaphore acquire_semaphore2(*m_device);
    vkt::Semaphore submit_semaphore0(*m_device);
    vkt::Semaphore submit_semaphore1(*m_device);

    vkt::Semaphore semaphore(*m_device);
    vkt::Semaphore timeline(*m_device, VK_SEMAPHORE_TYPE_TIMELINE);

    VkImageMemoryBarrier2 transition_swapchain_image0 = vku::InitStructHelper();
    transition_swapchain_image0.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT;
    transition_swapchain_image0.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
    transition_swapchain_image0.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    transition_swapchain_image0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    transition_swapchain_image0.image = swapchain_image0;
    transition_swapchain_image0.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    const SurfaceInformation info = GetSwapchainInfo(m_surface);
    const uint32_t width = GetSwapchainExtent(info.surface_capabilities).width;
    const uint32_t height = GetSwapchainExtent(info.surface_capabilities).height;
    const uint32_t format_size = vkuFormatTexelBlockSize(info.surface_formats[0].format);
    vkt::Buffer buffer(*m_device, width * height * format_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);

    VkBufferImageCopy copy_region{};
    copy_region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
    copy_region.imageExtent = {width, height, 1};

    // Frame 0
    uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore0, kWaitTimeout);
    if (image_index != 0) {
        GTEST_SKIP() << "This test requires the first acquired image index is 0";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore0), vkt::Signal(submit_semaphore0));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore0);
    m_default_queue->Submit2(vkt::no_cmd, vkt::Signal(semaphore));
    // Frame 1
    image_index = m_swapchain.AcquireNextImage(acquire_semaphore1, kWaitTimeout);
    if (image_index != 1) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the second acquired image index is 1";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore1), vkt::Signal(submit_semaphore1));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore1);
    // Frame 2. Re-acquire image0 that was presented in Frame0
    image_index = m_swapchain.AcquireNextImage(acquire_semaphore2, kWaitTimeout);
    if (image_index != 0) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the third acquired image index is 0";
    }

    m_command_buffer.Begin();
    m_command_buffer.Barrier(transition_swapchain_image0);
    m_command_buffer.End();
    m_default_queue->Submit2(m_command_buffer, vkt::Wait(acquire_semaphore2), vkt::TimelineSignal(timeline, 1));
    // WaitSemaphores filters synchronized swapchain accesses across queue contexts
    timeline.Wait(1, kWaitTimeout);

    m_command_buffer.Begin();
    vk::CmdCopyBufferToImage(m_command_buffer, buffer, swapchain_image0, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy_region);
    m_command_buffer.End();
    // Test semaphore wait does not import unsynchronized swapchain accesses
    m_default_queue->Submit2(m_command_buffer, vkt::Wait(semaphore));

    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, ResyncWithSwapchain4) {
    // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/10586
    // This test is a variation of PositiveSyncValWsi.ResyncWithSwapchain.
    // This scenario synchronizes with 2 swapchain images.
    // We need a swapchain with 3 images in order to acquire 2 images without blocking.
    TEST_DESCRIPTION("Try to introduce unsynchronized swapchain accesses after proper swapchain synchronization");
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);

    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSurface());

    const SurfaceInformation surface_info = GetSwapchainInfo(m_surface);
    if (surface_info.surface_capabilities.minImageCount > 3 || surface_info.surface_capabilities.maxImageCount < 3) {
        GTEST_SKIP() << "Surface must support swapchains with 3 images";
    }

    VkSwapchainCreateInfoKHR swapchain_ci = GetDefaultSwapchainCreateInfo(
        m_surface, surface_info, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);
    swapchain_ci.minImageCount = 3;
    vkt::Swapchain swapchain(*m_device, swapchain_ci);

    const auto swapchain_images = swapchain.GetImages();
    if (swapchain_images.size() != 3) {
        GTEST_SKIP() << "The test requires swapchain with 3 images";
    }
    if (!swapchain.TryTransitionToPresentLayout(*m_device, *m_default_queue, m_command_pool)) {
        GTEST_SKIP() << "Failed to pre-transition swapchain images";
    }

    const VkImage swapchain_image0 = swapchain_images[0];

    vkt::Semaphore acquire_semaphore0(*m_device);
    vkt::Semaphore acquire_semaphore1(*m_device);
    vkt::Semaphore acquire_semaphore2(*m_device);
    vkt::Semaphore acquire_semaphore3(*m_device);
    vkt::Semaphore acquire_semaphore4(*m_device);
    vkt::Semaphore submit_semaphore0(*m_device);
    vkt::Semaphore submit_semaphore1(*m_device);
    vkt::Semaphore submit_semaphore2(*m_device);

    // This semaphore is signaled when swapchain still uses image0
    vkt::Semaphore semaphore(*m_device);

    VkImageMemoryBarrier2 transition_swapchain_image = vku::InitStructHelper();
    transition_swapchain_image.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT;
    transition_swapchain_image.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
    transition_swapchain_image.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    transition_swapchain_image.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    transition_swapchain_image.image = swapchain_image0;
    transition_swapchain_image.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    const uint32_t width = GetSwapchainExtent(surface_info.surface_capabilities).width;
    const uint32_t height = GetSwapchainExtent(surface_info.surface_capabilities).height;
    const uint32_t format_size = vkuFormatTexelBlockSize(surface_info.surface_formats[0].format);
    vkt::Buffer buffer(*m_device, width * height * format_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);

    VkBufferImageCopy copy_region{};
    copy_region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
    copy_region.imageExtent = {width, height, 1};

    // Frame 0
    uint32_t image_index = swapchain.AcquireNextImage(acquire_semaphore0, kWaitTimeout);
    if (image_index != 0) {
        GTEST_SKIP() << "This test requires the first acquired image index is 0";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore0), vkt::Signal(submit_semaphore0));
    m_default_queue->Present(swapchain, image_index, submit_semaphore0);
    m_default_queue->Submit2(vkt::no_cmd, vkt::Signal(semaphore));

    // Frame 1
    image_index = swapchain.AcquireNextImage(acquire_semaphore1, kWaitTimeout);
    if (image_index != 1) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the second acquired image index is 1";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore1), vkt::Signal(submit_semaphore1));
    m_default_queue->Present(swapchain, image_index, submit_semaphore1);

    // Frame 2
    image_index = swapchain.AcquireNextImage(acquire_semaphore2, kWaitTimeout);
    if (image_index != 2) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the third acquired image index is 2";
    }
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore2), vkt::Signal(submit_semaphore2));
    m_default_queue->Present(swapchain, image_index, submit_semaphore2);

    // Frame 3. Re-acquire image0 that was presented in Frame0.
    // Also re-acquire image1 that was presented in Frame1.
    image_index = swapchain.AcquireNextImage(acquire_semaphore3, kWaitTimeout);
    if (image_index != 0) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the fourth acquired image index is 0";
    }

    uint32_t image_index2 = swapchain.AcquireNextImage(acquire_semaphore4, kWaitTimeout);
    if (image_index2 != 1) {
        m_default_queue->Wait();
        GTEST_SKIP() << "This test requires the fifth acquired image index is 1";
    }

    m_command_buffer.Begin();
    m_command_buffer.Barrier(transition_swapchain_image);
    m_command_buffer.End();
    // Import accesses from two swapchain images before applying command buffer accesses
    // (layout transition of the first image). Test that implementation properly tracks
    // multiple synchronized swapchain accesses (image0 and image1) accross all queue contexts
    // where these accesses are registered.
    m_default_queue->Submit2(vkt::no_cmd, vkt::Wait(acquire_semaphore3));
    m_default_queue->Submit2(m_command_buffer, vkt::Wait(acquire_semaphore4));
    // QueueWaitIdle filters synchronized swapchain accesses across queue contexts
    m_default_queue->Wait();

    m_command_buffer.Begin();
    vk::CmdCopyBufferToImage(m_command_buffer, buffer, swapchain_image0, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy_region);
    m_command_buffer.End();
    // Without proper tracking of multiple synchronized accesses the buggy implementation might remember only
    // the last one (image1 accesses synced by acquire_semaphore4) and can forget about image0 accesses synced
    // by acquire_semaphore3. By waiting on 'semaphore' that was signaled after image0 presentation we test
    // that imlementation remembers that image0 accesses were already synced and does not import them as
    // unsynchronized accesses (in that case they will hazard with command buffer copy operation).
    m_default_queue->Submit2(m_command_buffer, vkt::Wait(semaphore));

    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, PresentWithPrimaryLayoutTransitions) {
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    vkt::Semaphore acquire_semaphore(*m_device);
    vkt::Semaphore submit_semaphore(*m_device);
    const auto swapchain_images = m_swapchain.GetImages();
    const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);

    VkImageMemoryBarrier2 layout_transition_write = vku::InitStructHelper();
    layout_transition_write.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition_write.srcAccessMask = VK_ACCESS_2_NONE;
    layout_transition_write.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition_write.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
    layout_transition_write.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    layout_transition_write.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    layout_transition_write.image = swapchain_images[image_index];
    layout_transition_write.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    VkImageMemoryBarrier2 layout_transition_present = vku::InitStructHelper();
    layout_transition_present.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition_present.srcAccessMask = VK_ACCESS_2_NONE;
    layout_transition_present.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition_present.dstAccessMask = VK_ACCESS_2_NONE;
    layout_transition_present.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    layout_transition_present.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    layout_transition_present.image = swapchain_images[image_index];
    layout_transition_present.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    m_command_buffer.Begin();
    m_command_buffer.Barrier(layout_transition_write);
    m_command_buffer.Barrier(layout_transition_present);
    m_command_buffer.End();

    m_default_queue->Submit2(m_command_buffer, vkt::Wait(acquire_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT),
                             vkt::Signal(submit_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore);
    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, PresentWithSecondaryLayoutTransitions) {
    // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/10693
    TEST_DESCRIPTION("Test propagation of layout transition barriers in the context of submit time validation (ExecuteCommands)");
    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    vkt::Semaphore acquire_semaphore(*m_device);
    vkt::Semaphore submit_semaphore(*m_device);
    const auto swapchain_images = m_swapchain.GetImages();
    const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);

    VkImageMemoryBarrier2 layout_transition_write = vku::InitStructHelper();
    layout_transition_write.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition_write.srcAccessMask = VK_ACCESS_2_NONE;
    layout_transition_write.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition_write.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
    layout_transition_write.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    layout_transition_write.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    layout_transition_write.image = swapchain_images[image_index];
    layout_transition_write.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    VkImageMemoryBarrier2 layout_transition_present = vku::InitStructHelper();
    layout_transition_present.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition_present.srcAccessMask = VK_ACCESS_2_NONE;
    layout_transition_present.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
    layout_transition_present.dstAccessMask = VK_ACCESS_2_NONE;
    layout_transition_present.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    layout_transition_present.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    layout_transition_present.image = swapchain_images[image_index];
    layout_transition_present.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    vkt::CommandBuffer cmd_barrier_write(*m_device, m_command_pool, VK_COMMAND_BUFFER_LEVEL_SECONDARY);
    cmd_barrier_write.Begin();
    cmd_barrier_write.Barrier(layout_transition_write);
    cmd_barrier_write.End();

    vkt::CommandBuffer cmd_barrier_present(*m_device, m_command_pool, VK_COMMAND_BUFFER_LEVEL_SECONDARY);
    cmd_barrier_present.Begin();
    cmd_barrier_present.Barrier(layout_transition_present);
    cmd_barrier_present.End();

    m_command_buffer.Begin();
    m_command_buffer.ExecuteCommands(cmd_barrier_write);
    m_command_buffer.ExecuteCommands(cmd_barrier_present);
    m_command_buffer.End();

    m_default_queue->Submit2(m_command_buffer, vkt::Wait(acquire_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT),
                             vkt::Signal(submit_semaphore, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT));
    m_default_queue->Present(m_swapchain, image_index, submit_semaphore);
    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, BindSwapchainImage) {
    // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/9787
    TEST_DESCRIPTION("Bind custom swapchain images. Do not retrieve images using GetSwapchainImagesKHR");

    SetTargetApiVersion(VK_API_VERSION_1_1);
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    vkt::CommandBuffer cb0(*m_device, m_command_pool);
    vkt::CommandBuffer cb1(*m_device, m_command_pool);

    const uint32_t image_count = m_swapchain.GetImageCount();
    if (image_count < 2) {
        GTEST_SKIP() << "The test requires swapchain with at least 2 images";
    }

    VkImageSwapchainCreateInfoKHR image_swapchain_ci = vku::InitStructHelper();
    image_swapchain_ci.swapchain = m_swapchain;

    VkImageCreateInfo image_ci = vku::InitStructHelper();
    image_ci.pNext = &image_swapchain_ci;
    image_ci.imageType = VK_IMAGE_TYPE_2D;
    image_ci.format = m_surface_formats[0].format;
    image_ci.extent.width = GetSwapchainExtent(m_surface_capabilities).width;
    image_ci.extent.height = GetSwapchainExtent(m_surface_capabilities).height;
    image_ci.extent.depth = 1;
    image_ci.mipLevels = 1;
    image_ci.arrayLayers = 1;
    image_ci.samples = VK_SAMPLE_COUNT_1_BIT;
    image_ci.tiling = VK_IMAGE_TILING_OPTIMAL;
    image_ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    image_ci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
    image_ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    std::vector<vkt::Image> images;
    for (uint32_t i = 0; i < image_count; i++) {
        images.emplace_back(*m_device, image_ci, vkt::no_mem);

        VkBindImageMemorySwapchainInfoKHR bind_swapchain_info = vku::InitStructHelper();
        bind_swapchain_info.swapchain = m_swapchain;
        bind_swapchain_info.imageIndex = i;

        VkBindImageMemoryInfo bind_info = vku::InitStructHelper(&bind_swapchain_info);
        bind_info.image = images.back();

        vk::BindImageMemory2(device(), 1, &bind_info);
    }

    vkt::Semaphore acquire_semaphore0(*m_device);
    vkt::Semaphore submit_semaphore0(*m_device);

    vkt::Semaphore acquire_semaphore1(*m_device);
    vkt::Semaphore submit_semaphore1(*m_device);

    const uint32_t image_index0 = m_swapchain.AcquireNextImage(acquire_semaphore0, kWaitTimeout);
    cb0.Begin();
    cb0.TransitionLayout(images[image_index0], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
    cb0.End();
    m_default_queue->Submit(cb0, vkt::Wait(acquire_semaphore0), vkt::Signal(submit_semaphore0));
    m_default_queue->Present(m_swapchain, image_index0, submit_semaphore0);

    const uint32_t image_index1 = m_swapchain.AcquireNextImage(acquire_semaphore1, kWaitTimeout);
    cb1.Begin();
    cb1.TransitionLayout(images[image_index1], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
    cb1.End();
    m_default_queue->Submit(cb1, vkt::Wait(acquire_semaphore1), vkt::Signal(submit_semaphore1));
    m_default_queue->Present(m_swapchain, image_index1, submit_semaphore1);

    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, BindSwapchainImage2) {
    // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/9787
    TEST_DESCRIPTION("Bind custom swapchain images. Do not retrieve images using GetSwapchainImagesKHR");

    SetTargetApiVersion(VK_API_VERSION_1_3);
    AddSurfaceExtension();
    AddRequiredFeature(vkt::Feature::synchronization2);
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    const uint32_t image_count = m_swapchain.GetImageCount();
    if (image_count < 2) {
        GTEST_SKIP() << "The test requires swapchain with at least 2 images";
    }

    VkImageSwapchainCreateInfoKHR image_swapchain_ci = vku::InitStructHelper();
    image_swapchain_ci.swapchain = m_swapchain;

    VkImageCreateInfo image_ci = vku::InitStructHelper();
    image_ci.pNext = &image_swapchain_ci;
    image_ci.imageType = VK_IMAGE_TYPE_2D;
    image_ci.format = m_surface_formats[0].format;
    image_ci.extent.width = GetSwapchainExtent(m_surface_capabilities).width;
    image_ci.extent.height = GetSwapchainExtent(m_surface_capabilities).height;
    image_ci.extent.depth = 1;
    image_ci.mipLevels = 1;
    image_ci.arrayLayers = 1;
    image_ci.samples = VK_SAMPLE_COUNT_1_BIT;
    image_ci.tiling = VK_IMAGE_TILING_OPTIMAL;
    image_ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    image_ci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
    image_ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    std::vector<vkt::Image> images;
    for (uint32_t i = 0; i < image_count; i++) {
        images.emplace_back(*m_device, image_ci, vkt::no_mem);

        VkBindImageMemorySwapchainInfoKHR bind_swapchain_info = vku::InitStructHelper();
        bind_swapchain_info.swapchain = m_swapchain;
        bind_swapchain_info.imageIndex = i;

        VkBindImageMemoryInfo bind_info = vku::InitStructHelper(&bind_swapchain_info);
        bind_info.image = images.back();

        vk::BindImageMemory2(device(), 1, &bind_info);
    }

    vkt::Semaphore acquire_semaphore0(*m_device);
    vkt::Semaphore submit_semaphore0(*m_device);
    vkt::CommandBuffer command_buffer0(*m_device, m_command_pool);

    vkt::Semaphore acquire_semaphore1(*m_device);
    vkt::Semaphore submit_semaphore1(*m_device);
    vkt::CommandBuffer command_buffer1(*m_device, m_command_pool);

    VkImageMemoryBarrier2 layout_transition = vku::InitStructHelper();
    layout_transition.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
    layout_transition.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
    layout_transition.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    layout_transition.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    layout_transition.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    command_buffer0.Begin();
    layout_transition.image = images[0];
    command_buffer0.Barrier(layout_transition);
    command_buffer0.End();

    const uint32_t image_index0 = m_swapchain.AcquireNextImage(acquire_semaphore0, kWaitTimeout);
    m_default_queue->Submit2(command_buffer0, vkt::Wait(acquire_semaphore0), vkt::Signal(submit_semaphore0));
    m_default_queue->Present(m_swapchain, image_index0, submit_semaphore0);

    command_buffer1.Begin();
    layout_transition.image = images[1];
    command_buffer1.Barrier(layout_transition);
    command_buffer1.End();

    const uint32_t image_index1 = m_swapchain.AcquireNextImage(acquire_semaphore1, kWaitTimeout);
    m_default_queue->Submit2(command_buffer1, vkt::Wait(acquire_semaphore1), vkt::Signal(submit_semaphore1));
    m_default_queue->Present(m_swapchain, image_index1, submit_semaphore1);

    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, PresentAfterAcquire) {
    TEST_DESCRIPTION("Present waits for acquire's semaphore");
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    if (!m_swapchain.TryTransitionToPresentLayout(*m_device, *m_default_queue, m_command_pool)) {
        GTEST_SKIP() << "Failed to pre-transition swapchain images";
    }

    // Test that present can directly wait for acquire semaphore
    vkt::Semaphore acquire_semaphore(*m_device);
    const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);
    m_default_queue->Present(m_swapchain, image_index, acquire_semaphore);
    m_default_queue->Wait();
}

TEST_F(PositiveSyncValWsi, ConcurrentPresentAndSubmit) {
    // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/11226
    TEST_DESCRIPTION("Present and Submit to different queues at the same time");
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSwapchain());

    if (!m_second_queue) {
        GTEST_SKIP() << "Test requires two queues";
    }
    if (!m_swapchain.TryTransitionToPresentLayout(*m_device, *m_default_queue, m_command_pool)) {
        GTEST_SKIP() << "Failed to pre-transition swapchain images";
    }

    const auto swapchain_images = m_swapchain.GetImages();
    const int N = 100;  // Initially 500 for higher repro rate

    std::atomic<bool> bailout{false};
    monitor_.SetBailout(&bailout);

    // QueueSubmit on the second queue and QueuePresent on the main queue run in parallel.
    // It is a valid setup because submits to different queues do not require synchronization.
    std::thread thread([&] {
        vkt::Fence fence(*m_device);
        for (int i = 0; i < N; i++) {
            m_second_queue->Submit(vkt::no_cmd, fence);
            fence.Wait(kWaitTimeout);
            fence.Reset();
            if (bailout.load()) {
                break;
            }
        }
    });
    {
        vkt::Semaphore acquire_semaphore(*m_device);
        std::vector<vkt::Semaphore> present_wait_semaphores;
        for (size_t k = 0; k < swapchain_images.size(); k++) {
            present_wait_semaphores.emplace_back(*m_device);
        }
        for (int i = 0; i < N; i++) {
            const uint32_t image_index = m_swapchain.AcquireNextImage(acquire_semaphore, kWaitTimeout);
            m_default_queue->Submit(vkt::no_cmd, vkt::Wait(acquire_semaphore), vkt::Signal(present_wait_semaphores[image_index]));
            m_default_queue->Present(m_swapchain, image_index, present_wait_semaphores[image_index]);
            m_default_queue->Wait();
            if (bailout.load()) {
                break;
            }
        }
    }
    thread.join();
    monitor_.SetBailout(nullptr);
}

TEST_F(PositiveSyncValWsi, WaitAcquireFenceForDestoryedSwapchain) {
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());
    RETURN_IF_SKIP(InitSurface());
    InitSwapchainInfo();

    const SurfaceInformation surface_info = GetSwapchainInfo(m_surface);
    const VkSwapchainCreateInfoKHR swapchain_ci = GetDefaultSwapchainCreateInfo(m_surface, surface_info);
    vkt::Swapchain swapchain(*m_device, swapchain_ci);

    vkt::Fence fence(*m_device);
    [[maybe_unused]] const uint32_t image_index = swapchain.AcquireNextImage(fence, kWaitTimeout);
    swapchain.Destroy();
    fence.Wait(kWaitTimeout);
}

TEST_F(PositiveSyncValWsi, ConcurrentPresentMultipleSwapchains) {
    TEST_DESCRIPTION("https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/8576");
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());

    const uint32_t num_threads = 6; /* Initially 8 */
    const uint32_t num_frames_in_flight = 2;
    const uint32_t N = 100; /* Initially 200 */

    std::mutex queue_mutex;

    std::vector<SurfaceContext> surface_contexts(num_threads);
    std::vector<vkt::Surface> surfaces(num_threads);
    std::vector<vkt::Swapchain> swapchains;

    for (uint32_t i = 0; i < num_threads; i++) {
        if (CreateSurface(surface_contexts[i], surfaces[i]) != VK_SUCCESS) {
            GTEST_SKIP() << "Failed to create surface " << i;
        }
        const SurfaceInformation surface_info = GetSwapchainInfo(surfaces[i]);
        const VkSwapchainCreateInfoKHR swapchain_ci = GetDefaultSwapchainCreateInfo(surfaces[i], surface_info);
        swapchains.emplace_back(*m_device, swapchain_ci);
    }

    std::atomic<bool> bailout{false};
    monitor_.SetBailout(&bailout);

    auto presenter_thread = [&, this](uint32_t thread_index) {
        vkt::Swapchain& swapchain = swapchains[thread_index];
        const auto swapchain_images = swapchain.GetImages();

        std::vector<vkt::Semaphore> acquire_semaphores;
        std::vector<vkt::Semaphore> present_semaphores;
        std::vector<vkt::Fence> frame_fences;

        vkt::CommandPool command_pool(*m_device, m_default_queue->family_index, VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
        std::vector<vkt::CommandBuffer> command_buffers;
        for (uint32_t i = 0; i < num_frames_in_flight; i++) {
            acquire_semaphores.emplace_back(*m_device);
            frame_fences.emplace_back(*m_device, VK_FENCE_CREATE_SIGNALED_BIT);
            command_buffers.emplace_back(*m_device, command_pool);
        }
        for (uint32_t i = 0; i < swapchain.GetImageCount(); i++) {
            present_semaphores.emplace_back(*m_device);
        }
        uint32_t current_frame = 0;
        for (uint32_t i = 0; i < N; i++) {
            const uint32_t frame_index = current_frame % num_frames_in_flight;
            frame_fences[frame_index].Wait(kWaitTimeout);
            frame_fences[frame_index].Reset();

            const uint32_t image_index = swapchain.AcquireNextImage(acquire_semaphores[frame_index], kWaitTimeout);

            vkt::CommandBuffer& command_buffer = command_buffers[frame_index];
            command_buffer.Begin();
            command_buffer.TransitionLayout(swapchain_images[image_index], VK_IMAGE_LAYOUT_UNDEFINED,
                                            VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
            command_buffer.End();

            {
                std::unique_lock<std::mutex> lock(queue_mutex);
                m_default_queue->Submit(command_buffer, vkt::Wait(acquire_semaphores[frame_index]),
                                        vkt::Signal(present_semaphores[image_index]), frame_fences[frame_index]);
            }
            {
                std::unique_lock<std::mutex> lock(queue_mutex);
                m_default_queue->Present(swapchain, image_index, present_semaphores[image_index]);
            }
            current_frame++;
            if (bailout.load()) {
                break;
            }
        }
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            m_default_queue->Wait();
        }
    };
    std::vector<std::thread> presenters;
    for (int i = 0; i < num_threads; i++) {
        presenters.emplace_back(presenter_thread, i);
    }

    for (auto& presenter : presenters) {
        presenter.join();
    }
    monitor_.SetBailout(nullptr);
}

TEST_F(PositiveSyncValWsi, WaitForFencesClearsLastSynchronizedPresents) {
    // It's a single threaded deterministic version of ConcurrentPresentMultipleSwapchains.
    // If present access is marked as synchronized (last_synchronized_present in syncval implementation),
    // then WaitForFences must clear such accesses.
    TEST_DESCRIPTION("https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/8576");
    AddSurfaceExtension();
    RETURN_IF_SKIP(InitSyncVal());

    struct SwapchainContext {
        SurfaceContext surface_context;
        vkt::Surface surface;
        vkt::Swapchain swapchain;
        std::vector<VkImage> swapchain_images;
        vkt::Fence frame_fence;
        vkt::CommandBuffer command_buffer;
        vkt::Semaphore acquire_semaphore;
        std::vector<vkt::Semaphore> present_semaphores;

        void Init(PositiveSyncValWsi& test, vkt::CommandPool& command_pool) {
            if (test.CreateSurface(surface_context, surface) != VK_SUCCESS) {
                GTEST_SKIP() << "Failed to create surfac";
            }
            const SurfaceInformation surface_info = test.GetSwapchainInfo(surface);
            const VkSwapchainCreateInfoKHR swapchain_ci = GetDefaultSwapchainCreateInfo(surface, surface_info);
            swapchain = vkt::Swapchain(*test.DeviceObj(), swapchain_ci);
            swapchain_images = swapchain.GetImages();
            if (swapchain_images.size() != 2) {
                GTEST_SKIP() << "The test requires swapchain with 2 images";
            }
            frame_fence = vkt::Fence(*test.DeviceObj(), VK_FENCE_CREATE_SIGNALED_BIT);
            command_buffer = vkt::CommandBuffer(*test.DeviceObj(), command_pool);
            acquire_semaphore = vkt::Semaphore(*test.DeviceObj());
            for (size_t i = 0; i < swapchain_images.size(); i++) {
                present_semaphores.emplace_back(*test.DeviceObj());
            }
        }
    };
    SwapchainContext ctx_a;
    SwapchainContext ctx_b;
    RETURN_IF_SKIP(ctx_a.Init(*this, m_command_pool));
    RETURN_IF_SKIP(ctx_b.Init(*this, m_command_pool));

    auto begin_frame = [&](SwapchainContext& ctx) {
        ctx.frame_fence.Wait(kWaitTimeout);
        ctx.frame_fence.Reset();

        const uint32_t image_index = ctx.swapchain.AcquireNextImage(ctx.acquire_semaphore, kWaitTimeout);
        ctx.command_buffer.Begin();
        ctx.command_buffer.TransitionLayout(ctx.swapchain_images[image_index], VK_IMAGE_LAYOUT_UNDEFINED,
                                            VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
        ctx.command_buffer.End();
        return image_index;
    };
    auto submit = [&](SwapchainContext& ctx, uint32_t image_index) {
        m_default_queue->Submit(ctx.command_buffer, vkt::Wait(ctx.acquire_semaphore),
                                vkt::Signal(ctx.present_semaphores[image_index]), ctx.frame_fence);
    };
    auto present = [&](SwapchainContext& ctx, uint32_t image_index) {
        m_default_queue->Present(ctx.swapchain, image_index, ctx.present_semaphores[image_index]);
    };

    // Re-acquire image on swapchain A (need image that was already presented)
    {
        uint32_t image_index = begin_frame(ctx_a);
        if (image_index != 0) {
            GTEST_SKIP() << "Expected image index 0";
        }
        submit(ctx_a, image_index);
        present(ctx_a, image_index);

        image_index = begin_frame(ctx_a);
        if (image_index != 1) {
            m_default_queue->Wait();
            GTEST_SKIP() << "Expected image index 1";
        }
        submit(ctx_a, image_index);
        present(ctx_a, image_index);

        image_index = begin_frame(ctx_a);
        if (image_index != 0) {  // get image 0 again
            m_default_queue->Wait();
            GTEST_SKIP() << "Expected image index 0";
        }
    }

    // Run frame on B. This imports A's synchronized present access into ctx B.
    // (synchronized because image was re-acquired)
    {
        const uint32_t image_index = begin_frame(ctx_b);
        if (image_index != 0) {
            m_default_queue->Wait();
            GTEST_SKIP() << "Expected image index 0";
        }
        submit(ctx_b, image_index);
        present(ctx_b, image_index);
    }

    // Submit layout transition on A
    submit(ctx_a, 0);

    {
        // Import A's layout transition into B (via last batch)
        const uint32_t image_index = begin_frame(ctx_b);
        if (image_index != 1) {
            m_default_queue->Wait();
            GTEST_SKIP() << "Expected image index 1";
        }
        submit(ctx_b, image_index);
        present(ctx_b, image_index);

        // Import synchronized present accesses from B's image 0
        // Fence wait removes imported A's layout transitions.
        // In the original issue this did not remove *already synchronized present accesses* though
        const uint32_t image_index2 = begin_frame(ctx_b);
        if (image_index2 != 0) {
            m_default_queue->Wait();
            GTEST_SKIP() << "Expected image index 0";
        }
        submit(ctx_b, image_index2);
    }

    // This caused WRITE-AFTER-PRESENT hazard in the original issue
    present(ctx_a, 0);

    m_default_queue->Wait();
}
