General Structure of a Vulkan Application

A step-by-step on how a Vulkan application processes information and produces output from input.

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. Again, 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 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: * It is worth mentioning here that recording the command buffers is something that can be done in advance (i.e. as part of the initialisation steps. Usually this will be done in each frame in dynamic applications where geometry moves in and out of the frame, but for simple applications, it is possible to do this all in advance in Vulkan.

The concepts needed for understanding how Vulkan processes data and renders a frame has already been explained, but a lot 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 is something that 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 that are performed for each stage is outlined below, and each step will be elaborated on in its own section.

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 there are more objects that need to be created in order to properly initialise Vulkan and the GPU. These are all objects that are required in order to either present the rendered output, or facilitate the production of said output. This includes:

  • A window surface where the images will be presented
  • A swapchain which contains a collection of the images that are being rendered and then presented
  • The swapchain images themselves

The above list only covers elements that are 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 which is needed for rendering, for example textures, buffers, and images
  • Renderpass objects and framebuffer objects which represent all of the image objects that will be rendered to
  • A Pipeline object which represents 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 actual draw command itself.

During the initialisation, a number of Command Buffers are generated equal to the number of Swapchain images. 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 (e.g. shadow mapping, rendering, post-processing), but 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 then 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 in use, in order to free the resources they are using.