blob: 3633e4ceea609abf53ab0dd8e52206faa434c709 [file]
/* 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 "../framework/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);
for (int i = 0; i < N; i++) {
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();
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();
}