Make semaphores unique per swap chain image

Fixes #1210
This commit is contained in:
Sascha Willems 2025-06-09 19:57:47 +02:00
parent 422d54e387
commit 64ba099002
4 changed files with 77 additions and 59 deletions

View file

@ -3,7 +3,7 @@
* *
* A swap chain is a collection of framebuffers used for rendering and presentation to the windowing system * A swap chain is a collection of framebuffers used for rendering and presentation to the windowing system
* *
* Copyright (C) 2016-2024 by Sascha Willems - www.saschawillems.de * Copyright (C) 2016-2025 by Sascha Willems - www.saschawillems.de
* *
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) * This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
*/ */
@ -341,7 +341,6 @@ void VulkanSwapChain::create(uint32_t& width, uint32_t& height, bool vsync, bool
} }
vkDestroySwapchainKHR(device, oldSwapchain, nullptr); vkDestroySwapchainKHR(device, oldSwapchain, nullptr);
} }
uint32_t imageCount{ 0 };
VK_CHECK_RESULT(vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr)); VK_CHECK_RESULT(vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr));
// Get the swap chain images // Get the swap chain images

View file

@ -3,7 +3,7 @@
* *
* A swap chain is a collection of framebuffers used for rendering and presentation to the windowing system * A swap chain is a collection of framebuffers used for rendering and presentation to the windowing system
* *
* Copyright (C) 2016-2024 by Sascha Willems - www.saschawillems.de * Copyright (C) 2016-2025 by Sascha Willems - www.saschawillems.de
* *
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) * This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
*/ */
@ -41,6 +41,7 @@ public:
std::vector<VkImage> images{}; std::vector<VkImage> images{};
std::vector<VkImageView> imageViews{}; std::vector<VkImageView> imageViews{};
uint32_t queueNodeIndex{ UINT32_MAX }; uint32_t queueNodeIndex{ UINT32_MAX };
uint32_t imageCount{ 0 };
#if defined(VK_USE_PLATFORM_WIN32_KHR) #if defined(VK_USE_PLATFORM_WIN32_KHR)
void initSurface(void* platformHandle, void* platformWindow); void initSurface(void* platformHandle, void* platformWindow);

View file

@ -103,15 +103,16 @@ public:
// Synchronization is an important concept of Vulkan that OpenGL mostly hid away. Getting this right is crucial to using Vulkan. // Synchronization is an important concept of Vulkan that OpenGL mostly hid away. Getting this right is crucial to using Vulkan.
// Semaphores are used to coordinate operations within the graphics queue and ensure correct command ordering // Semaphores are used to coordinate operations within the graphics queue and ensure correct command ordering
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> presentCompleteSemaphores{}; std::vector<VkSemaphore> presentCompleteSemaphores{};
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> renderCompleteSemaphores{}; std::vector<VkSemaphore> renderCompleteSemaphores{};
VkCommandPool commandPool{ VK_NULL_HANDLE }; VkCommandPool commandPool{ VK_NULL_HANDLE };
std::array<VkCommandBuffer, MAX_CONCURRENT_FRAMES> commandBuffers{}; std::array<VkCommandBuffer, MAX_CONCURRENT_FRAMES> commandBuffers{};
std::array<VkFence, MAX_CONCURRENT_FRAMES> waitFences{}; std::array<VkFence, MAX_CONCURRENT_FRAMES> waitFences{};
// To select the correct sync objects, we need to keep track of the current frame // To select the correct sync and command objects, we need to keep track of the current frame and (swapchain) image index
uint32_t currentFrame{ 0 }; uint32_t currentFrame{ 0 };
uint32_t currentSemaphore{ 0 };
VulkanExample() : VulkanExampleBase() VulkanExample() : VulkanExampleBase()
{ {
@ -130,27 +131,28 @@ public:
{ {
// Clean up used Vulkan resources // Clean up used Vulkan resources
// Note: Inherited destructor cleans up resources stored in base class // Note: Inherited destructor cleans up resources stored in base class
if (device) {
vkDestroyPipeline(device, pipeline, nullptr); vkDestroyPipeline(device, pipeline, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr); vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
vkDestroyBuffer(device, vertices.buffer, nullptr); vkDestroyBuffer(device, vertices.buffer, nullptr);
vkFreeMemory(device, vertices.memory, nullptr); vkFreeMemory(device, vertices.memory, nullptr);
vkDestroyBuffer(device, indices.buffer, nullptr); vkDestroyBuffer(device, indices.buffer, nullptr);
vkFreeMemory(device, indices.memory, nullptr); vkFreeMemory(device, indices.memory, nullptr);
vkDestroyCommandPool(device, commandPool, nullptr); vkDestroyCommandPool(device, commandPool, nullptr);
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
vkDestroySemaphore(device, presentCompleteSemaphores[i], nullptr);
}
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
}
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) { for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
vkDestroyFence(device, waitFences[i], nullptr); vkDestroyFence(device, waitFences[i], nullptr);
vkDestroySemaphore(device, presentCompleteSemaphores[i], nullptr);
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
vkDestroyBuffer(device, uniformBuffers[i].buffer, nullptr); vkDestroyBuffer(device, uniformBuffers[i].buffer, nullptr);
vkFreeMemory(device, uniformBuffers[i].memory, nullptr); vkFreeMemory(device, uniformBuffers[i].memory, nullptr);
} }
} }
}
// This function is used to request a device memory type that supports all the property flags we request (e.g. device local, host visible) // This function is used to request a device memory type that supports all the property flags we request (e.g. device local, host visible)
// Upon success it will return the index of the memory type that fits our requested memory properties // Upon success it will return the index of the memory type that fits our requested memory properties
@ -178,24 +180,25 @@ public:
// Create the per-frame (in flight) Vulkan synchronization primitives used in this example // Create the per-frame (in flight) Vulkan synchronization primitives used in this example
void createSynchronizationPrimitives() void createSynchronizationPrimitives()
{ {
// Semaphores are used for correct command ordering within a queue
VkSemaphoreCreateInfo semaphoreCI{};
semaphoreCI.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
// Fences are used to check draw command buffer completion on the host // Fences are used to check draw command buffer completion on the host
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
VkFenceCreateInfo fenceCI{}; VkFenceCreateInfo fenceCI{};
fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
// Create the fences in signaled state (so we don't wait on first render of each command buffer) // Create the fences in signaled state (so we don't wait on first render of each command buffer)
fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT; fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT;
// Fence used to ensure that command buffer has completed exection before using it again
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) { VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
}
// Semaphores are per swapchain image
presentCompleteSemaphores.resize(swapChain.images.size());
renderCompleteSemaphores.resize(swapChain.images.size());
for (size_t i = 0; i < swapChain.images.size(); i++) {
// Semaphores are used for correct command ordering within a queue
VkSemaphoreCreateInfo semaphoreCI{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
// Semaphore used to ensure that image presentation is complete before starting to submit again // Semaphore used to ensure that image presentation is complete before starting to submit again
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &presentCompleteSemaphores[i])); VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &presentCompleteSemaphores[i]));
// Semaphore used to ensure that all commands submitted have been finished before submitting the image to the queue // Semaphore used to ensure that all commands submitted have been finished before submitting the image to the queue
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &renderCompleteSemaphores[i])); VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &renderCompleteSemaphores[i]));
// Fence used to ensure that command buffer has completed exection before using it again
VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
} }
} }
@ -912,7 +915,7 @@ public:
// Get the next swap chain image from the implementation // Get the next swap chain image from the implementation
// Note that the implementation is free to return the images in any order, so we must use the acquire function and can't just cycle through the images/imageIndex on our own // Note that the implementation is free to return the images in any order, so we must use the acquire function and can't just cycle through the images/imageIndex on our own
uint32_t imageIndex; uint32_t imageIndex;
VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentSemaphore], VK_NULL_HANDLE, &imageIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR) { if (result == VK_ERROR_OUT_OF_DATE_KHR) {
windowResize(); windowResize();
return; return;
@ -1008,10 +1011,10 @@ public:
submitInfo.commandBufferCount = 1; // We submit a single command buffer submitInfo.commandBufferCount = 1; // We submit a single command buffer
// Semaphore to wait upon before the submitted command buffer starts executing // Semaphore to wait upon before the submitted command buffer starts executing
submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentFrame]; submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentSemaphore];
submitInfo.waitSemaphoreCount = 1; submitInfo.waitSemaphoreCount = 1;
// Semaphore to be signaled when command buffers have completed // Semaphore to be signaled when command buffers have completed
submitInfo.pSignalSemaphores = &renderCompleteSemaphores[currentFrame]; submitInfo.pSignalSemaphores = &renderCompleteSemaphores[currentSemaphore];
submitInfo.signalSemaphoreCount = 1; submitInfo.signalSemaphoreCount = 1;
// Submit to the graphics queue passing a wait fence // Submit to the graphics queue passing a wait fence
@ -1024,7 +1027,7 @@ public:
VkPresentInfoKHR presentInfo{}; VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1; presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = &renderCompleteSemaphores[currentFrame]; presentInfo.pWaitSemaphores = &renderCompleteSemaphores[currentSemaphore];
presentInfo.swapchainCount = 1; presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapChain.swapChain; presentInfo.pSwapchains = &swapChain.swapChain;
presentInfo.pImageIndices = &imageIndex; presentInfo.pImageIndices = &imageIndex;
@ -1039,6 +1042,8 @@ public:
// Select the next frame to render to, based on the max. no. of concurrent frames // Select the next frame to render to, based on the max. no. of concurrent frames
currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES; currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
// Similar for the semaphores, which need to be unique to the swapchain images
currentSemaphore = (currentSemaphore + 1) % swapChain.imageCount;
} }
}; };

View file

@ -5,7 +5,7 @@
* This is a variation of the the triangle sample that makes use of Vulkan 1.3 features * This is a variation of the the triangle sample that makes use of Vulkan 1.3 features
* This simplifies the api a bit, esp. with dynamic rendering replacing render passes (and with that framebuffers) * This simplifies the api a bit, esp. with dynamic rendering replacing render passes (and with that framebuffers)
* *
* Copyright (C) 2024 by Sascha Willems - www.saschawillems.de * Copyright (C) 2024-2025 by Sascha Willems - www.saschawillems.de
* *
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) * This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
*/ */
@ -87,16 +87,17 @@ public:
// Synchronization primitives // Synchronization primitives
// Synchronization is an important concept of Vulkan that OpenGL mostly hid away. Getting this right is crucial to using Vulkan. // Synchronization is an important concept of Vulkan that OpenGL mostly hid away. Getting this right is crucial to using Vulkan.
// Semaphores are used to coordinate operations within the graphics queue and ensure correct command ordering // Semaphores are used to coordinate operations within the graphics queue and ensure correct command ordering
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> presentCompleteSemaphores{}; std::vector<VkSemaphore> presentCompleteSemaphores{};
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> renderCompleteSemaphores{}; std::vector<VkSemaphore> renderCompleteSemaphores{};
// Fences are used to make sure command buffers aren't rerecorded until they've finished executing // Fences are used to make sure command buffers aren't rerecorded until they've finished executing
std::array<VkFence, MAX_CONCURRENT_FRAMES> waitFences{}; std::array<VkFence, MAX_CONCURRENT_FRAMES> waitFences{};
VkCommandPool commandPool{ VK_NULL_HANDLE }; VkCommandPool commandPool{ VK_NULL_HANDLE };
std::array<VkCommandBuffer, MAX_CONCURRENT_FRAMES> commandBuffers{}; std::array<VkCommandBuffer, MAX_CONCURRENT_FRAMES> commandBuffers{};
// To select the correct sync and command objects, we need to keep track of the current frame // To select the correct sync and command objects, we need to keep track of the current frame and (swapchain) image index
uint32_t currentFrame{ 0 }; uint32_t currentFrame{ 0 };
uint32_t currentSemaphore{ 0 };
VkPhysicalDeviceVulkan13Features enabledFeatures{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES }; VkPhysicalDeviceVulkan13Features enabledFeatures{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES };
@ -130,10 +131,14 @@ public:
vkDestroyBuffer(device, indexBuffer.handle, nullptr); vkDestroyBuffer(device, indexBuffer.handle, nullptr);
vkFreeMemory(device, indexBuffer.memory, nullptr); vkFreeMemory(device, indexBuffer.memory, nullptr);
vkDestroyCommandPool(device, commandPool, nullptr); vkDestroyCommandPool(device, commandPool, nullptr);
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
vkDestroySemaphore(device, presentCompleteSemaphores[i], nullptr);
}
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
}
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) { for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
vkDestroyFence(device, waitFences[i], nullptr); vkDestroyFence(device, waitFences[i], nullptr);
vkDestroySemaphore(device, presentCompleteSemaphores[i], nullptr);
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
vkDestroyBuffer(device, uniformBuffers[i].handle, nullptr); vkDestroyBuffer(device, uniformBuffers[i].handle, nullptr);
vkFreeMemory(device, uniformBuffers[i].memory, nullptr); vkFreeMemory(device, uniformBuffers[i].memory, nullptr);
} }
@ -166,21 +171,27 @@ public:
throw "Could not find a suitable memory type!"; throw "Could not find a suitable memory type!";
} }
// Create the per-frame (in flight) Vulkan synchronization primitives used in this example // Create the per-frame (in flight) and per (swapchain image) Vulkan synchronization primitives used in this example
void createSynchronizationPrimitives() void createSynchronizationPrimitives()
{ {
// Fences are per frame in flight
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) { for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
// Fence used to ensure that command buffer has completed exection before using it again
VkFenceCreateInfo fenceCI{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
// Create the fences in signaled state (so we don't wait on first render of each command buffer)
fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT;
VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
}
// Semaphores are per swapchain image
presentCompleteSemaphores.resize(swapChain.images.size());
renderCompleteSemaphores.resize(swapChain.images.size());
for (size_t i = 0; i < swapChain.images.size(); i++) {
// Semaphores are used for correct command ordering within a queue // Semaphores are used for correct command ordering within a queue
VkSemaphoreCreateInfo semaphoreCI{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; VkSemaphoreCreateInfo semaphoreCI{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
// Semaphore used to ensure that image presentation is complete before starting to submit again // Semaphore used to ensure that image presentation is complete before starting to submit again
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &presentCompleteSemaphores[i])); VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &presentCompleteSemaphores[i]));
// Semaphore used to ensure that all commands submitted have been finished before submitting the image to the queue // Semaphore used to ensure that all commands submitted have been finished before submitting the image to the queue
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &renderCompleteSemaphores[i])); VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &renderCompleteSemaphores[i]));
// Fence used to ensure that command buffer has completed exection before using it again
VkFenceCreateInfo fenceCI{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
// Create the fences in signaled state (so we don't wait on first render of each command buffer)
fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT;
VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
} }
} }
@ -689,8 +700,8 @@ public:
// Get the next swap chain image from the implementation // Get the next swap chain image from the implementation
// Note that the implementation is free to return the images in any order, so we must use the acquire function and can't just cycle through the images/imageIndex on our own // Note that the implementation is free to return the images in any order, so we must use the acquire function and can't just cycle through the images/imageIndex on our own
uint32_t imageIndex; uint32_t imageIndex{ 0 };
VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentSemaphore], VK_NULL_HANDLE, &imageIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR) { if (result == VK_ERROR_OUT_OF_DATE_KHR) {
windowResize(); windowResize();
return; return;
@ -777,10 +788,10 @@ public:
submitInfo.commandBufferCount = 1; // We submit a single command buffer submitInfo.commandBufferCount = 1; // We submit a single command buffer
// Semaphore to wait upon before the submitted command buffer starts executing // Semaphore to wait upon before the submitted command buffer starts executing
submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentFrame]; submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentSemaphore];
submitInfo.waitSemaphoreCount = 1; submitInfo.waitSemaphoreCount = 1;
// Semaphore to be signaled when command buffers have completed // Semaphore to be signaled when command buffers have completed
submitInfo.pSignalSemaphores = &renderCompleteSemaphores[currentFrame]; submitInfo.pSignalSemaphores = &renderCompleteSemaphores[currentSemaphore];
submitInfo.signalSemaphoreCount = 1; submitInfo.signalSemaphoreCount = 1;
// Submit to the graphics queue passing a wait fence // Submit to the graphics queue passing a wait fence
@ -791,7 +802,7 @@ public:
// This ensures that the image is not presented to the windowing system until all commands have been submitted // This ensures that the image is not presented to the windowing system until all commands have been submitted
VkPresentInfoKHR presentInfo{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; VkPresentInfoKHR presentInfo{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR };
presentInfo.waitSemaphoreCount = 1; presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = &renderCompleteSemaphores[currentFrame]; presentInfo.pWaitSemaphores = &renderCompleteSemaphores[currentSemaphore];
presentInfo.swapchainCount = 1; presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapChain.swapChain; presentInfo.pSwapchains = &swapChain.swapChain;
presentInfo.pImageIndices = &imageIndex; presentInfo.pImageIndices = &imageIndex;
@ -804,6 +815,8 @@ public:
// Select the next frame to render to, based on the max. no. of concurrent frames // Select the next frame to render to, based on the max. no. of concurrent frames
currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES; currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
// Similar for the semaphores, which need to be unique to the swapchain images
currentSemaphore = (currentSemaphore + 1) % swapChain.imageCount;
} }
// Override these as otherwise the base class would generate frame buffers and render passes // Override these as otherwise the base class would generate frame buffers and render passes