Vulkan is not well-suited to simple test applications; neither is it a suitable aid for teaching graphics concepts.
To remain true to the quote, we actually don't want to fully explain how to write a minimal Vulkan application. The main reasons for this decision are, on the one hand, that such a "minimal" project consists of approximately 2000 lines of code. On the other hand these lines are quite verbose and thus boring to read. That's why we are only show code examples where we can compare Vulkan to OpenGL easily.
For the curious, the whole code can, of course, be found on out gitlab page. There are also detailed instructions on how to build the project yourself.
We also want to draw your attention to these excellent Vulkan sample projects by Sascha Willems and this extremely comprehensive Vulkan tutorial.
From this small sample code, we can already observe some recurring patterns. Pay attention to the
sType
data member of each Vulkan struct: it must always
be filled with an enumeration value that corresponds to the struct's type.
The first code paragraph shows how to initialize the command buffer begin info struct. The flag
we are giving is the most flexible one, though it is not really needed here.
In line 6, we actually begin the recording of the command buffer. All vkCmd...
functions
from now on will put the commands into the currently active command buffer. Jumping to line 42, you see the
corresponding vkEndCommandBuffer
call, which ends the recording. All errors that
occured during the recording are postponed until now. hasError
is a simple helper function that checks
for generic Vulkan errors [source].
Starting with line 8, we begin our first and only render pass by, again, filling out a struct. The most notable
member variables are the area we want to draw on, given by an offset
of (0, 0)
and the width and height of basically the whole window.
The next paragraph is similar to glClearColor
and glClearDepth
. Here,
we set the clear color for the frame buffer to a dark grey with full opacity and the depth buffer to 1.0f
which is also the
default value in OpenGL and denotes the maximum depth.
Now, line 22, we record the start of the render pass, which is ended in line 40. The next paragraph is again similar to the OpenGL render loop. However, instead of binding the whole vertex array object, we must manually bind the buffer that holds the vertex data and after that the buffer containing the indices. For performance reasons, all objects are stored inside a single buffer, because allocating many small memory segments is very inefficient in Vulkan. The distinction is simply made by an offset and length, which is stored for each object.
Inside the for loop, we bind the pipeline, which is similar to the glUseProgram
, although there
are more things configured inside the Vulkan pipeline – e.g. wireframe mode ...
vkCmdBindDescriptorSets
is handled completely transparent in OpenGL. In Vulkan
however, we must explicitly describe the uniform data we are about to send during render.
Finally, after calculating the next vertex buffer offset, we record the actual draw call. Note that in Vulkan there is no distinction between an indexed draw call and an individual draw call. Everything is handled with the function arguments.
m_
are member variables of the
application class. The exact definition is not important here – the name should make the meaning clear.
1 VkCommandBufferBeginInfo begin;
2 begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
3 begin.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
4 begin.pInheritanceInfo = nullptr;
5
6 vkBeginCommandBuffer(m_drawCommandBuffer, &begin);
7
8 VkRenderPassBeginInfo render;
9 render.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
10 render.renderPass = m_renderPass;
11 render.framebuffer = m_framebuffer;
12 render.renderArea.offset = {0, 0};
13 render.renderArea.extent = m_swapchain.extent;
14
15 std::array<VkClearValue, 2> clear;
16 clear[0].color = {0.1f, 0.1f, 0.1f, 1.0f};
17 clear[1].depthStencil = {1.0f, 0};
18
19 render.clearValueCount = (uint32_t)clear.size();
20 render.pClearValues = clear.data();
21
22 vkCmdBeginRenderPass(m_drawCommandBuffer, &render, VK_SUBPASS_CONTENTS_INLINE);
23
24 VkDeviceSize offsets[] = {0};
25 vkCmdBindVertexBuffers(m_drawCommandBuffer, 0, 1, m_vertexBuffer, offsets);
26 vkCmdBindIndexBuffer(m_drawCommandBuffer, m_indexBuffer, 0, VK_INDEX_TYPE_UINT32);
27
28 uint32_t dynamicOffset = 0;
29 for (const auto &object : m_scene.objects)
30 {
31 vkCmdBindPipeline(m_drawCommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, object.pipeline);
32
33 vkCmdBindDescriptorSets(m_drawCommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, object.pipeline.layout, 0, 1,
34 &object.texture.descriptorSet, 1, &dynamicOffset);
35
36 dynamicOffset += object.pipeline.uniform.localAlignment;
37 vkCmdDrawIndexed(m_drawCommandBuffer, object.model.size, object.instanceCount, object.model.offset, 0, object.firstInstance);
38 }
39
40 vkCmdEndRenderPass(m_drawCommandBuffer);
41
42 if(hasError(vkEndCommandBuffer(m_drawCommandBuffer)))
43 {
44 throw std::runtime_error("Failed to record command buffers");
45 }
In contrast to OpenGL, we must explicitly take care of the synchronization
when rendering and operating the swap chain. Let's start with acquiring an image
from the swapchain so we can start drawing a frame. At first, we must make sure that
the command queue that handles presentation is not busy anymore (line 1). Next, we
actually request the image, passing a reference to a semaphore (m_imageAvailableSemaphore
)
that will be signaled once the image is, indeed, available. There are some things that
can go wrong, but we won't discuss the error handling here.
1 vkQueueWaitIdle(m_presentationQueue);
2
3 uint32_t image;
4 VkResult result = vkAcquireNextImageKHR(m_device, m_swapchain, std::numeric_limits<uint64_t>::max(), m_imageAvailableSemaphore, VK_NULL_HANDLE, &image);
5
6 if(result == VK_ERROR_OUT_OF_DATE_KHR)
7 {
8 recreateSwapChain();
9 return;
10 }
11 else if(result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR)
12 {
13 throw std::runtime_error("Failed to acquire swap chain image");
14 }
Now, we must fill a struct to handle additional synchronizations. In line 6 we specify that the rendering can only start when the swapchain image is available. Actually only the stages that output color information on this image are required to wait – which is what we configure with lines 4 and 7. Last, we pass the command queue responsible for rendering and a semaphore that is signaled once the rendering is done. Finally we submit the commands and, again, handle the possible errors.
1 VkSubmitInfo submit;
2 submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
3
4 VkPipelineStageFlags wait[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
5 submit.waitSemaphoreCount = 1;
6 submit.pWaitSemaphores = &m_imageAvailableSemaphore;
7 submit.pWaitDstStageMask = wait;
8 submit.commandBufferCount = 1;
9 submit.pCommandBuffers = &m_drawCommandBuffer;
10 submit.signalSemaphoreCount = 1;
11 submit.pSignalSemaphores = &m_renderFinishedSemaphore;
12
13 if(hasError(vkQueueSubmit(m_graphicsQueue, 1, &submit, VK_NULL_HANDLE)))
14 {
15 throw std::runtime_error("Failed to submit draw command buffer");
16 }
In the last step, we submit the fresh image back to the swapchain so it can be displayed. We want the display step to wait for the rendering which is why we pass the appropriate semaphore here. In the end we actually present the image and handle the errors.
KHR
suffix are
actually extensions as Vulkan itself does not handle the presentation
in order to keep the core simple.
1 VkPresentInfoKHR present;
2 present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
3 present.waitSemaphoreCount = 1;
4 present.pWaitSemaphores = &m_renderFinishedSemaphore;
5 present.swapchainCount = 1;
6 present.pSwapchains = m_swapchain;
7 present.pImageIndices = ℑ
8 present.pResults = nullptr;
9
10 result = vkQueuePresentKHR(m_presentationQueue, &present);
11
12 if(result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR)
13 {
14 recreateSwapChain();
15 }
16 else if(hasError(result))
17 {
18 throw std::runtime_error("Failed to present swap chain image");
19 }