diff --git a/triangle/triangle.cpp b/triangle/triangle.cpp index b486e256..7d29e5d5 100644 --- a/triangle/triangle.cpp +++ b/triangle/triangle.cpp @@ -1,7 +1,7 @@ /* * Vulkan Example - Basic indexed triangle rendering * -* Note : +* Note: * This is a "pedal to the metal" example to show off how to get Vulkan up an displaying something * Contrary to the other examples, this one won't make use of helper functions or initializers * Except in a few cases (swap chain setup e.g.) @@ -27,35 +27,36 @@ #include "vulkanexamplebase.h" #define VERTEX_BUFFER_BIND_ID 0 -// Set to "true" to enable Vulkan's validation layers -// See vulkandebug.cpp for details +// Set to "true" to enable Vulkan's validation layers (see vulkandebug.cpp for details) #define ENABLE_VALIDATION false -// Set to "true" to use staging buffers for uploading -// vertex and index data to device local memory -// See "prepareVertices" for details on what's staging -// and on why to use it +// Set to "true" to use staging buffers for uploading vertex and index data to device local memory +// See "prepareVertices" for details on what's staging and on why to use it #define USE_STAGING true class VulkanExample : public VulkanExampleBase { public: + // Vertex buffer and attributes struct { - VkBuffer buf; - VkDeviceMemory mem; + VkDeviceMemory memory; // Handle to the device memory for this buffer + VkBuffer buffer; // Handle to the Vulkan buffer object that the memory is bound to VkPipelineVertexInputStateCreateInfo inputState; - std::vector bindingDescriptions; - std::vector attributeDescriptions; + VkVertexInputBindingDescription inputBinding; + std::vector inputAttributes; } vertices; - struct { - int count; - VkBuffer buf; - VkDeviceMemory mem; + // Index buffer + struct + { + VkDeviceMemory memory; + VkBuffer buffer; + uint32_t count; } indices; + // Uniform block object struct { - VkBuffer buffer; - VkDeviceMemory memory; + VkDeviceMemory memory; + VkBuffer buffer; VkDescriptorBufferInfo descriptor; } uniformDataVS; @@ -69,48 +70,44 @@ public: // } ubo; // // This way we can just memcopy the ubo data to the ubo - // Note that you should be using data types that align with the GPU - // in order to avoid manual padding + // Note: You should use data types that align with the GPU in order to avoid manual padding (vec4, mat4) struct { glm::mat4 projectionMatrix; glm::mat4 modelMatrix; glm::mat4 viewMatrix; } uboVS; - // The pipeline (state objects) is a static store for the 3D pipeline states (including shaders) - // Other than OpenGL this makes you setup the render states up-front - // If different render states are required you need to setup multiple pipelines - // and switch between them - // Note that there are a few dynamic states (scissor, viewport, line width) that - // can be set from a command buffer and does not have to be part of the pipeline - // This basic example only uses one pipeline + // The pipeline layout is used by a pipline 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 + // A pipeline layout can be shared among multiple pipelines as long as their interfaces match + VkPipelineLayout pipelineLayout; + + // 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 + // 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 planing ahead, it's a great opportunity for performance optimizations by the driver VkPipeline pipeline; - // The pipeline layout defines the resource binding slots to be used with a pipeline - // This includes bindings for buffes (ubos, ssbos), images and sampler - // A pipeline layout can be used for multiple pipeline (state objects) as long as - // their shaders use the same binding layout - VkPipelineLayout pipelineLayout; - - // The descriptor set stores the resources bound to the binding points in a shader - // It connects the binding points of the different shaders with the buffers and images - // used for those bindings - VkDescriptorSet descriptorSet; - - // The descriptor set layout describes the shader binding points without referencing - // the actual buffers. - // Like the pipeline layout it's pretty much a blueprint and can be used with - // different descriptor sets as long as the binding points (and shaders) match + // 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 VkDescriptorSetLayout descriptorSetLayout; - // Synchronization semaphores - // Semaphores are used to synchronize dependencies between command buffers - // We use them to ensure that we e.g. don't present to the swap chain - // until all rendering has completed - struct { - VkSemaphore presentComplete; - VkSemaphore renderComplete; - } semaphores; + // The descriptor set stores the resources bound to the binding points in a shader + // It connects the binding points of the different shaders with the buffers and images used for those bindings + VkDescriptorSet descriptorSet; + + + // Synchronization primitives + // Synchronization is an important concept of Vulkan that OpenGL mostly hid away. Getting this right is crucial to using Vulkan. + + // Semaphores + // Used to coordinate operations within the graphics queue and ensure correct command ordering + VkSemaphore presentCompleteSemaphore; + VkSemaphore renderCompleteSemaphore; + + // Fences + // Used to check the completion of queue operations (e.g. command buffer execution) + std::vector waitFences; VulkanExample() : VulkanExampleBase(ENABLE_VALIDATION) { @@ -124,23 +121,28 @@ public: ~VulkanExample() { // 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 vkDestroyPipeline(device, pipeline, nullptr); vkDestroyPipelineLayout(device, pipelineLayout, nullptr); vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); - vkDestroyBuffer(device, vertices.buf, nullptr); - vkFreeMemory(device, vertices.mem, nullptr); + vkDestroyBuffer(device, vertices.buffer, nullptr); + vkFreeMemory(device, vertices.memory, nullptr); - vkDestroyBuffer(device, indices.buf, nullptr); - vkFreeMemory(device, indices.mem, nullptr); - - vkDestroySemaphore(device, semaphores.presentComplete, nullptr); - vkDestroySemaphore(device, semaphores.renderComplete, nullptr); + vkDestroyBuffer(device, indices.buffer, nullptr); + vkFreeMemory(device, indices.memory, nullptr); vkDestroyBuffer(device, uniformDataVS.buffer, nullptr); vkFreeMemory(device, uniformDataVS.memory, nullptr); + + vkDestroySemaphore(device, presentCompleteSemaphore, nullptr); + vkDestroySemaphore(device, renderCompleteSemaphore, nullptr); + + for (auto& fence : waitFences) + { + vkDestroyFence(device, fence, nullptr); + } } // This function is used to request a device memory type that supports all the property flags we request (e.g. device local, host visibile) @@ -166,11 +168,33 @@ public: throw "Could not find a suitable memory type!"; } - // We are going to use several temporary command buffers for setup stuff - // like submitting barriers for layout translations of buffer copies - // This function will allocate a single (primary) command buffer from the - // examples' command buffer pool and if begin is set to true it will - // also start the buffer so we can directly put commands into it + // Create the Vulkan synchronization primitives used in this example + void prepareSynchronizationPrimitives() + { + // Semaphores (Used for correct command ordering) + VkSemaphoreCreateInfo semaphoreCreateInfo = {}; + semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + semaphoreCreateInfo.pNext = nullptr; + + // Semaphore used to ensures that image presentation is complete before starting to submit again + VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &presentCompleteSemaphore)); + + // Semaphore used to ensures that all commands submitted have been finished before submitting the image to the queue + VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &renderCompleteSemaphore)); + + // Fences (Used to check draw command buffer completion) + VkFenceCreateInfo fenceCreateInfo = {}; + fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceCreateInfo.flags = 0; + waitFences.resize(drawCmdBuffers.size()); + for (auto& fence : waitFences) + { + VK_CHECK_RESULT(vkCreateFence(device, &fenceCreateInfo, nullptr, &fence)); + } + } + + // Get a new command buffer from the command pool + // If begin is true, the command buffer is also started so we can start adding commands VkCommandBuffer getCommandBuffer(bool begin) { VkCommandBuffer cmdBuffer; @@ -193,10 +217,8 @@ public: return cmdBuffer; } - // This will end the command buffer and submit it to the examples' queue - // Then waits for the queue to become idle so our submission is finished - // and then deletes the command buffer - // For use with setup command buffers created by getCommandBuffer + // End the command buffer and submit it to the queue + // Uses a fence to ensure command buffer has finished executing before deleting it void flushCommandBuffer(VkCommandBuffer commandBuffer) { assert(commandBuffer != VK_NULL_HANDLE); @@ -211,28 +233,40 @@ public: VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE)); VK_CHECK_RESULT(vkQueueWaitIdle(queue)); + // Create fence to ensure that the command buffer has finished executing + VkFenceCreateInfo fenceCreateInfo = {}; + fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceCreateInfo.flags = 0; + VkFence fence; + VK_CHECK_RESULT(vkCreateFence(device, &fenceCreateInfo, nullptr, &fence)); + + // Submit to the queue + VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, fence)); + // Wait for the fence to signal that command buffer has finished executing + VK_CHECK_RESULT(vkWaitForFences(device, 1, &fence, VK_TRUE, DEFAULT_FENCE_TIMEOUT)); + + vkDestroyFence(device, fence, nullptr); vkFreeCommandBuffers(device, cmdPool, 1, &commandBuffer); } // Build separate command buffers for every framebuffer image - // Unlike in OpenGL all rendering commands are recorded once - // into command buffers that are then resubmitted to the queue + // Unlike in OpenGL all rendering commands are recorded once into command buffers that are then resubmitted to the queue + // This allows to generate work upfront and from multiple threads, one of the biggest advantages of Vulkan void buildCommandBuffers() { VkCommandBufferBeginInfo cmdBufInfo = {}; cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - cmdBufInfo.pNext = NULL; + cmdBufInfo.pNext = nullptr; // Set clear values for all framebuffer attachments with loadOp set to clear - // We use two attachments (color and depth) that are cleared at the - // start of the subpass and as such we need to set clear values for both + // We use two attachments (color and depth) that are cleared at the start of the subpass and as such we need to set clear values for both VkClearValue clearValues[2]; - clearValues[0].color = defaultClearColor; + clearValues[0].color = { { 0.0f, 0.0f, 0.2f, 1.0f } }; clearValues[1].depthStencil = { 1.0f, 0 }; VkRenderPassBeginInfo renderPassBeginInfo = {}; renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - renderPassBeginInfo.pNext = NULL; + renderPassBeginInfo.pNext = nullptr; renderPassBeginInfo.renderPass = renderPass; renderPassBeginInfo.renderArea.offset.x = 0; renderPassBeginInfo.renderArea.offset.y = 0; @@ -269,168 +303,78 @@ public: vkCmdSetScissor(drawCmdBuffers[i], 0, 1, &scissor); // Bind descriptor sets describing shader binding points - vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, NULL); + vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); // Bind the rendering pipeline - // The pipeline (state object) contains all states of the rendering pipeline - // So once we bind a pipeline all states that were set upon creation of that - // pipeline will be set + // The pipeline (state object) contains all states of the rendering pipeline, binding it will set all the states specified at pipeline creation time vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); // Bind triangle vertex buffer (contains position and colors) VkDeviceSize offsets[1] = { 0 }; - vkCmdBindVertexBuffers(drawCmdBuffers[i], VERTEX_BUFFER_BIND_ID, 1, &vertices.buf, offsets); + vkCmdBindVertexBuffers(drawCmdBuffers[i], VERTEX_BUFFER_BIND_ID, 1, &vertices.buffer, offsets); // Bind triangle index buffer - vkCmdBindIndexBuffer(drawCmdBuffers[i], indices.buf, 0, VK_INDEX_TYPE_UINT32); + vkCmdBindIndexBuffer(drawCmdBuffers[i], indices.buffer, 0, VK_INDEX_TYPE_UINT32); // Draw indexed triangle vkCmdDrawIndexed(drawCmdBuffers[i], indices.count, 1, 0, 0, 1); vkCmdEndRenderPass(drawCmdBuffers[i]); - // Add a present memory barrier to the end of the command buffer - // This will transform the frame buffer color attachment to a - // new layout for presenting it to the windowing system integration - VkImageMemoryBarrier prePresentBarrier = {}; - prePresentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - prePresentBarrier.pNext = NULL; - prePresentBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - prePresentBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; - prePresentBarrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - prePresentBarrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - prePresentBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - prePresentBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - prePresentBarrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - prePresentBarrier.image = swapChain.buffers[i].image; - - VkImageMemoryBarrier *pMemoryBarrier = &prePresentBarrier; - vkCmdPipelineBarrier( - drawCmdBuffers[i], - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, - VK_FLAGS_NONE, - 0, nullptr, - 0, nullptr, - 1, &prePresentBarrier); + // 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_CHECK_RESULT(vkEndCommandBuffer(drawCmdBuffers[i])); } - - // Build command buffers for the post present image barrier for each swap chain image - // Note: The command Buffers are allocated in the base class - - for (uint32_t i = 0; i < swapChain.imageCount; i++) - { - // Insert a post present image barrier to transform the image back to a - // color attachment that our render pass can write to - // We always use undefined image layout as the source as it doesn't actually matter - // what is done with the previous image contents - VkImageMemoryBarrier postPresentBarrier = vkTools::initializers::imageMemoryBarrier(); - postPresentBarrier.srcAccessMask = 0; - postPresentBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - postPresentBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - postPresentBarrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - postPresentBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - postPresentBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - postPresentBarrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - postPresentBarrier.image = swapChain.buffers[i].image; - - // Use dedicated command buffer from example base class for submitting the post present barrier - VkCommandBufferBeginInfo cmdBufInfo = {}; - cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - - VK_CHECK_RESULT(vkBeginCommandBuffer(postPresentCmdBuffers[i], &cmdBufInfo)); - - // Put post present barrier into command buffer - vkCmdPipelineBarrier( - postPresentCmdBuffers[i], - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, - VK_FLAGS_NONE, - 0, nullptr, - 0, nullptr, - 1, &postPresentBarrier); - - VK_CHECK_RESULT(vkEndCommandBuffer(postPresentCmdBuffers[i])); - } } void draw() { // Get next image in the swap chain (back/front buffer) - VK_CHECK_RESULT(swapChain.acquireNextImage(semaphores.presentComplete, ¤tBuffer)); + VK_CHECK_RESULT(swapChain.acquireNextImage(presentCompleteSemaphore, ¤tBuffer)); - // Submit the post present image barrier to transform the image back to a color attachment - // that can be used to write to by our render pass + // The submit info structure specifices a command buffer queue submission batch + VkPipelineStageFlags pipelineStages = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &postPresentCmdBuffers[currentBuffer]; - - VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE)); - - // Make sure that the image barrier command submitted to the queue - // has finished executing - VK_CHECK_RESULT(vkQueueWaitIdle(queue)); - - // The submit infor strcuture contains a list of - // command buffers and semaphores to be submitted to a queue - // If you want to submit multiple command buffers, pass an array - VkPipelineStageFlags pipelineStages = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pWaitDstStageMask = &pipelineStages; - // The wait semaphore ensures that the image is presented - // before we start submitting command buffers agein - submitInfo.waitSemaphoreCount = 1; - submitInfo.pWaitSemaphores = &semaphores.presentComplete; - // Submit the currently active command buffer - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer]; - // The signal semaphore is used during queue presentation - // to ensure that the image is not rendered before all - // commands have been submitted - submitInfo.signalSemaphoreCount = 1; - submitInfo.pSignalSemaphores = &semaphores.renderComplete; + submitInfo.pWaitSemaphores = &presentCompleteSemaphore; // Semaphore(s) to wait upon before the submitted command buffer starts executing + submitInfo.waitSemaphoreCount = 1; // One wait semaphore + submitInfo.pSignalSemaphores = &renderCompleteSemaphore; // Semaphore(s) to be signaled when command buffers have completed + submitInfo.signalSemaphoreCount = 1; // One signal semaphore + submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer]; // Command buffers(s) to execute in this batch (submission) + submitInfo.commandBufferCount = 1; // One command buffer - // Submit to the graphics queue - VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE)); + // Submit to the graphics queue passing a wait fence + VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, waitFences[currentBuffer])); + + // Wait until the fence has signaled, which is the case when the command buffer has actually finished execution of all it's commands + VK_CHECK_RESULT(vkWaitForFences(device, 2, &waitFences[currentBuffer], VK_TRUE, UINT64_MAX)); + VK_CHECK_RESULT(vkResetFences(device, 2, &waitFences[currentBuffer])); // Present the current buffer to the swap chain - // We pass the signal semaphore from the submit info - // to ensure that the image is not rendered until - // all commands have been submitted - VK_CHECK_RESULT(swapChain.queuePresent(queue, currentBuffer, semaphores.renderComplete)); + // Pass the semaphore signaled by the command buffer submission from the submit info as the wait semaphore for swap chain presentation + // This ensures that the image is not presented to the windowing system until all commands have been submitted + VK_CHECK_RESULT(swapChain.queuePresent(queue, currentBuffer, renderCompleteSemaphore)); } - // Create synchronzation semaphores - void prepareSemaphore() - { - VkSemaphoreCreateInfo semaphoreCreateInfo = {}; - semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - semaphoreCreateInfo.pNext = NULL; - - // This semaphore ensures that the image is complete - // before starting to submit again - VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &semaphores.presentComplete)); - - // This semaphore ensures that all commands submitted - // have been finished before submitting the image to the queue - VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &semaphores.renderComplete)); - } - - // Setups vertex and index buffers for an indexed triangle, - // uploads them to the VRAM and sets binding points and attribute - // descriptions to match locations inside the shaders + // Prepare vertex and index buffers for an indexed triangle + // Also uploads them to device local memory using staging and initializes vertex input and attribute binding to match the vertex shader void prepareVertices(bool useStagingBuffers) { - struct Vertex { - float pos[3]; - float col[3]; + // A note on memory management in Vulkan in general: + // This is a very complex topic and while it's fine for an example application to to small individual memory allocations that is not + // what should be done a real-world application, where you should allocate large chunkgs of memory at once isntead. + + struct Vertex + { + float position[3]; + float color[3]; }; // Setup vertices - std::vector vertexBuffer = { + std::vector vertexBuffer = + { { { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, { { -1.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } }, { { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } } @@ -491,15 +435,14 @@ public: vkUnmapMemory(device, stagingBuffers.vertices.memory); VK_CHECK_RESULT(vkBindBufferMemory(device, stagingBuffers.vertices.buffer, stagingBuffers.vertices.memory, 0)); - // Create the destination buffer with device only visibility - // Buffer will be used as a vertex buffer and is the copy destination + // Create a device local buffer to which the (host local) vertex data will be copied and which will be used for rendering vertexBufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; - VK_CHECK_RESULT(vkCreateBuffer(device, &vertexBufferInfo, nullptr, &vertices.buf)); - vkGetBufferMemoryRequirements(device, vertices.buf, &memReqs); + VK_CHECK_RESULT(vkCreateBuffer(device, &vertexBufferInfo, nullptr, &vertices.buffer)); + vkGetBufferMemoryRequirements(device, vertices.buffer, &memReqs); memAlloc.allocationSize = memReqs.size; memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &vertices.mem)); - VK_CHECK_RESULT(vkBindBufferMemory(device, vertices.buf, vertices.mem, 0)); + VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &vertices.memory)); + VK_CHECK_RESULT(vkBindBufferMemory(device, vertices.buffer, vertices.memory, 0)); // Index buffer VkBufferCreateInfo indexbufferInfo = {}; @@ -519,48 +462,36 @@ public: // Create destination buffer with device only visibility indexbufferInfo.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; - VK_CHECK_RESULT(vkCreateBuffer(device, &indexbufferInfo, nullptr, &indices.buf)); - vkGetBufferMemoryRequirements(device, indices.buf, &memReqs); + VK_CHECK_RESULT(vkCreateBuffer(device, &indexbufferInfo, nullptr, &indices.buffer)); + vkGetBufferMemoryRequirements(device, indices.buffer, &memReqs); memAlloc.allocationSize = memReqs.size; memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &indices.mem)); - VK_CHECK_RESULT(vkBindBufferMemory(device, indices.buf, indices.mem, 0)); + VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &indices.memory)); + VK_CHECK_RESULT(vkBindBufferMemory(device, indices.buffer, indices.memory, 0)); VkCommandBufferBeginInfo cmdBufferBeginInfo = {}; cmdBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - cmdBufferBeginInfo.pNext = NULL; + cmdBufferBeginInfo.pNext = nullptr; // Buffer copies have to be submitted to a queue, so we need a command buffer for them - // Note that some devices offer a dedicated transfer queue (with only the transfer bit set) - // If you do lots of copies (especially at runtime) it's advised to use such a queu instead - // of a generalized graphics queue (that also supports transfers) + // Note: Some devices offer a dedicated transfer queue (with only the transfer bit set) that may be faster when doing lots of copies VkCommandBuffer copyCmd = getCommandBuffer(true); // Put buffer region copies into command buffer - // Note that the staging buffer must not be deleted before the copies have been submitted and executed - VkBufferCopy copyRegion = {}; // Vertex buffer copyRegion.size = vertexBufferSize; - vkCmdCopyBuffer( - copyCmd, - stagingBuffers.vertices.buffer, - vertices.buf, - 1, - ©Region); + vkCmdCopyBuffer(copyCmd, stagingBuffers.vertices.buffer, vertices.buffer, 1, ©Region); // Index buffer copyRegion.size = indexBufferSize; - vkCmdCopyBuffer( - copyCmd, - stagingBuffers.indices.buffer, - indices.buf, - 1, - ©Region); + vkCmdCopyBuffer(copyCmd, stagingBuffers.indices.buffer, indices.buffer, 1, ©Region); + // Flushing the command buffer will also submit it to the queue and uses a fence to ensure that all commands have been executed before returning flushCommandBuffer(copyCmd); // Destroy staging buffers + // Note: Staging buffer must not be deleted before the copies have been submitted and executed vkDestroyBuffer(device, stagingBuffers.vertices.buffer, nullptr); vkFreeMemory(device, stagingBuffers.vertices.memory, nullptr); vkDestroyBuffer(device, stagingBuffers.indices.buffer, nullptr); @@ -569,10 +500,7 @@ public: else { // Don't use staging - // Create host-visible buffers only and use these for rendering - // This is not advised for real world applications and will - // result in lower performances at least on devices that - // separate between host visible and device local memory + // Create host-visible buffers only and use these for rendering. This is not advised and will usually result in lower rendering performance // Vertex buffer VkBufferCreateInfo vertexBufferInfo = {}; @@ -581,15 +509,15 @@ public: vertexBufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; // Copy vertex data to a buffer visible to the host - VK_CHECK_RESULT(vkCreateBuffer(device, &vertexBufferInfo, nullptr, &vertices.buf)); - vkGetBufferMemoryRequirements(device, vertices.buf, &memReqs); + VK_CHECK_RESULT(vkCreateBuffer(device, &vertexBufferInfo, nullptr, &vertices.buffer)); + vkGetBufferMemoryRequirements(device, vertices.buffer, &memReqs); memAlloc.allocationSize = memReqs.size; memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); - VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &vertices.mem)); - VK_CHECK_RESULT(vkMapMemory(device, vertices.mem, 0, memAlloc.allocationSize, 0, &data)); + VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &vertices.memory)); + VK_CHECK_RESULT(vkMapMemory(device, vertices.memory, 0, memAlloc.allocationSize, 0, &data)); memcpy(data, vertexBuffer.data(), vertexBufferSize); - vkUnmapMemory(device, vertices.mem); - VK_CHECK_RESULT(vkBindBufferMemory(device, vertices.buf, vertices.mem, 0)); + vkUnmapMemory(device, vertices.memory); + VK_CHECK_RESULT(vkBindBufferMemory(device, vertices.buffer, vertices.memory, 0)); // Index buffer VkBufferCreateInfo indexbufferInfo = {}; @@ -598,53 +526,53 @@ public: indexbufferInfo.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT; // Copy index data to a buffer visible to the host - VK_CHECK_RESULT(vkCreateBuffer(device, &indexbufferInfo, nullptr, &indices.buf)); - vkGetBufferMemoryRequirements(device, indices.buf, &memReqs); + VK_CHECK_RESULT(vkCreateBuffer(device, &indexbufferInfo, nullptr, &indices.buffer)); + vkGetBufferMemoryRequirements(device, indices.buffer, &memReqs); memAlloc.allocationSize = memReqs.size; memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); - VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &indices.mem)); - VK_CHECK_RESULT(vkMapMemory(device, indices.mem, 0, indexBufferSize, 0, &data)); + VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &indices.memory)); + VK_CHECK_RESULT(vkMapMemory(device, indices.memory, 0, indexBufferSize, 0, &data)); memcpy(data, indexBuffer.data(), indexBufferSize); - vkUnmapMemory(device, indices.mem); - VK_CHECK_RESULT(vkBindBufferMemory(device, indices.buf, indices.mem, 0)); + vkUnmapMemory(device, indices.memory); + VK_CHECK_RESULT(vkBindBufferMemory(device, indices.buffer, indices.memory, 0)); } - // Binding description - vertices.bindingDescriptions.resize(1); - vertices.bindingDescriptions[0].binding = VERTEX_BUFFER_BIND_ID; - vertices.bindingDescriptions[0].stride = sizeof(Vertex); - vertices.bindingDescriptions[0].inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + // Vertex input binding + vertices.inputBinding.binding = VERTEX_BUFFER_BIND_ID; + vertices.inputBinding.stride = sizeof(Vertex); + vertices.inputBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - // Attribute descriptions - // Describes memory layout and shader attribute locations - vertices.attributeDescriptions.resize(2); - // Location 0 : Position - vertices.attributeDescriptions[0].binding = VERTEX_BUFFER_BIND_ID; - vertices.attributeDescriptions[0].location = 0; - vertices.attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT; - vertices.attributeDescriptions[0].offset = 0; - // Location 1 : Color - vertices.attributeDescriptions[1].binding = VERTEX_BUFFER_BIND_ID; - vertices.attributeDescriptions[1].location = 1; - vertices.attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; - vertices.attributeDescriptions[1].offset = sizeof(float) * 3; + // Inpute attribute binding describe shader attribute locations and memory layouts + // These match the following shader layout (see triangle.vert): + // layout (location = 0) in vec3 inPos; + // layout (location = 1) in vec3 inColor; + vertices.inputAttributes.resize(2); + // Attribute location 0: Position + vertices.inputAttributes[0].binding = VERTEX_BUFFER_BIND_ID; + vertices.inputAttributes[0].location = 0; + vertices.inputAttributes[0].format = VK_FORMAT_R32G32B32_SFLOAT; + vertices.inputAttributes[0].offset = offsetof(Vertex, position); + // Attribute location 1: Color + vertices.inputAttributes[1].binding = VERTEX_BUFFER_BIND_ID; + vertices.inputAttributes[1].location = 1; + vertices.inputAttributes[1].format = VK_FORMAT_R32G32B32_SFLOAT; + vertices.inputAttributes[1].offset = offsetof(Vertex, color); - // Assign to vertex input state + // Assign to the vertex input state used for pipeline creation vertices.inputState.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; - vertices.inputState.pNext = NULL; + vertices.inputState.pNext = nullptr; vertices.inputState.flags = VK_FLAGS_NONE; - vertices.inputState.vertexBindingDescriptionCount = static_cast(vertices.bindingDescriptions.size()); - vertices.inputState.pVertexBindingDescriptions = vertices.bindingDescriptions.data(); - vertices.inputState.vertexAttributeDescriptionCount = static_cast(vertices.attributeDescriptions.size()); - vertices.inputState.pVertexAttributeDescriptions = vertices.attributeDescriptions.data(); + vertices.inputState.vertexBindingDescriptionCount = 1; + vertices.inputState.pVertexBindingDescriptions = &vertices.inputBinding; + vertices.inputState.vertexAttributeDescriptionCount = static_cast(vertices.inputAttributes.size()); + vertices.inputState.pVertexAttributeDescriptions = vertices.inputAttributes.data(); } void setupDescriptorPool() { // We need to tell the API the number of max. requested descriptors per type VkDescriptorPoolSize typeCounts[1]; - // This example only uses one descriptor type (uniform buffer) and only - // requests one descriptor of this type + // This example only uses one descriptor type (uniform buffer) and only requests one descriptor of this type typeCounts[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; typeCounts[0].descriptorCount = 1; // For additional types you need to add new entries in the type count list @@ -656,11 +584,10 @@ public: // All descriptors used in this example are allocated from this pool VkDescriptorPoolCreateInfo descriptorPoolInfo = {}; descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - descriptorPoolInfo.pNext = NULL; + descriptorPoolInfo.pNext = nullptr; descriptorPoolInfo.poolSizeCount = 1; descriptorPoolInfo.pPoolSizes = typeCounts; - // Set the max. number of sets that can be requested - // Requesting descriptors beyond maxSets will result in an error + // Set the max. number of descriptor sets that can be requested from this pool (requesting beyond this limit will result in an error) descriptorPoolInfo.maxSets = 1; VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &descriptorPool)); @@ -669,33 +596,29 @@ public: void setupDescriptorSetLayout() { // Setup layout of descriptors used in this example - // Basically connects the different shader stages to descriptors - // for binding uniform buffers, image samplers, etc. - // So every shader binding should map to one descriptor set layout - // binding + // Basically connects the different shader stages to descriptors for binding uniform buffers, image samplers, etc. + // So every shader binding should map to one descriptor set layout binding - // Binding 0 : Uniform buffer (Vertex shader) + // Binding 0: Uniform buffer (Vertex shader) VkDescriptorSetLayoutBinding layoutBinding = {}; layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; layoutBinding.descriptorCount = 1; layoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - layoutBinding.pImmutableSamplers = NULL; + layoutBinding.pImmutableSamplers = nullptr; VkDescriptorSetLayoutCreateInfo descriptorLayout = {}; descriptorLayout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - descriptorLayout.pNext = NULL; + descriptorLayout.pNext = nullptr; descriptorLayout.bindingCount = 1; descriptorLayout.pBindings = &layoutBinding; - VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, NULL, &descriptorSetLayout)); + VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, nullptr, &descriptorSetLayout)); - // Create the pipeline layout that is used to generate the rendering pipelines that - // are based on this descriptor set layout - // In a more complex scenario you would have different pipeline layouts for different - // descriptor set layouts that could be reused + // Create the pipeline layout that is used to generate the rendering pipelines that are based on this descriptor set layout + // In a more complex scenario you would have different pipeline layouts for different descriptor set layouts that could be reused VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {}; pPipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - pPipelineLayoutCreateInfo.pNext = NULL; + pPipelineLayoutCreateInfo.pNext = nullptr; pPipelineLayoutCreateInfo.setLayoutCount = 1; pPipelineLayoutCreateInfo.pSetLayouts = &descriptorSetLayout; @@ -728,11 +651,11 @@ public: // Binds this uniform buffer to binding point 0 writeDescriptorSet.dstBinding = 0; - vkUpdateDescriptorSets(device, 1, &writeDescriptorSet, 0, NULL); + vkUpdateDescriptorSets(device, 1, &writeDescriptorSet, 0, nullptr); } // Create the depth (and stencil) buffer attachments used by our framebuffers - // Note : Override of virtual function in the base class and called from within VulkanExampleBase::prepare + // Note: Override of virtual function in the base class and called from within VulkanExampleBase::prepare void setupDepthStencil() { // Create an optimal image used as the depth stencil attachment @@ -760,39 +683,9 @@ public: VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &depthStencil.mem)); VK_CHECK_RESULT(vkBindImageMemory(device, depthStencil.image, depthStencil.mem, 0)); - // We need to do an initial layout transition before we can use this image as the depth (and stencil) attachment - // Note that this may be ignored by implementations that don't care about image layout - // transitions, but it's crucial for those that do - - VkCommandBuffer layoutCmd = getCommandBuffer(true); - - VkImageMemoryBarrier imageMemoryBarrier = {}; - imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - imageMemoryBarrier.srcAccessMask = VK_FLAGS_NONE; - imageMemoryBarrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - // Transform layout from undefined (initial) to depth/stencil attachment (usage) - imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, 0, 1, 0, 1 }; - imageMemoryBarrier.image = depthStencil.image; - - vkCmdPipelineBarrier( - layoutCmd, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, - VK_FLAGS_NONE, - 0, nullptr, - 0, nullptr, - 1, &imageMemoryBarrier); - - flushCommandBuffer(layoutCmd); - - // Create a view for ourt depth stencil image - // In Vulkan we can't access the images directly, but rather through views - // that describe a sub resource range - // So you can actually have multiple views for a single image if required + // Create a view for the depth stencil image + // Images aren't directly accessed in Vulkan, but rather through views described by a subresource range + // This allows for multiple views of one image with differing ranges (e.g. for different layers) VkImageViewCreateInfo depthStencilView = {}; depthStencilView.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; depthStencilView.viewType = VK_IMAGE_VIEW_TYPE_2D; @@ -808,22 +701,20 @@ public: } // Create a frame buffer for each swap chain image - // Note : Override of virtual function in the base class and called from within VulkanExampleBase::prepare + // Note: Override of virtual function in the base class and called from within VulkanExampleBase::prepare void setupFrameBuffer() { - // Create a frame buffers for every image in our swapchain + // Create a frame buffer for every image in the swapchain frameBuffers.resize(swapChain.imageCount); for (size_t i = 0; i < frameBuffers.size(); i++) { - std::array attachments; - // Color attachment is the view of the swapchain image - attachments[0] = swapChain.buffers[i].view; - // Depth/Stencil attachment is the same for all frame buffers - attachments[1] = depthStencil.view; + std::array attachments; + attachments[0] = swapChain.buffers[i].view; // Color attachment is the view of the swapchain image + attachments[1] = depthStencil.view; // Depth/Stencil attachment is the same for all frame buffers VkFramebufferCreateInfo frameBufferCreateInfo = {}; frameBufferCreateInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - // All frame buffers use the same renderpass setuü + // All frame buffers use the same renderpass setup frameBufferCreateInfo.renderPass = renderPass; frameBufferCreateInfo.attachmentCount = static_cast(attachments.size()); frameBufferCreateInfo.pAttachments = attachments.data(); @@ -835,145 +726,126 @@ public: } } - // Render pass setup for this example - // Note : Override of virtual function in the base class and called from within VulkanExampleBase::prepare + // Render pass setup + // Render passes are a new concept in Vulkan. They describe the attachments used during rendering and may contain multiple subpasses with attachment dependencies + // This allows the driver to know up-front what the rendering will look like and is a good opportunity to optimize especially on tile-based renderers (with multiple subpasses) + // Using sub pass dependencies also adds implicit layout transitions for the attachment used, so we don't need to add explicit image memory barriers to transform them + // Note: Override of virtual function in the base class and called from within VulkanExampleBase::prepare void setupRenderPass() { - // Render passes are a new concept in Vulkan - // They describe the attachments used during rendering - // and may contain multiple subpasses with attachment - // dependencies - // This allows the driver to know up-front how the - // rendering will look like and is a good opportunity to - // optimize, especially on tile-based renderers (with multiple subpasses) + // This example will use a single render pass with one subpass - // This example will use a single render pass with - // one subpass, which is the minimum setup - - // Two attachments - // Basic setup with a color and a depth attachments + // Descriptors for the attachments used by this renderpass std::array attachments = {}; // Color attachment attachments[0].format = colorformat; - // We don't use multi sampling - // Multi sampling requires a setup with resolve attachments - // See the multisampling example for more details - attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; - // loadOp describes what happens with the attachment content at the beginning of the subpass - // We want the color buffer to be cleared - attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - // storeOp describes what happens with the attachment content after the subpass is finished - // As we want to display the color attachment after rendering is done we have to store it - attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - // We don't use stencil and DONT_CARE allows the driver to discard the result - attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - // Set the initial image layout for the color atttachment - attachments[0].initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - + attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; // We don't use multi sampling in this example + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // Clear this attachment at the start of the render pass + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; // Keep it's contents after the render pass is finished (for displaying it) + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; // We don't use stencil, so don't care for load + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // Same for store + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // Layout at render pass start. Initial doesn't matter, so we use undefined + attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; // Layout to which the attachment is transitioned when the render pass is finished + // As we want to present the color buffer to the swapchain, we transition to PRESENT_KHR // Depth attachment - attachments[1].format = depthFormat; - attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; - // Clear depth at the beginnging of the subpass - attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - // We don't need the contents of the depth buffer after the sub pass - // anymore, so let the driver discard it - attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - // Don't care for stencil - attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - // Set the initial image layout for the depth attachment - attachments[1].initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + attachments[1].format = depthFormat; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // Clear depth at start of first subpass + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // We don't need depth after render pass has finished (DONT_CARE may result in better performance) + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; // No stencil + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // No Stencil + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // Layout at render pass start. Initial doesn't matter, so we use undefined + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; // Transition to depth/stencil attachment - // Setup references to our attachments for the sub pass + // Setup attachment references VkAttachmentReference colorReference = {}; - colorReference.attachment = 0; - colorReference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + colorReference.attachment = 0; // Attachment 0 is color + colorReference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; // Attachment layout used as color during the subpass VkAttachmentReference depthReference = {}; - depthReference.attachment = 1; - depthReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + depthReference.attachment = 1; // Attachment 1 is color + depthReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; // Attachment used as depth/stemcil used during the subpass - // Setup a single subpass that references our color and depth attachments - VkSubpassDescription subpass = {}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - // Input attachments can be used to sample from contents of attachments - // written to by previous sub passes in a setup with multiple sub passes - // This example doesn't make use of this - subpass.inputAttachmentCount = 0; - subpass.pInputAttachments = nullptr; - // Preserved attachments can be used to loop (and preserve) attachments - // through a sub pass that does not actually use them - // This example doesn't make use of this - subpass.preserveAttachmentCount = 0; - subpass.pPreserveAttachments = nullptr; - // Resoluve attachments are resolved at the end of a sub pass and can be - // used for e.g. multi sampling - // This example doesn't make use of this - subpass.pResolveAttachments = nullptr; - // Reference to the color attachment - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &colorReference; - // Reference to the depth attachment - subpass.pDepthStencilAttachment = &depthReference; + // Setup a single subpass reference + VkSubpassDescription subpassDescription = {}; + subpassDescription.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpassDescription.colorAttachmentCount = 1; // Subpass uses one color attachment + subpassDescription.pColorAttachments = &colorReference; // Reference to the color attachment in slot 0 + subpassDescription.pDepthStencilAttachment = &depthReference; // Reference to the depth attachment in slot 1 + subpassDescription.inputAttachmentCount = 0; // Input attachments can be used to sample from contents of a previous subpass + subpassDescription.pInputAttachments = nullptr; // (Input attachments not used by this example) + subpassDescription.preserveAttachmentCount = 0; // Preserved attachments can be used to loop (and preserve) attachments through subpasses + subpassDescription.pPreserveAttachments = nullptr; // (Preserve attachments not used by this example) + subpassDescription.pResolveAttachments = nullptr; // Resolve attachments are resolved at the end of a sub pass and can be used for e.g. multi sampling - // Setup the render pass + // Setup subpass dependencies + // These will add the implicit ttachment layout transitionss specified by the attachment descriptions + // The actual usage layout is preserved through the layout specified in the attachment reference + // Each subpass dependency will introduce a memory and execution dependency between the source and dest subpass described by + // srcStageMask, dstStageMask, srcAccessMask, dstAccessMask (and dependencyFlags is set) + // Note: VK_SUBPASS_EXTERNAL is a special constant that refers to all commands executed outside of the actual renderpass) + std::array dependencies; + + // First dependency at the start of the renderpass + // Does the transition from final to initial layout + dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL; // Producer of the dependency + dependencies[0].dstSubpass = 0; // Consumer is our single subpass that will wait for the execution depdendency + dependencies[0].srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + dependencies[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependencies[0].srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; + dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dependencies[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; + + // Second dependency at the end the renderpass + // Does the transition from the initial to the final layout + dependencies[1].srcSubpass = 0; // Producer of the dependency is our single subpass + dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL; // Consumer are all commands outside of the renderpass + dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependencies[1].dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dependencies[1].dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; + dependencies[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; + + // Create the actual renderpass VkRenderPassCreateInfo renderPassInfo = {}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; - // Set attachments used in our renderpass - renderPassInfo.attachmentCount = static_cast(attachments.size()); - renderPassInfo.pAttachments = attachments.data(); - // We only use one subpass - renderPassInfo.subpassCount = 1; - renderPassInfo.pSubpasses = &subpass; - // As we only use one subpass we don't have any subpass dependencies - renderPassInfo.dependencyCount = 0; - renderPassInfo.pDependencies = nullptr; + renderPassInfo.attachmentCount = static_cast(attachments.size()); // Number of attachments used by this render pass + renderPassInfo.pAttachments = attachments.data(); // Descriptions of the attachments used by the render pass + renderPassInfo.subpassCount = 1; // We only use one subpass in this example + renderPassInfo.pSubpasses = &subpassDescription; // Description of that subpass + renderPassInfo.dependencyCount = static_cast(dependencies.size()); // Number of subpass dependencies + renderPassInfo.pDependencies = dependencies.data(); // Subpass dependencies used by the render pass - // Create the renderpass VK_CHECK_RESULT(vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass)); } void preparePipelines() { - // Create our rendering pipeline used in this example - // Vulkan uses the concept of rendering pipelines to encapsulate - // fixed states - // This replaces OpenGL's huge (and cumbersome) state machine - // A pipeline is then stored and hashed on the GPU making - // pipeline changes much faster than having to set dozens of - // states - // In a real world application you'd have dozens of pipelines - // for every shader set used in a scene - // Note that there are a few states that are not stored with - // the pipeline. These are called dynamic states and the - // pipeline only stores that they are used with this pipeline, - // but not their states + // Create the graphics pipeline used in this example + // Vulkan uses the concept of rendering pipelines to encapsulate fixed states, replacing OpenGL's complex state machine + // A pipeline is then stored and hashed on the GPU making pipeline changes very fast + // Note: There are still a few dynamic states that are not directly part of the pipeline (but the info that they are used is) VkGraphicsPipelineCreateInfo pipelineCreateInfo = {}; - pipelineCreateInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - // The layout used for this pipeline + // The layout used for this pipeline (can be shared among multiple pipelines using the same layout) pipelineCreateInfo.layout = pipelineLayout; // Renderpass this pipeline is attached to pipelineCreateInfo.renderPass = renderPass; - // Vertex input state - // Describes the topoloy used with this pipeline + // Construct the differnent states making up the pipeline + + // Input assembly state describes how primitives are assembled + // This pipeline will assemble vertex data as a triangle lists (though we only use one triangle) VkPipelineInputAssemblyStateCreateInfo inputAssemblyState = {}; inputAssemblyState.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - // This pipeline renders vertex data as triangle lists inputAssemblyState.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; // Rasterization state VkPipelineRasterizationStateCreateInfo rasterizationState = {}; - rasterizationState.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - // Solid polygon mode - rasterizationState.polygonMode = VK_POLYGON_MODE_FILL; - // No culling + rasterizationState.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizationState.polygonMode = VK_POLYGON_MODE_FILL; rasterizationState.cullMode = VK_CULL_MODE_NONE; rasterizationState.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rasterizationState.depthClampEnable = VK_FALSE; @@ -981,45 +853,38 @@ public: rasterizationState.depthBiasEnable = VK_FALSE; rasterizationState.lineWidth = 1.0f; - // Color blend state - // Describes blend modes and color masks - VkPipelineColorBlendStateCreateInfo colorBlendState = {}; - colorBlendState.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - // One blend attachment state - // Blending is not used in this example + // Color blend state describes how blend factors are calculated (if used) + // We need one blend attachment state per color attachment (even if blending is not used VkPipelineColorBlendAttachmentState blendAttachmentState[1] = {}; blendAttachmentState[0].colorWriteMask = 0xf; blendAttachmentState[0].blendEnable = VK_FALSE; + VkPipelineColorBlendStateCreateInfo colorBlendState = {}; + colorBlendState.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; colorBlendState.attachmentCount = 1; colorBlendState.pAttachments = blendAttachmentState; - // Viewport state + // Viewport state sets the number of viewports and scissor used in this pipeline + // Note: This is actually overriden by the dynamic states (see below) VkPipelineViewportStateCreateInfo viewportState = {}; viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - // One viewport viewportState.viewportCount = 1; - // One scissor rectangle viewportState.scissorCount = 1; // Enable dynamic states - // Describes the dynamic states to be used with this pipeline - // Dynamic states can be set even after the pipeline has been created - // So there is no need to create new pipelines just for changing - // a viewport's dimensions or a scissor box - VkPipelineDynamicStateCreateInfo dynamicState = {}; - // The dynamic state properties themselves are stored in the command buffer + // Most states are baked into the pipeline, but there are still a few dynamic states that can be changed within a command buffer + // To be able to change these we need do specify which dynamic states will be changed using this pipeline. Their actual states are set later on in the command buffer. + // For this example we will set the viewport and scissor using dynamic states std::vector dynamicStateEnables; dynamicStateEnables.push_back(VK_DYNAMIC_STATE_VIEWPORT); dynamicStateEnables.push_back(VK_DYNAMIC_STATE_SCISSOR); + VkPipelineDynamicStateCreateInfo dynamicState = {}; dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamicState.pDynamicStates = dynamicStateEnables.data(); dynamicState.dynamicStateCount = static_cast(dynamicStateEnables.size()); - // Depth and stencil state - // Describes depth and stenctil test and compare ops + // Depth and stencil state containing depth and stencil compare and test operations + // We only use depth tests and want depth tests and writes to be enabled and compare with less or equal VkPipelineDepthStencilStateCreateInfo depthStencilState = {}; - // Basic depth compare setup with depth writes and depth test enabled - // No stencil used depthStencilState.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depthStencilState.depthTestEnable = VK_TRUE; depthStencilState.depthWriteEnable = VK_TRUE; @@ -1032,20 +897,20 @@ public: depthStencilState.front = depthStencilState.back; // Multi sampling state + // This example does not make use fo multi sampling (for anti-aliasing), the state must still be set and passed to the pipeline VkPipelineMultisampleStateCreateInfo multisampleState = {}; multisampleState.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - multisampleState.pSampleMask = NULL; - // No multi sampling used in this example multisampleState.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + multisampleState.pSampleMask = nullptr; // Load shaders - // Shaders are loaded from the SPIR-V format, which can be generated from glsl + // Vulkan loads it's shaders from an immediate binary representation called SPIR-V + // Shaders are compiled offline from e.g. GLSL using the reference glslang compiler std::array shaderStages; shaderStages[0] = loadShader(getAssetPath() + "shaders/triangle.vert.spv", VK_SHADER_STAGE_VERTEX_BIT); shaderStages[1] = loadShader(getAssetPath() + "shaders/triangle.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT); - // Assign states - // Assign pipeline state create information + // Assign the pipeline states to the pipeline creation info structure pipelineCreateInfo.stageCount = static_cast(shaderStages.size()); pipelineCreateInfo.pStages = shaderStages.data(); pipelineCreateInfo.pVertexInputState = &vertices.inputState; @@ -1058,27 +923,27 @@ public: pipelineCreateInfo.renderPass = renderPass; pipelineCreateInfo.pDynamicState = &dynamicState; - // Create rendering pipeline + // Create rendering pipeline using the specified states VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCreateInfo, nullptr, &pipeline)); } void prepareUniformBuffers() { // Prepare and initialize a uniform buffer block containing shader uniforms - // In Vulkan there are no more single uniforms like in GL - // All shader uniforms are passed as uniform buffer blocks + // Single uniforms like in OpenGL are no longer present in Vulkan. All Shader uniforms are passed via uniform buffer blocks VkMemoryRequirements memReqs; // Vertex shader uniform buffer block VkBufferCreateInfo bufferInfo = {}; VkMemoryAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.pNext = NULL; + allocInfo.pNext = nullptr; allocInfo.allocationSize = 0; allocInfo.memoryTypeIndex = 0; bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferInfo.size = sizeof(uboVS); + // This buffer will be used as a uniform buffer bufferInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; // Create a new buffer @@ -1087,19 +952,16 @@ public: vkGetBufferMemoryRequirements(device, uniformDataVS.buffer, &memReqs); allocInfo.allocationSize = memReqs.size; // Get the memory type index that supports host visibile memory access - // Most implementations offer multiple memory tpyes and selecting the - // correct one to allocate memory from is important - // We also want the buffer to be host coherent so we don't have to flush - // after every update. - // Note that this may affect performance so you might not want to do this - // in a real world application that updates buffers on a regular base + // Most implementations offer multiple memory types and selecting the correct one to allocate memory from is crucial + // We also want the buffer to be host coherent so we don't have to flush (or sync after every update. + // Note: This may affect performance so you might not want to do this in a real world application that updates buffers on a regular base allocInfo.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); // Allocate memory for the uniform buffer VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &(uniformDataVS.memory))); // Bind memory to buffer VK_CHECK_RESULT(vkBindBufferMemory(device, uniformDataVS.buffer, uniformDataVS.memory, 0)); - // Store information in the uniform's descriptor + // Store information in the uniform's descriptor that is used by the descriptor set uniformDataVS.descriptor.buffer = uniformDataVS.buffer; uniformDataVS.descriptor.offset = 0; uniformDataVS.descriptor.range = sizeof(uboVS); @@ -1123,13 +985,15 @@ public: uint8_t *pData; VK_CHECK_RESULT(vkMapMemory(device, uniformDataVS.memory, 0, sizeof(uboVS), 0, (void **)&pData)); memcpy(pData, &uboVS, sizeof(uboVS)); + // Unmap after data has been copied + // Note: Since we requested a host coherent memory type for the uniform buffer, the write is instantly visible to the GPU vkUnmapMemory(device, uniformDataVS.memory); } void prepare() { VulkanExampleBase::prepare(); - prepareSemaphore(); + prepareSynchronizationPrimitives(); prepareVertices(USE_STAGING); prepareUniformBuffers(); setupDescriptorSetLayout(); @@ -1149,8 +1013,7 @@ public: virtual void viewChanged() { - // This function is called by the base example class - // each time the view is changed by user input + // This function is called by the base example class each time the view is changed by user input updateUniformBuffers(); } }; @@ -1160,7 +1023,7 @@ VulkanExample *vulkanExample; #if defined(_WIN32) LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { - if (vulkanExample != NULL) + if (vulkanExample != nullptr) { vulkanExample->handleMessages(hWnd, uMsg, wParam, lParam); } @@ -1169,7 +1032,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) #elif defined(__linux__) && !defined(__ANDROID__) static void handleEvent(const xcb_generic_event_t *event) { - if (vulkanExample != NULL) + if (vulkanExample != nullptr) { vulkanExample->handleEvent(event); }