Mutiple frames in fight

Fixes #1093
This commit is contained in:
Sascha Willems 2023-12-26 18:49:39 +01:00
parent 99d6cce6b5
commit f6e77be11f

View file

@ -31,6 +31,7 @@
#define ENABLE_VALIDATION false #define ENABLE_VALIDATION false
// We want to keep GPU and CPU busy. To do that we may start building a new command buffer while the previous one is still being executed // We want to keep GPU and CPU busy. To do that we may start building a new command buffer while the previous one is still being executed
// This number defines how many frames may be worked on simultaneously at once // This number defines how many frames may be worked on simultaneously at once
// Increasing this number may improve performance but will also introduce additional latency
#define MAX_CONCURRENT_FRAMES 2 #define MAX_CONCURRENT_FRAMES 2
class VulkanExample : public VulkanExampleBase class VulkanExample : public VulkanExampleBase
@ -44,15 +45,15 @@ public:
// Vertex buffer and attributes // Vertex buffer and attributes
struct { struct {
VkDeviceMemory memory; // Handle to the device memory for this buffer VkDeviceMemory memory{ VK_NULL_HANDLE }; // Handle to the device memory for this buffer
VkBuffer buffer; // Handle to the Vulkan buffer object that the memory is bound to VkBuffer buffer; // Handle to the Vulkan buffer object that the memory is bound to
} vertices; } vertices;
// Index buffer // Index buffer
struct { struct {
VkDeviceMemory memory; VkDeviceMemory memory{ VK_NULL_HANDLE };
VkBuffer buffer; VkBuffer buffer;
uint32_t count; uint32_t count{ 0 };
} indices; } indices;
// Uniform buffer block object // Uniform buffer block object
@ -88,30 +89,31 @@ public:
// The pipeline layout is used by a pipeline to access the descriptor sets // The pipeline layout is used by a pipeline to access the descriptor sets
// It defines interface (without binding any actual data) between the shader stages used by the pipeline and the shader resources // It defines interface (without binding any actual data) between the shader stages used by the pipeline and the shader resources
// A pipeline layout can be shared among multiple pipelines as long as their interfaces match // A pipeline layout can be shared among multiple pipelines as long as their interfaces match
VkPipelineLayout pipelineLayout; VkPipelineLayout pipelineLayout{ VK_NULL_HANDLE };
// Pipelines (often called "pipeline state objects") are used to bake all states that affect a pipeline // Pipelines (often called "pipeline state objects") are used to bake all states that affect a pipeline
// While in OpenGL every state can be changed at (almost) any time, Vulkan requires to layout the graphics (and compute) pipeline states upfront // While in OpenGL every state can be changed at (almost) any time, Vulkan requires to layout the graphics (and compute) pipeline states upfront
// So for each combination of non-dynamic pipeline states you need a new pipeline (there are a few exceptions to this not discussed here) // So for each combination of non-dynamic pipeline states you need a new pipeline (there are a few exceptions to this not discussed here)
// Even though this adds a new dimension of planning ahead, it's a great opportunity for performance optimizations by the driver // Even though this adds a new dimension of planning ahead, it's a great opportunity for performance optimizations by the driver
VkPipeline pipeline; VkPipeline pipeline{ VK_NULL_HANDLE };
// The descriptor set layout describes the shader binding layout (without actually referencing descriptor) // The descriptor set layout describes the shader binding layout (without actually referencing descriptor)
// Like the pipeline layout it's pretty much a blueprint and can be used with different descriptor sets as long as their layout matches // Like the pipeline layout it's pretty much a blueprint and can be used with different descriptor sets as long as their layout matches
VkDescriptorSetLayout descriptorSetLayout; VkDescriptorSetLayout descriptorSetLayout{ VK_NULL_HANDLE };
// 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::array<VkSemaphore, MAX_CONCURRENT_FRAMES> presentCompleteSemaphores{};
std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> renderCompleteSemaphores; std::array<VkSemaphore, MAX_CONCURRENT_FRAMES> renderCompleteSemaphores{};
VkCommandPool commandPool; 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{};
uint32_t currentFrame = 0; // To select the correct sync objects, we need to keep track of the current frame
uint32_t currentFrame{ 0 };
VulkanExample() : VulkanExampleBase(ENABLE_VALIDATION) VulkanExample() : VulkanExampleBase(ENABLE_VALIDATION)
{ {
@ -143,8 +145,7 @@ public:
vkDestroyCommandPool(device, commandPool, nullptr); vkDestroyCommandPool(device, commandPool, 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, presentCompleteSemaphores[i], nullptr);
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr); vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
@ -176,26 +177,27 @@ public:
throw "Could not find a suitable memory type!"; throw "Could not find a suitable memory type!";
} }
// Create the Vulkan synchronization primitives used in this example // Create the per-frame (in flight) sVulkan synchronization primitives used in this example
void createSynchronizationPrimitives() void createSynchronizationPrimitives()
{ {
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) { // Semaphores are used for correct command ordering within a queue
// Semaphores (Used for correct command ordering)
VkSemaphoreCreateInfo semaphoreCI{}; VkSemaphoreCreateInfo semaphoreCI{};
semaphoreCI.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; semaphoreCI.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
// Fences are used to check draw command buffer completion on the host
VkFenceCreateInfo fenceCI{};
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)
fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
// 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]));
// Fences (Used to check draw command buffer completion) // Fence used to ensure that command buffer has completed exection before using it again
VkFenceCreateInfo fenceCI{};
fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
// Create 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])); VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
} }
} }
@ -907,15 +909,17 @@ public:
// Use a fence to wait until the command buffer has finished execution before using it again // Use a fence to wait until the command buffer has finished execution before using it again
vkWaitForFences(device, 1, &waitFences[currentFrame], VK_TRUE, UINT64_MAX); vkWaitForFences(device, 1, &waitFences[currentFrame], VK_TRUE, UINT64_MAX);
VK_CHECK_RESULT(vkResetFences(device, 1, &waitFences[currentFrame]));
// 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 // 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[currentFrame], VK_NULL_HANDLE, &imageIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR) { if (result == VK_ERROR_OUT_OF_DATE_KHR) {
windowResize(); windowResize();
return; return;
} else if ((result != VK_SUCCESS) && (result != VK_SUBOPTIMAL_KHR)) { }
else if ((result != VK_SUCCESS) && (result != VK_SUBOPTIMAL_KHR)) {
throw "Could not acquire the next swap chain image!"; throw "Could not acquire the next swap chain image!";
} }
@ -927,16 +931,14 @@ public:
// Copy the current matrices to the current frame's uniform buffer // Copy the current matrices to the current frame's uniform buffer
// Note: Since we requested a host coherent memory type for the uniform buffer, the write is instantly visible to the GPU // Note: Since we requested a host coherent memory type for the uniform buffer, the write is instantly visible to the GPU
memcpy(uniformBuffers[currentBuffer].mapped, &shaderData, sizeof(ShaderData)); memcpy(uniformBuffers[currentFrame].mapped, &shaderData, sizeof(ShaderData));
VK_CHECK_RESULT(vkResetFences(device, 1, &waitFences[currentFrame]));
// Build the command buffer // Build the command buffer
// Unlike in OpenGL all rendering commands are recorded into command buffers that are then submitted to the queue // Unlike in OpenGL all rendering commands are recorded into command buffers that are then submitted to the queue
// This allows to generate work upfront in a separate thread // This allows to generate work upfront in a separate thread
// For basic command buffers (like in this sample), recording is so fast that there is no need to offload this // For basic command buffers (like in this sample), recording is so fast that there is no need to offload this
vkResetCommandBuffer(commandBuffers[currentBuffer], 0); vkResetCommandBuffer(commandBuffers[currentFrame], 0);
VkCommandBufferBeginInfo cmdBufInfo{}; VkCommandBufferBeginInfo cmdBufInfo{};
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
@ -958,41 +960,43 @@ public:
renderPassBeginInfo.clearValueCount = 2; renderPassBeginInfo.clearValueCount = 2;
renderPassBeginInfo.pClearValues = clearValues; renderPassBeginInfo.pClearValues = clearValues;
renderPassBeginInfo.framebuffer = frameBuffers[imageIndex]; renderPassBeginInfo.framebuffer = frameBuffers[imageIndex];
VK_CHECK_RESULT(vkBeginCommandBuffer(commandBuffers[currentBuffer], &cmdBufInfo));
const VkCommandBuffer commandBuffer = commandBuffers[currentFrame];
VK_CHECK_RESULT(vkBeginCommandBuffer(commandBuffer, &cmdBufInfo));
// Start the first sub pass specified in our default render pass setup by the base class // Start the first sub pass specified in our default render pass setup by the base class
// This will clear the color and depth attachment // This will clear the color and depth attachment
vkCmdBeginRenderPass(commandBuffers[currentBuffer], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// Update dynamic viewport state // Update dynamic viewport state
VkViewport viewport{}; VkViewport viewport{};
viewport.height = (float)height; viewport.height = (float)height;
viewport.width = (float)width; viewport.width = (float)width;
viewport.minDepth = (float)0.0f; viewport.minDepth = (float)0.0f;
viewport.maxDepth = (float)1.0f; viewport.maxDepth = (float)1.0f;
vkCmdSetViewport(commandBuffers[currentBuffer], 0, 1, &viewport); vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
// Update dynamic scissor state // Update dynamic scissor state
VkRect2D scissor{}; VkRect2D scissor{};
scissor.extent.width = width; scissor.extent.width = width;
scissor.extent.height = height; scissor.extent.height = height;
scissor.offset.x = 0; scissor.offset.x = 0;
scissor.offset.y = 0; scissor.offset.y = 0;
vkCmdSetScissor(commandBuffers[currentBuffer], 0, 1, &scissor); vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
// Bind descriptor set for the currrent frame's uniform buffer, so the shader uses the data from that buffer for this draw // Bind descriptor set for the currrent frame's uniform buffer, so the shader uses the data from that buffer for this draw
vkCmdBindDescriptorSets(commandBuffers[currentBuffer], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &uniformBuffers[currentBuffer].descriptorSet, 0, nullptr); vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &uniformBuffers[currentFrame].descriptorSet, 0, nullptr);
// Bind the rendering pipeline // Bind the rendering pipeline
// The pipeline (state object) contains all states of the rendering pipeline, binding it will set all the states specified at pipeline creation time // The pipeline (state object) contains all states of the rendering pipeline, binding it will set all the states specified at pipeline creation time
vkCmdBindPipeline(commandBuffers[currentBuffer], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
// Bind triangle vertex buffer (contains position and colors) // Bind triangle vertex buffer (contains position and colors)
VkDeviceSize offsets[1]{ 0 }; VkDeviceSize offsets[1]{ 0 };
vkCmdBindVertexBuffers(commandBuffers[currentBuffer], 0, 1, &vertices.buffer, offsets); vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertices.buffer, offsets);
// Bind triangle index buffer // Bind triangle index buffer
vkCmdBindIndexBuffer(commandBuffers[currentBuffer], indices.buffer, 0, VK_INDEX_TYPE_UINT32); vkCmdBindIndexBuffer(commandBuffer, indices.buffer, 0, VK_INDEX_TYPE_UINT32);
// Draw indexed triangle // Draw indexed triangle
vkCmdDrawIndexed(commandBuffers[currentBuffer], indices.count, 1, 0, 0, 1); vkCmdDrawIndexed(commandBuffer, indices.count, 1, 0, 0, 1);
vkCmdEndRenderPass(commandBuffers[currentBuffer]); vkCmdEndRenderPass(commandBuffer);
// Ending the render pass will add an implicit barrier transitioning the frame buffer color attachment to // Ending the render pass will add an implicit barrier transitioning the frame buffer color attachment to
// VK_IMAGE_LAYOUT_PRESENT_SRC_KHR for presenting it to the windowing system // VK_IMAGE_LAYOUT_PRESENT_SRC_KHR for presenting it to the windowing system
VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffers[currentBuffer])); VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer));
// Submit the command buffer to the graphics queue // Submit the command buffer to the graphics queue
@ -1002,15 +1006,15 @@ public:
VkSubmitInfo submitInfo{}; VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.pWaitDstStageMask = &waitStageMask; // Pointer to the list of pipeline stages that the semaphore waits will occur at submitInfo.pWaitDstStageMask = &waitStageMask; // Pointer to the list of pipeline stages that the semaphore waits will occur at
submitInfo.waitSemaphoreCount = 1; // One wait semaphore submitInfo.pCommandBuffers = &commandBuffer; // Command buffers(s) to execute in this batch (submission)
submitInfo.signalSemaphoreCount = 1; // One signal semaphore submitInfo.commandBufferCount = 1; // We submit a single command buffer
submitInfo.pCommandBuffers = &commandBuffers[currentBuffer]; // Command buffers(s) to execute in this batch (submission)
submitInfo.commandBufferCount = 1; // One 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[currentFrame];
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[currentFrame];
submitInfo.signalSemaphoreCount = 1;
// Submit to the graphics queue passing a wait fence // Submit to the graphics queue passing a wait fence
VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, waitFences[currentFrame])); VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, waitFences[currentFrame]));
@ -1035,6 +1039,8 @@ public:
throw "Could not present the image to the swap chain!"; throw "Could not present the image to the swap chain!";
} }
// Select the next frame to render to, based on the max. no. of concurrent frames
currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
} }
}; };