Structure of a Vulkan Application#

While a Vulkan application can follow the same structure of initialisation, rendering, and deinitialisation as most other graphics applications, a lot of the work in Vulkan can be done in advance and reused. While this may bring about more work for the developer to do during the initialisation step, it also paves the way for significant gains elsewhere.

The overall structure of a Vulkan application is as follows:

  1. Creating the Vulkan instance.

  2. Creating and initialising the components used for rendering.

  3. Recording the command buffers.

  4. Drawing the frame.

  5. Deinitialisation and cleanup.

Note

In simple Vulkan applications, recording the command buffers can be done in advance, as part of the initialisation steps. This does not apply for dynamic applications, where geometry moves in and out of the frame; in those cases, recording the command buffers is done per frame.

The concepts needed for understanding how Vulkan processes data and renders a frame has already been explained, but many of these objects need to be initialised before the rendering occurs. Of note is the step where commands are recorded into the command buffer. As mentioned above, while this is commonly unrealistic in applications such as games, where data is constantly changing during runtime, it can be done during the initialisation stages in Vulkan, and it is desirable to do so where possible in order to improve performance.

The steps performed for each stage are detailed below.

Creating the Vulkan instance#

This refers to setting up the application to use Vulkan. This does not just refer to the API call, but also to various Vulkan-specific features such as validation layers and extensions, as well as some basic objects needed for the application. Some of these include:

  • The Vulkan library instance.

  • A physical device to represent the GPU.

  • A logical device and any queues needed.

Creating and initialising the components used for rendering#

Some objects were instantiated alongside the instance, but more objects need to be created to properly initialise Vulkan and the GPU. This includes all objects required to either present the rendered output, or facilitate the production of said output:

  • A window surface where the images will be presented.

  • A swapchain containing a collection of the images being rendered and presented.

  • The swapchain images themselves.

The list above only covers elements to do with output to a screen. Even in a simple application, Vulkan requires several different objects that are linked and interact with one another in order to perform rendering; most of which need to be initialised before rendering can begin. These include:

  • Command buffers to hold commands that will be submitted to the queue.

  • Shader modules to represent the shader source code. Sometimes the shader code may be precompiled.

  • Various other memory objects to store data needed for rendering – for example, textures, buffers, and images.

  • Renderpass objects and framebuffer objects representing all of the image objects that will be rendered to.

  • A Pipeline object representing the GPU as it transforms vertex data into a final rendered image.

  • Synchronisation objects to ensure the rendering operations occur in the correct order.

Recording command buffers#

This is the stage where GPU commands such as VkDrawElements get recorded into buffers so that they can be submitted to the GPU. These commands will also contain information about which pipeline to use for rendering, what resources the pipeline will need (i.e. buffers), as well as the draw command itself.

During the initialisation, a number of Command Buffers equal to the number of Swapchain images is generated. The application will then iterate through each command buffer and record the same set of commands into them before using specific Command Buffers each frame to execute the tasks asynchronously.

Different Command Buffers would usually be recorded for different rendering tasks (for example, shadow mapping, rendering, and post-processing). In the case of this example, because there is only one pass, all the command buffers can have the same information.

Drawing the frame#

This is where the actual work begins. Once the command buffer is submitted to the queue, the queue is executed by the GPU, subject to synchronisation (which will be discussed later). The scene is then rendered from the commands executed. The rendered result is submitted to the queue and then presented to the screen. Most graphics applications use a rendering loop to constantly draw frames in the background and present them to the display. In this example, the application will redraw the frame 800 times before exiting.

Deinitialisation and cleanup#

As with any other well-written application, various objects and memory allocated need to be freed up to prevent resource leaks. In this example, on application exit, everything would be freed anyway. However, in more complex applications, many objects may need to be accounted for and released when they are no longer needed, in order to free the resources they are using.