Introduction

Drawing our first triangle is the Hello World of Vulkan.

In the following paragraphs, we will provide an overview of the Vulkan API necessary to achieve this goal. If you find it overly complex or daunting, this is precisely why Linuxviewer was created. At this stage, there’s no need to grasp every detail; in the next chapter, we will explain how to accomplish the same objective using the Linuxviewer API.

Note that each header is a hyperlink to the respective Vulkan API documentation, but since they are all handles all you will find there are more links to other Vulkan functions that are related. For example, among those you will find vkCreateXyz function(s), that are typically used to create such a handle. These hyper links are mostly there to further emphasize the difficulty of using the Vulkan API directly, but have a blast clicking on them.

VkSurfaceKHR

Before that first triangle can appear on the screen, we need to create a drawing surface. That is usually the inner part of a window, managed by the window manager, or it could be the full display output surface (the root window on X11) of one of the monitors connected to the PC. In some cases the driver of the GPU card might be able to provide a surface that spans multiple monitors.

Vulkan itself is a platform agnostic API and doesn’t care if the surface is a window, a monitor, or something else. To establish the connection between Vulkan and the window system to present results to the screen, we need to use one of the WSI (Window System Integration) extensions.

Note:

Linuxviewer currently only supports drawing to an XCB window. XCB (X protocol C-language Binding) is a low-level library specifically designed for communicating with the X11 window system on GNU/Linux. There is no direct support for Wayland at the moment, except by using a compatibility layer like XWayland. Support for Wayland and/or full-screen should be rather easy to add (will not be influenced by other code).

VkPhysicalDevice

The choice of surface to draw on may affect the selection of GPU card to be used. The specific GPU, or physical device, that will be utilized primarily depends on the desired monitor for display.

stefan didak's home office with five monitors

VkDevice

Once the GPU has been established one needs to create a so called Logical Device which involves specifying which Vulkan features are required and which command queues will be needed.

VkSwapchainKHR

The swapchain is a collection of render targets (VkImage’s). Its main purpose is to ensure that the image that we’re currently rendering to is different from the one that is currently on the screen.

At the beginning of the render loop of a window we acquire an unused VkImage from the swapchain that we can render to. The GPU will then render into this image and, once completed, swap it with the image that is being shown on the screen (called presenting) usually synced with the VSYNC of the monitor in order to avoid tearing. The old image that was being displayed is returned to the swapchain for reuse with a future frame.

To draw to an image acquired from the swapchain, we have to wrap it into a VkImageView .

VkRenderPass

Attachments

In the context of Vulkan and Render Passes, an attachment refers to a memory resource, usually an image, that serves as an input or output target for rendering operations. Attachments are bound to a framebuffer and are utilized during the execution of a Render Pass.

There are different types of attachments, each with its specific role in the rendering pipeline:

  1. Color attachment: This type of attachment stores color information generated by the fragment shader during the rendering process. A Render Pass can have multiple color attachments, which are typically used for rendering to multiple render targets or when implementing techniques like deferred shading.

  2. Depth attachment: A depth attachment is used to store depth information, which is essential for depth testing in 3D graphics. The depth buffer helps determine whether a fragment is occluded by other geometry in the scene and should be discarded or not.

  3. Stencil attachment: The stencil attachment is used for stencil testing and operations, allowing for more complex visibility tests and masking operations. Stencil testing is often used in techniques such as shadow mapping, decals, and outlining objects.

Attachments are described using the VkAttachmentDescription structure, which specifies their format, sample count, and load/store operations.

Framebuffer

A framebuffer is a collection of attachments, such as color, depth, and stencil buffers, that serve as input or output targets for rendering operations within a Render Pass.

When you create a framebuffer for a Render Pass, you typically provide a list of VkImageViews that correspond to the attachment descriptions. The VkImageViews define how the underlying VkImages will be accessed during rendering operations, including details such as format, dimensions, and specific layers or mip levels of the image.

One of these VkImageViews will be created from a swapchain image, which serves as the final color output target where the rendered scene will be stored. In this context, the swapchain image is used as an (output) color attachment.

Note:

The vk::Framebuffer that is used rendering by Linuxviewer is encapsulated by vulkan::RenderPass that stores a unique pointer to an imageless framebuffer. Consequently, Vulkan 1.3 is required as is support for this feature by the physical device. As a result of using an imageless framebuffer we only require one framebuffer per renderpass as opposed to needing as many as there are VkImage’s in the swapchain.

An imageless framebuffer is a special kind of framebuffer that does not have any VkImages directly attached to it. Instead, it is created with a list of attachment descriptions that define the properties of the attachments, but without specifying the actual VkImageViews that will be used.

When using an imageless framebuffer, the actual VkImageViews are specified dynamically during command buffer recording using the vkCmdBeginRenderPass2 function, and its VkRenderPassAttachmentBeginInfo structure.

Render Pass

A Render Pass in Vulkan is an important high-level abstraction representing a series of rendering operations that are executed on one or more attachments. These attachments are usually images that serve as input or output targets for the various rendering stages in the pipeline. A Render Pass specifies the layout and format of these attachments, as well as their usage and load/store operations.

During the execution of a Render Pass, rendering commands will read from and write to the specified attachments, effectively defining the input and output of the rendering process.

VkPipeline

The configurable state of the graphics card, excluding the programmable shader stages, consists of, for example, the viewport size, scissor rectangles, blend modes, depth and stencil operations, rasterization settings, and the binding of shader resources such as textures, samplers, and uniform buffers.

On top of that it can have several stages that are programmable with shader code, in Vulkan referenced with a VkShaderModule handle, corresponding to a memory image containing SPIR-V code.

Both types of state together, with the exception of a few dynamically configurable states (independent of the VkPipeline) like the viewport and scissors - or, in the case of an imageless framebuffer, the actual images used for the attachments - are referenced with a VkPipeline handle, corresponding to a memory image that contains most data of the configurable state and which is likely cached in order to ensure that frequently used pipeline state data is readily accessible on the GPU.

It is worthwhile to note that when creating a VkPipeline, the SPIR-V is compiled into machine code specific to the target GPU’s Instruction Set Architecture (ISA). This is the main reason why creating a (new) pipeline is very expensive.

Hence, a VkPipeline is an abstraction existing of the static configurable state of how the GPU must process vertex input data and attachments, transforming and shading the 3D geometry into a 2D image and blend that into a target image, in order to render the next frame.

The stages in the Graphics Pipeline include:

  1. Input Assembly: This stage assembles the input vertex data from vertex buffers, defining the topology (e.g., triangles, lines, or points) of the primitives to be processed.
  2. Vertex Shader: A programmable stage that processes each vertex independently, typically responsible for transforming vertex positions, calculating texture coordinates, and generating vertex attributes like normals or tangents.
  3. Tessellation Control and Evaluation Shaders: Optional programmable stages that subdivide and manipulate geometry to produce more detailed and smoother surfaces.
  4. Geometry Shader: Another optional programmable stage that processes entire primitives (e.g., triangles or lines), allowing for geometry modification, generation, or culling.
  5. Rasterization: A fixed-function stage that converts primitives into fragments, which are potential pixels on the screen, performing operations like clipping, culling, and generating fragment attributes.
  6. Fragment Shader: A programmable stage that processes each fragment independently, computing the final color and other outputs, such as depth or stencil values, for each fragment.
  7. Depth and Stencil Testing: Fixed-function stages that perform depth and stencil tests to determine if a fragment should be discarded or written to the framebuffer.
  8. Color Blending: A fixed-function stage that combines the output of the fragment shader with the existing contents of the framebuffer, defining how colors are blended, added, or otherwise combined.

VkDescriptorSet

The necessary information for the shader stages to access resources like buffers, images and samplers are encapsulated by a data structure called a “descriptor” (VkDescriptor ).

A VkDescriptorSet serves as a container for descriptors, and enables efficient resource management and organization for shader stages within a graphics or compute pipeline. The primary role of a descriptor set is to group these descriptors together in a well-defined layout, facilitating ease of use and optimal performance when binding shader resources to pipeline stages (see below).

VkCommandBuffer

Since VkPipelines consist of a collection of static data detailing input processing, and their creation is resource-intensive, it’s common to create numerous VkPipeline objects for use as building blocks in describing the rendering of a complete frame. After all, each minor variation in configuration necessitates the creation of a new VkPipeline.

Those building blocks are combined with “dynamic” data every frame when recording a command buffer. There are several dynamic data components used, besides VkPipeline, VkFramebuffer and VkRenderPass objects.

Here’s a list of dynamic data, organized by command buffer function, that is explicitly used when recording a command buffer:

  1. Render pass and framebuffer:
  2. Binding Vertex and Index Buffers:
  3. Specifying pipeline dynamic state:
  4. Binding pipeline, descriptor sets, and push constants:
  5. Drawing commands (indexed, non-indexed, indirect, etc.):
    • vkCmdDraw: Records non-indexed drawing commands.
      • Vertex count, instance count, first vertex, first instance.
    • vkCmdDrawIndexed: Records indexed drawing commands.
      • Index count, instance count, first index, vertex offset, first instance.
    • vkCmdDrawIndirect and vkCmdDrawIndexedIndirect: Records indirect drawing commands.
      • VkBuffer (for indirect drawing commands).