Skip to content

Commit 64ba099

Browse files
committed
Make semaphores unique per swap chain image
Fixes SaschaWillems#1210
1 parent 422d54e commit 64ba099

File tree

4 files changed

+77
-59
lines changed

4 files changed

+77
-59
lines changed

base/VulkanSwapChain.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* A swap chain is a collection of framebuffers used for rendering and presentation to the windowing system
55
*
6-
* Copyright (C) 2016-2024 by Sascha Willems - www.saschawillems.de
6+
* Copyright (C) 2016-2025 by Sascha Willems - www.saschawillems.de
77
*
88
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
99
*/
@@ -341,7 +341,6 @@ void VulkanSwapChain::create(uint32_t& width, uint32_t& height, bool vsync, bool
341341
}
342342
vkDestroySwapchainKHR(device, oldSwapchain, nullptr);
343343
}
344-
uint32_t imageCount{ 0 };
345344
VK_CHECK_RESULT(vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr));
346345

347346
// Get the swap chain images

base/VulkanSwapChain.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* A swap chain is a collection of framebuffers used for rendering and presentation to the windowing system
55
*
6-
* Copyright (C) 2016-2024 by Sascha Willems - www.saschawillems.de
6+
* Copyright (C) 2016-2025 by Sascha Willems - www.saschawillems.de
77
*
88
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
99
*/
@@ -41,6 +41,7 @@ class VulkanSwapChain
4141
std::vector<VkImage> images{};
4242
std::vector<VkImageView> imageViews{};
4343
uint32_t queueNodeIndex{ UINT32_MAX };
44+
uint32_t imageCount{ 0 };
4445

4546
#if defined(VK_USE_PLATFORM_WIN32_KHR)
4647
void initSurface(void* platformHandle, void* platformWindow);

examples/triangle/triangle.cpp

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,16 @@ class VulkanExample : public VulkanExampleBase
103103
// Synchronization is an important concept of Vulkan that OpenGL mostly hid away. Getting this right is crucial to using Vulkan.
104104

105105
// Semaphores are used to coordinate operations within the graphics queue and ensure correct command ordering
106-
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> presentCompleteSemaphores{};
107-
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> renderCompleteSemaphores{};
106+
std::vector<VkSemaphore> presentCompleteSemaphores{};
107+
std::vector<VkSemaphore> renderCompleteSemaphores{};
108108

109109
VkCommandPool commandPool{ VK_NULL_HANDLE };
110110
std::array<VkCommandBuffer, MAX_CONCURRENT_FRAMES> commandBuffers{};
111111
std::array<VkFence, MAX_CONCURRENT_FRAMES> waitFences{};
112112

113-
// To select the correct sync objects, we need to keep track of the current frame
113+
// To select the correct sync and command objects, we need to keep track of the current frame and (swapchain) image index
114114
uint32_t currentFrame{ 0 };
115+
uint32_t currentSemaphore{ 0 };
115116

116117
VulkanExample() : VulkanExampleBase()
117118
{
@@ -130,25 +131,26 @@ class VulkanExample : public VulkanExampleBase
130131
{
131132
// Clean up used Vulkan resources
132133
// Note: Inherited destructor cleans up resources stored in base class
133-
vkDestroyPipeline(device, pipeline, nullptr);
134-
135-
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
136-
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
137-
138-
vkDestroyBuffer(device, vertices.buffer, nullptr);
139-
vkFreeMemory(device, vertices.memory, nullptr);
140-
141-
vkDestroyBuffer(device, indices.buffer, nullptr);
142-
vkFreeMemory(device, indices.memory, nullptr);
143-
144-
vkDestroyCommandPool(device, commandPool, nullptr);
145-
146-
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
147-
vkDestroyFence(device, waitFences[i], nullptr);
148-
vkDestroySemaphore(device, presentCompleteSemaphores[i], nullptr);
149-
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
150-
vkDestroyBuffer(device, uniformBuffers[i].buffer, nullptr);
151-
vkFreeMemory(device, uniformBuffers[i].memory, nullptr);
134+
if (device) {
135+
vkDestroyPipeline(device, pipeline, nullptr);
136+
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
137+
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
138+
vkDestroyBuffer(device, vertices.buffer, nullptr);
139+
vkFreeMemory(device, vertices.memory, nullptr);
140+
vkDestroyBuffer(device, indices.buffer, nullptr);
141+
vkFreeMemory(device, indices.memory, nullptr);
142+
vkDestroyCommandPool(device, commandPool, nullptr);
143+
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
144+
vkDestroySemaphore(device, presentCompleteSemaphores[i], nullptr);
145+
}
146+
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
147+
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
148+
}
149+
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
150+
vkDestroyFence(device, waitFences[i], nullptr);
151+
vkDestroyBuffer(device, uniformBuffers[i].buffer, nullptr);
152+
vkFreeMemory(device, uniformBuffers[i].memory, nullptr);
153+
}
152154
}
153155
}
154156

@@ -178,24 +180,25 @@ class VulkanExample : public VulkanExampleBase
178180
// Create the per-frame (in flight) Vulkan synchronization primitives used in this example
179181
void createSynchronizationPrimitives()
180182
{
181-
// Semaphores are used for correct command ordering within a queue
182-
VkSemaphoreCreateInfo semaphoreCI{};
183-
semaphoreCI.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
184-
185183
// Fences are used to check draw command buffer completion on the host
186-
VkFenceCreateInfo fenceCI{};
187-
fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
188-
// Create the fences in signaled state (so we don't wait on first render of each command buffer)
189-
fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT;
190-
191184
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
185+
VkFenceCreateInfo fenceCI{};
186+
fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
187+
// Create the fences in signaled state (so we don't wait on first render of each command buffer)
188+
fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT;
189+
// Fence used to ensure that command buffer has completed exection before using it again
190+
VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
191+
}
192+
// Semaphores are per swapchain image
193+
presentCompleteSemaphores.resize(swapChain.images.size());
194+
renderCompleteSemaphores.resize(swapChain.images.size());
195+
for (size_t i = 0; i < swapChain.images.size(); i++) {
196+
// Semaphores are used for correct command ordering within a queue
197+
VkSemaphoreCreateInfo semaphoreCI{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
192198
// Semaphore used to ensure that image presentation is complete before starting to submit again
193199
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &presentCompleteSemaphores[i]));
194200
// Semaphore used to ensure that all commands submitted have been finished before submitting the image to the queue
195201
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &renderCompleteSemaphores[i]));
196-
197-
// Fence used to ensure that command buffer has completed exection before using it again
198-
VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
199202
}
200203
}
201204

@@ -912,7 +915,7 @@ class VulkanExample : public VulkanExampleBase
912915
// Get the next swap chain image from the implementation
913916
// 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
914917
uint32_t imageIndex;
915-
VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
918+
VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentSemaphore], VK_NULL_HANDLE, &imageIndex);
916919
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
917920
windowResize();
918921
return;
@@ -1008,10 +1011,10 @@ class VulkanExample : public VulkanExampleBase
10081011
submitInfo.commandBufferCount = 1; // We submit a single command buffer
10091012

10101013
// Semaphore to wait upon before the submitted command buffer starts executing
1011-
submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentFrame];
1014+
submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentSemaphore];
10121015
submitInfo.waitSemaphoreCount = 1;
10131016
// Semaphore to be signaled when command buffers have completed
1014-
submitInfo.pSignalSemaphores = &renderCompleteSemaphores[currentFrame];
1017+
submitInfo.pSignalSemaphores = &renderCompleteSemaphores[currentSemaphore];
10151018
submitInfo.signalSemaphoreCount = 1;
10161019

10171020
// Submit to the graphics queue passing a wait fence
@@ -1024,7 +1027,7 @@ class VulkanExample : public VulkanExampleBase
10241027
VkPresentInfoKHR presentInfo{};
10251028
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
10261029
presentInfo.waitSemaphoreCount = 1;
1027-
presentInfo.pWaitSemaphores = &renderCompleteSemaphores[currentFrame];
1030+
presentInfo.pWaitSemaphores = &renderCompleteSemaphores[currentSemaphore];
10281031
presentInfo.swapchainCount = 1;
10291032
presentInfo.pSwapchains = &swapChain.swapChain;
10301033
presentInfo.pImageIndices = &imageIndex;
@@ -1039,6 +1042,8 @@ class VulkanExample : public VulkanExampleBase
10391042

10401043
// Select the next frame to render to, based on the max. no. of concurrent frames
10411044
currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
1045+
// Similar for the semaphores, which need to be unique to the swapchain images
1046+
currentSemaphore = (currentSemaphore + 1) % swapChain.imageCount;
10421047
}
10431048
};
10441049

examples/trianglevulkan13/trianglevulkan13.cpp

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* This is a variation of the the triangle sample that makes use of Vulkan 1.3 features
66
* This simplifies the api a bit, esp. with dynamic rendering replacing render passes (and with that framebuffers)
77
*
8-
* Copyright (C) 2024 by Sascha Willems - www.saschawillems.de
8+
* Copyright (C) 2024-2025 by Sascha Willems - www.saschawillems.de
99
*
1010
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
1111
*/
@@ -87,16 +87,17 @@ class VulkanExample : public VulkanExampleBase
8787
// Synchronization primitives
8888
// Synchronization is an important concept of Vulkan that OpenGL mostly hid away. Getting this right is crucial to using Vulkan.
8989
// Semaphores are used to coordinate operations within the graphics queue and ensure correct command ordering
90-
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> presentCompleteSemaphores{};
91-
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> renderCompleteSemaphores{};
90+
std::vector<VkSemaphore> presentCompleteSemaphores{};
91+
std::vector<VkSemaphore> renderCompleteSemaphores{};
9292
// Fences are used to make sure command buffers aren't rerecorded until they've finished executing
9393
std::array<VkFence, MAX_CONCURRENT_FRAMES> waitFences{};
9494

9595
VkCommandPool commandPool{ VK_NULL_HANDLE };
9696
std::array<VkCommandBuffer, MAX_CONCURRENT_FRAMES> commandBuffers{};
9797

98-
// To select the correct sync and command objects, we need to keep track of the current frame
98+
// To select the correct sync and command objects, we need to keep track of the current frame and (swapchain) image index
9999
uint32_t currentFrame{ 0 };
100+
uint32_t currentSemaphore{ 0 };
100101

101102
VkPhysicalDeviceVulkan13Features enabledFeatures{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES };
102103

@@ -130,10 +131,14 @@ class VulkanExample : public VulkanExampleBase
130131
vkDestroyBuffer(device, indexBuffer.handle, nullptr);
131132
vkFreeMemory(device, indexBuffer.memory, nullptr);
132133
vkDestroyCommandPool(device, commandPool, nullptr);
133-
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
134-
vkDestroyFence(device, waitFences[i], nullptr);
134+
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
135135
vkDestroySemaphore(device, presentCompleteSemaphores[i], nullptr);
136+
}
137+
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
136138
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
139+
}
140+
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
141+
vkDestroyFence(device, waitFences[i], nullptr);
137142
vkDestroyBuffer(device, uniformBuffers[i].handle, nullptr);
138143
vkFreeMemory(device, uniformBuffers[i].memory, nullptr);
139144
}
@@ -166,22 +171,28 @@ class VulkanExample : public VulkanExampleBase
166171
throw "Could not find a suitable memory type!";
167172
}
168173

169-
// Create the per-frame (in flight) Vulkan synchronization primitives used in this example
174+
// Create the per-frame (in flight) and per (swapchain image) Vulkan synchronization primitives used in this example
170175
void createSynchronizationPrimitives()
171176
{
177+
// Fences are per frame in flight
172178
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
173-
// Semaphores are used for correct command ordering within a queue
174-
VkSemaphoreCreateInfo semaphoreCI{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
175-
// Semaphore used to ensure that image presentation is complete before starting to submit again
176-
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &presentCompleteSemaphores[i]));
177-
// Semaphore used to ensure that all commands submitted have been finished before submitting the image to the queue
178-
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &renderCompleteSemaphores[i]));
179179
// Fence used to ensure that command buffer has completed exection before using it again
180180
VkFenceCreateInfo fenceCI{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
181181
// Create the fences in signaled state (so we don't wait on first render of each command buffer)
182182
fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT;
183183
VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
184184
}
185+
// Semaphores are per swapchain image
186+
presentCompleteSemaphores.resize(swapChain.images.size());
187+
renderCompleteSemaphores.resize(swapChain.images.size());
188+
for (size_t i = 0; i < swapChain.images.size(); i++) {
189+
// Semaphores are used for correct command ordering within a queue
190+
VkSemaphoreCreateInfo semaphoreCI{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
191+
// Semaphore used to ensure that image presentation is complete before starting to submit again
192+
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &presentCompleteSemaphores[i]));
193+
// Semaphore used to ensure that all commands submitted have been finished before submitting the image to the queue
194+
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &renderCompleteSemaphores[i]));
195+
}
185196
}
186197

187198
// Command buffers are used to record commands to and are submitted to a queue for execution ("rendering")
@@ -689,8 +700,8 @@ class VulkanExample : public VulkanExampleBase
689700

690701
// Get the next swap chain image from the implementation
691702
// 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
692-
uint32_t imageIndex;
693-
VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
703+
uint32_t imageIndex{ 0 };
704+
VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentSemaphore], VK_NULL_HANDLE, &imageIndex);
694705
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
695706
windowResize();
696707
return;
@@ -777,10 +788,10 @@ class VulkanExample : public VulkanExampleBase
777788
submitInfo.commandBufferCount = 1; // We submit a single command buffer
778789

779790
// Semaphore to wait upon before the submitted command buffer starts executing
780-
submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentFrame];
791+
submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentSemaphore];
781792
submitInfo.waitSemaphoreCount = 1;
782793
// Semaphore to be signaled when command buffers have completed
783-
submitInfo.pSignalSemaphores = &renderCompleteSemaphores[currentFrame];
794+
submitInfo.pSignalSemaphores = &renderCompleteSemaphores[currentSemaphore];
784795
submitInfo.signalSemaphoreCount = 1;
785796

786797
// Submit to the graphics queue passing a wait fence
@@ -791,7 +802,7 @@ class VulkanExample : public VulkanExampleBase
791802
// This ensures that the image is not presented to the windowing system until all commands have been submitted
792803
VkPresentInfoKHR presentInfo{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR };
793804
presentInfo.waitSemaphoreCount = 1;
794-
presentInfo.pWaitSemaphores = &renderCompleteSemaphores[currentFrame];
805+
presentInfo.pWaitSemaphores = &renderCompleteSemaphores[currentSemaphore];
795806
presentInfo.swapchainCount = 1;
796807
presentInfo.pSwapchains = &swapChain.swapChain;
797808
presentInfo.pImageIndices = &imageIndex;
@@ -804,6 +815,8 @@ class VulkanExample : public VulkanExampleBase
804815

805816
// Select the next frame to render to, based on the max. no. of concurrent frames
806817
currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
818+
// Similar for the semaphores, which need to be unique to the swapchain images
819+
currentSemaphore = (currentSemaphore + 1) % swapChain.imageCount;
807820
}
808821

809822
// Override these as otherwise the base class would generate frame buffers and render passes

0 commit comments

Comments
 (0)