The previous article was devoted to a bit more general overview of the new Vulkan API. In the next article, we’ll look Vulcan under the hood and compare Vulcan and OpenGL programming approaches.
Vulcan is less abstract and more low-level. This will allow the end-user application to better optimize performance, but in return it will require much more code and much more responsibility from the developer. Vulcan is really much more complicated than OpenGL, and junior graphic programmers should still start learning with OpenGL (which is far from retiring).
Vulkan is an ANSI-C API. It does not use object-oriented approaches and just like OpenGL is based on free-entry-point functions (if you want to make Vukan more user-friendly, I would recommend using the C++ wrapper Vulkan-HPP). OpenGL was based on a global state machine that was bound to the current OpenGL context. All settings (e.g. GlPointSize()) were fed by the global functions to the current context, and context remembered everything until the setting was overwritten by the new one or the context was destroyed. Vulcan does not have a context or a state machine. Status and data are bound to a handler named VkInstance. A lot of data is also passed between functions using a number of emunerations – unlike to OpenGL, Vulkan is strong-typed. Vulkan is also not bound by the paradigm that “something will be drawn on the screen”. For example, an application may require offscreen rendering, but may not require rendering at all. Vulcan can also be used for purely computational operations – just as we would normally expect from OpenCL.
To meet its ambitious goals, Vulkan adds a number of new features and mechanisms that OpenGL did not contain or hide from the user beneath the layer of abstractness. So lets take a look on all those Command buffers, pipelines, rendering passes, Swapchains,…
The Command Buffer mechanism is probably one of the most beneficial Vulcan innovations. One of the basic flaws in OpenGL was the impossibility of using multiple CPU cores to do a draw-calls. Even on today’s 4 or more core CPUs, OpenGL drew only with one thread. Of course even OpenGL had some command buffer, but it was hidden in the driver structure, and had only implicit sync mechanisms (if we omit glFinish() and glFlush()). OpenGL was an immediate mode API. So, if the application called a draw-call (e.g. glDrawArrays(GL_TRIANGLES, 0, 3)), the operation was immediately queued on the driver side, but it could not be detected on the application side if the call had already been processed. It was also not possible to optimize the calls to prevent driver to unnecessarily submit an incomplete queue of operations to the GPU. Starting/ending the submit always means a certain overhead, so it’s beneficial to transfer as many draw-calls as possible in a one batch.
The Command Buffers mechanism is the key to multi-threaded Vulkan API support. The end application can create any number of Command buffers, and then fill each with a draw-calls or other operations from a multiple threads. Subsequently application can submit all of the stacked operations at once (it is often possible to limit the submits to the GPU to one for each frame), or according to what is advantageous for the current graphical/computing task.
Again – Vulkan is not just about drawing. Command Buffer may contain commands for:
- Transfer of data (Transfer)
- Drawing (Graphics)
- General compute operations (Compute)
There are two types of Command buffers – Primary and Secondary. Primary is usually only one. A secondary buffer typically exists one for each thread. Upon finalizing the work with the buffers, the content of the secondary Command buffers is merged into the Primary buffer and then is submitted to the GPU. Each Command buffer is allocated from its Command Pool – an object that manages the required memory and controls the basic synchronization.
A very useful feature of Command buffers is their reusability. If a currently rendered image is not very different from the previous image (in matter of performed operations, not necessarily pixel by pixel), the Command buffers can be reused to save a significant amount of CPU time.
It’s like a rulebook of how to render the graphics. Includes basic settings for scene rendering or management of input parameters for the shaders. It can also include multiple subpasses for rendering into different output buffers (Depth-buffer, Gbuffer, etc…). Crucial object for drawing anything in Vulcan. All rendering commands typically require to be “inside” the Rendering pass. In case of simpliest rendering application we find in Command buffer at least vkCmdBeginRenderPass → Bind Pipeline → Bind Descriptors → Draw → vkCmdEndRenderPass.
In case of OpenGL, at the end of the rendering process the scene usually ends up in the output buffer (in the case of the double buffering in the back buffer). Vulcan, however, has no similar default output buffer. Here, a series of output buffers is called Swapchain and the end application has to create it itself. Because work with output buffers requires platform-dependent components, Swapchain is not a fixed part of the API. It is available only as an extension and support for the Swapchain must be explicitly verify the by the end application.
Vulkan is undoubtedly the future, but the ease of deployment of OpenGL still gives it the right to live in many (especially non-gaming) applications. Especially since OpenGL 4.0. lot of rendering can be done in very efficient ways – employing mechanisms similar to what is typical for Vulkan. Implementation of Vulcan is still at the begin and especially nVidia graphics cards may even show a drop in performance compared to OpenGL. Better HW/SW adaptation to the modern low-level API (in the case of nVidia, especially Asynchronous compute enhancements) can be hovewer expected soon. Then Vulkan can become a synonym for interacting with the GPU for several decades to come.