Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 83 additions & 73 deletions en/01_Overview.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ graphics architectures. It reduces the driver overhead by allowing programmers t
purpose processing capabilities of modern graphics cards by unifying the
graphics and compute functionality into a single API.

=== Coding conventions

All the Vulkan functions, enumerations and structs are defined in the
`vulkan.h` header, which is included in the https://lunarg.com/vulkan-sdk/[Vulkan SDK]
developed by LunarG. We'll look into installing this SDK in the next chapter.
In this tutorial, we’ll be using the C++ Vulkan API provided by the vulkan.hpp header,
which comes with the official Vulkan SDK. This header offers a type-safe, RAII-friendly,
and slightly more ergonomic interface over the raw C Vulkan API,
while still maintaining a very close, low-level mapping
to the underlying Vulkan functions and structures.

== What it takes to draw a triangle

We'll now look at an overview of all the steps it takes to render a triangle
Expand All @@ -52,26 +63,23 @@ This is just to give you a big picture to relate all the individual components t

=== Step 1 - Instance and physical device selection

A Vulkan application starts by setting up the Vulkan API through a `VkInstance`.
A Vulkan application starts by setting up the Vulkan API through a `vk::Instance`.
An instance is created by describing your application and any API extensions
you will be using. After creating the instance, you can query for Vulkan
supported hardware and select one or more ``VkPhysicalDevice``s to use for
supported hardware and select one or more ``vk::PhysicalDevice``s to use for
operations. You can query for properties like VRAM size and device
capabilities to select desired devices, for example, to prefer using
dedicated graphics cards.

For vk::raii we need to use a `vk::raii::Context`, which manages functions that
are not bound to either the `VkInstance` or a `VkPhysicalDevice`

=== Step 2 - Logical device and queue families

After selecting the right hardware device to use, you need to create a
VkDevice (logical device), where you describe more specifically which
VkPhysicalDeviceFeatures you will be using, like multi viewport rendering
vk::Device (logical device), where you describe more specifically which
physical device features you will be using, like multi viewport rendering
and 64-bit floats.
You also need to specify which queue families you would like to use.
Most operations performed with Vulkan, like draw commands and memory
operations, are asynchronously executed by submitting them to a VkQueue.
operations, are asynchronously executed by submitting them to a vk::Queue.
Queues are allocated from queue families, where each queue family supports a
specific set of operations in its queues.
For example, there could be separate queue families for graphics, compute
Expand All @@ -92,7 +100,7 @@ We will be using GLFW in this tutorial, but more about that in the next
chapter.

We need two more parts to actually render to a window: a window surface
(VkSurfaceKHR) and a swap chain (VkSwapchainKHR).
(vk::SurfaceKHR) and a swap chain (vk::SwapchainKHR).
Note the `KHR` postfix, which means that these objects are part of a Vulkan
extension. The Vulkan API itself is completely platform-agnostic, which is
why we need to use the standardized WSI (Window System Interface) extension
Expand Down Expand Up @@ -122,34 +130,45 @@ Some platforms allow you to render directly to a display without interacting
These allow you to create a surface that represents the entire screen and
could be used to implement your own window manager, for example.

=== Step 4 - Image views and framebuffers
=== Step 4 - Image views and Dynamic Rendering
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would move all the dynamic rendering stuff to Step 5.


To draw to an image acquired from the swap chain, we would typically wrap
it into a vk::ImageView and vk::Framebuffer. An image view references a specific
part of an image to be used, and a framebuffer references image views that are
to be used for color, depth, and stencil targets.
Because there could be many different images in the swap chain,
we would preemptively create an image view and framebuffer for each
of them and select the right one at draw time.

=== Step 5 - Dynamic Rendering Overview

To draw to an image acquired from the swap chain, we have to wrap it into a
VkImageView and VkFramebuffer.
An image view references a specific part of an image to be used, and a
framebuffer references image views that are to be used for color, depth and
stencil targets.
Because there could be many different images in the swap chain, we'll
preemptively create an image view and framebuffer for each of them and
select the right one at draw time.
In earlier versions of Vulkan, a render pass defined how rendering operations
should occur with framebuffers, specifying the types of images used (e.g., color, depth)
and how their contents should be treated (e.g., cleared, loaded, or stored).
A `vk::RenderPass` would define subpasses and attachment usage, and a `vk::Framebuffer`
would bind specific image views to these attachments.

=== Step 5 - Render passes
However, with dynamic rendering (introduced in Vulkan 1.3),
you no longer need to create a `vk::Framebuffer` at all.
Dynamic rendering eliminates the need for predefined render passes and framebuffers,
allowing you to specify rendering attachments directly during command recording.
This makes the API much simpler, as we can define the rendering targets on the
fly without worrying about the overhead of managing framebuffers.

Render passes in Vulkan describe the type of images that are used during
rendering operations, how they will be used, and how their contents should
be treated.
In our initial triangle rendering application, we'll tell Vulkan that we
will use a single image as a color target and that we want it to be cleared to
a solid color right before the drawing operation.
Whereas a render pass only describes the type of images, a VkFramebuffer
actually binds specific images to these slots.
With dynamic rendering, you no longer need to predefine `vk::RenderPass` or `vk::Framebuffer`.
Instead, you specify the rendering attachments at the start of command recording, using `vk::beginRendering`
and structs like `vk::RenderingInfo` to provide all necessary attachment information dynamically.

In our initial triangle rendering application,
we'll use dynamic rendering to specify
a single image as a color target and instruct Vulkan to clear it to a solid color right before drawing.

=== Step 6 - Graphics pipeline

The graphics pipeline in Vulkan is set up by creating a VkPipeline object.
It describes the configurable state of the graphics card, like the viewport
size and depth buffer operation and the programmable state using VkShaderModule objects.
The VkShaderModule objects are created from shader byte code.
size and depth buffer operation and the programmable state using vk::ShaderModule objects.
The vk::ShaderModule objects are created from shader byte code.
The driver also needs to know which render targets will be used in the
pipeline, which we specify by referencing the render pass.

Expand All @@ -159,7 +178,7 @@ One of the most distinctive features of Vulkan compared to existing APIs, is
That means that if you want to switch to a different shader or slightly
change your vertex layout, then you need to entirely recreate the graphics
pipeline.
That means that you will have to create many VkPipeline objects in advance
That means that you will have to create many vk::Pipeline objects in advance
for all the different combinations you need for your rendering operations.
Only some basic configuration, like viewport size and clear color, can be
changed dynamically.
Expand All @@ -176,41 +195,47 @@ are made very explicit.

As mentioned earlier, many of the operations in Vulkan that we want to
execute, like drawing operations, need to be submitted to a queue.
These operations first need to be recorded into a VkCommandBuffer before
These operations first need to be recorded into a vk::CommandBuffer before
they can be submitted.
These command buffers are allocated from a `VkCommandPool` that is
These command buffers are allocated from a `vk::CommandPool` that is
associated with a specific queue family.
To draw a simple triangle, we need to record a command buffer with the
Traditionally, to draw a simple triangle, we need to record a command buffer with the
following operations:

* Begin the render pass
* Bind the graphics pipeline
* Draw three vertices
* End the render pass

Because the image in the framebuffer depends on which specific image the
swap chain will give us, we need to record a command buffer for each
possible image and select the right one at draw time.
The alternative would be to record the command buffer again every frame,
which is not as efficient.
However, with dynamic rendering, things change.
Instead of "beginning" and "ending" a render pass,
you directly define the rendering attachments when you start
rendering with vk::BeginRendering.

This simplifies the process by allowing you to specify the necessary attachments on the fly,
making it more adaptable to scenarios where the swap chain images are dynamically selected.
Therefore, you don't need to record a command buffer for each image in the swap
chain or repeatedly record the same command buffer every frame.
The operations become more streamlined and efficient,
allowing Vulkan to be more flexible in handling rendering scenarios.

=== Step 8 - Main loop

Now that the drawing commands have been wrapped into a command buffer, the
main loop is quite straightforward.
We first acquire an image from the swap chain with vkAcquireNextImageKHR.
We first acquire an image from the swap chain with device.acquireNextImageKHR.
We can then select the appropriate command buffer for that image and execute
it with vkQueueSubmit.
it with graphicsQueue.submit().
Finally, we return the image to the swap chain for presentation to the
screen with vkQueuePresentKHR.
screen with presentQueue.presentKHR(presentInfo).

Operations that are submitted to queues are executed asynchronously.
Therefore, we have to use synchronization objects like semaphores to ensure a
correct order of execution.
Execution of the draw command buffer must be set up to wait on image
acquisition to finish; otherwise it may occur that we start rendering to an
image that is still being read for presentation on the screen.
The vkQueuePresentKHR call in turn needs to wait for rendering to be
The presentQueue.presentKHR(presentInfoKHR) call in turn needs to wait for rendering to be
finished, for which we'll use a second semaphore that is signaled after
rendering completes.

Expand All @@ -229,13 +254,12 @@ command buffers first.

So in short, to draw the first triangle, we need to:

* Create a VkInstance
* Select a supported graphics card (VkPhysicalDevice)
* Create a VkDevice and VkQueue for drawing and presentation
* Create an Instance
* Select a supported graphics card (PhysicalDevice)
* Create a Device and Queue for drawing and presentation
* Create a window, window surface and swap chain
* Wrap the swap chain images into VkImageView
* Create a render pass that specifies the render targets and usage
* Create framebuffers for the render pass
* Set up dynamic rendering
* Set up the graphics pipeline
* Allocate and record a command buffer with the draw commands for every
possible swap chain image
Expand All @@ -252,28 +276,23 @@ If you're confused about the relation of a single step compared to the whole
This chapter will conclude with a short overview of how the Vulkan API is
structured at a lower level.

=== Coding conventions

All the Vulkan functions, enumerations and structs are defined in the
`vulkan.h` header, which is included in the https://lunarg.com/vulkan-sdk/[Vulkan SDK]
developed by LunarG. We'll look into installing this SDK in the next chapter.

Functions have a lower case `vk` prefix, types like enumerations and structs
have a `Vk` prefix and enumeration values have a `VK_` prefix.
The API heavily uses structs to provide parameters to functions.
For example, object creation generally follows this pattern:

[,c++]
----
VkXXXCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
vk::XXXCreateInfo createInfo{};
createInfo.sType = vk::StructureType::eXXXCreateInfo;
createInfo.pNext = nullptr;
createInfo.foo = ...;
createInfo.bar = ...;

VkXXX object;
if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) {
std::cerr << "failed to create object" << std::endl;
vk::XXX object;


try {
object = device.createXXX(createInfo);
} catch (vk::SystemError& err) {
std::cerr << "Failed to create object: " << err.what() << std::endl;
return false;
}
----
Expand All @@ -286,23 +305,14 @@ Functions that create or destroy an object will have a VkAllocationCallbacks
parameter that allows you to use a custom allocator for driver memory,
which will also be left `nullptr` in this tutorial.

Almost all functions return a VkResult that is either `VK_SUCCESS` or an
error code.
Almost all functions return a vk::Result that is either `vk::result::eSuccess`
or an error code.
The specification describes which error codes each function can return and
what they mean.

To help illustrate the utility of using the RAII C++ Vulkan abstraction; this
is the same code written with our modern API:

[,c++]
----
auto createInfo = vk::xxx();
auto object = vk::raii::XXX(context, createInfo);
----

Failure of such calls is reported by C++ exceptions. The exception will
respond with more information about the error including the aforementioned
vkResult, this enables us to check multiple commands from one call and keep
vk::Result, this enables us to check multiple commands from one call and keep
the command syntax clean.

=== Validation layers
Expand Down
Loading