0% found this document useful (0 votes)
106 views50 pages

Vulkan Tutorial En-1-50

ini adalah bagian pertama dari vulkan turorial

Uploaded by

rendy anggara
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
106 views50 pages

Vulkan Tutorial En-1-50

ini adalah bagian pertama dari vulkan turorial

Uploaded by

rendy anggara
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 50

Vulkan Tutorial

Alexander Overvoorde

April 2023
Contents

Introduction 6
About . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
E-book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Tutorial structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

Overview 9
Origin of Vulkan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
What it takes to draw a triangle . . . . . . . . . . . . . . . . . . . . . 10
Step 1 - Instance and physical device selection . . . . . . . . . . . 10
Step 2 - Logical device and queue families . . . . . . . . . . . . . 10
Step 3 - Window surface and swap chain . . . . . . . . . . . . . . 10
Step 4 - Image views and framebuffers . . . . . . . . . . . . . . . 11
Step 5 - Render passes . . . . . . . . . . . . . . . . . . . . . . . . 11
Step 6 - Graphics pipeline . . . . . . . . . . . . . . . . . . . . . . 12
Step 7 - Command pools and command buffers . . . . . . . . . . 12
Step 8 - Main loop . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
API concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Coding conventions . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Validation layers . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

Development environment 16
Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Vulkan SDK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
GLFW . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
GLM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Setting up Visual Studio . . . . . . . . . . . . . . . . . . . . . . . 19
Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Vulkan Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
GLFW . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
GLM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Shader Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Setting up a makefile project . . . . . . . . . . . . . . . . . . . . 28

1
MacOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Vulkan SDK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
GLFW . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
GLM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Setting up Xcode . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

Drawing a triangle 39
Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Base code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Instance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Validation layers . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Physical devices and queue families . . . . . . . . . . . . . . . . . 58
Logical device and queues . . . . . . . . . . . . . . . . . . . . . . 65
Presentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Window surface . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Swap chain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Image views . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Graphics pipeline basics . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Shader modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Fixed functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Render passes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Drawing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
Framebuffers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
Command buffers . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
Rendering and presentation . . . . . . . . . . . . . . . . . . . . . 124
Frames in flight . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Swap chain recreation . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Recreating the swap chain . . . . . . . . . . . . . . . . . . . . . . 139
Suboptimal or out-of-date swap chain . . . . . . . . . . . . . . . 142
Fixing a deadlock . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
Handling resizes explicitly . . . . . . . . . . . . . . . . . . . . . . 143
Handling minimization . . . . . . . . . . . . . . . . . . . . . . . . 145

Vertex buffers 146


Vertex input description . . . . . . . . . . . . . . . . . . . . . . . . . . 146
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
Vertex shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
Vertex data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Binding descriptions . . . . . . . . . . . . . . . . . . . . . . . . . 147
Attribute descriptions . . . . . . . . . . . . . . . . . . . . . . . . 148
Pipeline vertex input . . . . . . . . . . . . . . . . . . . . . . . . . 150
Vertex buffer creation . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150

2
Buffer creation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
Memory requirements . . . . . . . . . . . . . . . . . . . . . . . . 152
Memory allocation . . . . . . . . . . . . . . . . . . . . . . . . . . 154
Filling the vertex buffer . . . . . . . . . . . . . . . . . . . . . . . 155
Binding the vertex buffer . . . . . . . . . . . . . . . . . . . . . . 156
Staging buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Transfer queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Abstracting buffer creation . . . . . . . . . . . . . . . . . . . . . 159
Using a staging buffer . . . . . . . . . . . . . . . . . . . . . . . . 160
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
Index buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Index buffer creation . . . . . . . . . . . . . . . . . . . . . . . . . 165
Using an index buffer . . . . . . . . . . . . . . . . . . . . . . . . . 167

Uniform buffers 169


Descriptor layout and buffer . . . . . . . . . . . . . . . . . . . . . . . . 169
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
Vertex shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
Descriptor set layout . . . . . . . . . . . . . . . . . . . . . . . . . 171
Uniform buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Updating uniform data . . . . . . . . . . . . . . . . . . . . . . . . 175
Descriptor pool and sets . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Descriptor pool . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Descriptor set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Using descriptor sets . . . . . . . . . . . . . . . . . . . . . . . . . 181
Alignment requirements . . . . . . . . . . . . . . . . . . . . . . . 182
Multiple descriptor sets . . . . . . . . . . . . . . . . . . . . . . . 185

Texture mapping 186


Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
Image library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
Loading an image . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Staging buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Texture Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Layout transitions . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Copying buffer to image . . . . . . . . . . . . . . . . . . . . . . . 198
Preparing the texture image . . . . . . . . . . . . . . . . . . . . . 199
Transition barrier masks . . . . . . . . . . . . . . . . . . . . . . . 200
Cleanup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Image view and sampler . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Texture image view . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Samplers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205

3
Anisotropy device feature . . . . . . . . . . . . . . . . . . . . . . 209
Combined image sampler . . . . . . . . . . . . . . . . . . . . . . . . . 210
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Updating the descriptors . . . . . . . . . . . . . . . . . . . . . . . 210
Texture coordinates . . . . . . . . . . . . . . . . . . . . . . . . . 213
Shaders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214

Depth buffering 219


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
3D geometry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
Depth image and view . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
Explicitly transitioning the depth image . . . . . . . . . . . . . . 226
Render pass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Framebuffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
Clear values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
Depth and stencil state . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Handling window resize . . . . . . . . . . . . . . . . . . . . . . . . . . 231

Loading models 233


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Sample mesh . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
Loading vertices and indices . . . . . . . . . . . . . . . . . . . . . . . . 235
Vertex deduplication . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

Generating Mipmaps 242


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
Image creation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
Generating Mipmaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
Linear filtering support . . . . . . . . . . . . . . . . . . . . . . . . . . 249
Sampler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250

Multisampling 254
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Getting available sample count . . . . . . . . . . . . . . . . . . . . . . 256
Setting up a render target . . . . . . . . . . . . . . . . . . . . . . . . . 257
Adding new attachments . . . . . . . . . . . . . . . . . . . . . . . . . . 259
Quality improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263

Compute Shader 265


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
Advantages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
The Vulkan pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
An example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Data manipulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268

4
Shader storage buffer objects (SSBO) . . . . . . . . . . . . . . . . 268
Storage images . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Compute queue families . . . . . . . . . . . . . . . . . . . . . . . . . . 270
The compute shader stage . . . . . . . . . . . . . . . . . . . . . . . . . 271
Loading compute shaders . . . . . . . . . . . . . . . . . . . . . . . . . 271
Preparing the shader storage buffers . . . . . . . . . . . . . . . . . . . 272
Descriptors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
Compute pipelines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Compute space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
Compute shaders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
Running compute commands . . . . . . . . . . . . . . . . . . . . . . . 280
Dispatch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Submitting work . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
Synchronizing graphics and compute . . . . . . . . . . . . . . . . 281
Drawing the particle system . . . . . . . . . . . . . . . . . . . . . . . . 284
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285

FAQ 286
I get an access violation error in the core validation layer . . . . . . . 286
I don’t see any messages from the validation layers / Validation layers
are not available . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
vkCreateSwapchainKHR triggers an error in SteamOverlayVulkan-
Layer64.dll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
vkCreateInstance fails with VK_ERROR_INCOMPATIBLE_DRIVER287

5
Introduction

About
This tutorial will teach you the basics of using the Vulkan graphics and compute
API. Vulkan is a new API by the Khronos group (known for OpenGL) that
provides a much better abstraction of modern graphics cards. This new interface
allows you to better describe what your application intends to do, which can lead
to better performance and less surprising driver behavior compared to existing
APIs like OpenGL and Direct3D. The ideas behind Vulkan are similar to those
of Direct3D 12 and Metal, but Vulkan has the advantage of being fully cross-
platform and allows you to develop for Windows, Linux and Android at the
same time.
However, the price you pay for these benefits is that you have to work with a
significantly more verbose API. Every detail related to the graphics API needs
to be set up from scratch by your application, including initial frame buffer
creation and memory management for objects like buffers and texture images.
The graphics driver will do a lot less hand holding, which means that you will
have to do more work in your application to ensure correct behavior.
The takeaway message here is that Vulkan is not for everyone. It is targeted at
programmers who are enthusiastic about high performance computer graphics,
and are willing to put some work in. If you are more interested in game devel-
opment, rather than computer graphics, then you may wish to stick to OpenGL
or Direct3D, which will not be deprecated in favor of Vulkan anytime soon. An-
other alternative is to use an engine like Unreal Engine or Unity, which will be
able to use Vulkan while exposing a much higher level API to you.
With that out of the way, let’s cover some prerequisites for following this tutorial:
• A graphics card and driver compatible with Vulkan (NVIDIA, AMD, Intel,
Apple Silicon (Or the Apple M1))
• Experience with C++ (familiarity with RAII, initializer lists)
• A compiler with decent support of C++17 features (Visual Studio 2017+,
GCC 7+, Or Clang 5+)
• Some existing experience with 3D computer graphics

6
This tutorial will not assume knowledge of OpenGL or Direct3D concepts, but
it does require you to know the basics of 3D computer graphics. It will not
explain the math behind perspective projection, for example. See this online
book for a great introduction of computer graphics concepts. Some other great
computer graphics resources are:
• Ray tracing in one weekend
• Physically Based Rendering book
• Vulkan being used in a real engine in the open-source Quake and DOOM
3
You can use C instead of C++ if you want, but you will have to use a different
linear algebra library and you will be on your own in terms of code structuring.
We will use C++ features like classes and RAII to organize logic and resource
lifetimes. There is also an alternative version of this tutorial available for Rust
developers.
To make it easier to follow along for developers using other programming lan-
guages, and to get some experience with the base API we’ll be using the original
C API to work with Vulkan. If you are using C++, however, you may prefer
using the newer Vulkan-Hpp bindings that abstract some of the dirty work and
help prevent certain classes of errors.

E-book
If you prefer to read this tutorial as an e-book, then you can download an EPUB
or PDF version here:
• EPUB
• PDF

Tutorial structure
We’ll start with an overview of how Vulkan works and the work we’ll have
to do to get the first triangle on the screen. The purpose of all the smaller
steps will make more sense after you’ve understood their basic role in the whole
picture. Next, we’ll set up the development environment with the Vulkan SDK,
the GLM library for linear algebra operations and GLFW for window creation.
The tutorial will cover how to set these up on Windows with Visual Studio, and
on Ubuntu Linux with GCC.
After that we’ll implement all of the basic components of a Vulkan program that
are necessary to render your first triangle. Each chapter will follow roughly the
following structure:
• Introduce a new concept and its purpose
• Use all of the relevant API calls to integrate it into your program

7
• Abstract parts of it into helper functions
Although each chapter is written as a follow-up on the previous one, it is also
possible to read the chapters as standalone articles introducing a certain Vulkan
feature. That means that the site is also useful as a reference. All of the Vulkan
functions and types are linked to the specification, so you can click them to
learn more. Vulkan is a very new API, so there may be some shortcomings in
the specification itself. You are encouraged to submit feedback to this Khronos
repository.
As mentioned before, the Vulkan API has a rather verbose API with many
parameters to give you maximum control over the graphics hardware. This
causes basic operations like creating a texture to take a lot of steps that have to
be repeated every time. Therefore we’ll be creating our own collection of helper
functions throughout the tutorial.
Every chapter will also conclude with a link to the full code listing up to that
point. You can refer to it if you have any doubts about the structure of the code,
or if you’re dealing with a bug and want to compare. All of the code files have
been tested on graphics cards from multiple vendors to verify correctness. Each
chapter also has a comment section at the end where you can ask any questions
that are relevant to the specific subject matter. Please specify your platform,
driver version, source code, expected behavior and actual behavior to help us
help you.
This tutorial is intended to be a community effort. Vulkan is still a very new
API and best practices have not really been established yet. If you have any
type of feedback on the tutorial and site itself, then please don’t hesitate to
submit an issue or pull request to the GitHub repository. You can watch the
repository to be notified of updates to the tutorial.
After you’ve gone through the ritual of drawing your very first Vulkan powered
triangle onscreen, we’ll start expanding the program to include linear transfor-
mations, textures and 3D models.
If you’ve played with graphics APIs before, then you’ll know that there can be
a lot of steps until the first geometry shows up on the screen. There are many
of these initial steps in Vulkan, but you’ll see that each of the individual steps
is easy to understand and does not feel redundant. It’s also important to keep
in mind that once you have that boring looking triangle, drawing fully textured
3D models does not take that much extra work, and each step beyond that point
is much more rewarding.
If you encounter any problems while following the tutorial, then first check the
FAQ to see if your problem and its solution is already listed there. If you are
still stuck after that, then feel free to ask for help in the comment section of the
closest related chapter.
Ready to dive into the future of high performance graphics APIs? Let’s go!

8
Overview

This chapter will start off with an introduction of Vulkan and the problems it
addresses. After that we’re going to look at the ingredients that are required for
the first triangle. This will give you a big picture to place each of the subsequent
chapters in. We will conclude by covering the structure of the Vulkan API and
the general usage patterns.

Origin of Vulkan
Just like the previous graphics APIs, Vulkan is designed as a cross-platform
abstraction over GPUs. The problem with most of these APIs is that the era in
which they were designed featured graphics hardware that was mostly limited
to configurable fixed functionality. Programmers had to provide the vertex data
in a standard format and were at the mercy of the GPU manufacturers with
regards to lighting and shading options.
As graphics card architectures matured, they started offering more and more
programmable functionality. All this new functionality had to be integrated
with the existing APIs somehow. This resulted in less than ideal abstractions
and a lot of guesswork on the graphics driver side to map the programmer’s in-
tent to the modern graphics architectures. That’s why there are so many driver
updates for improving the performance in games, sometimes by significant mar-
gins. Because of the complexity of these drivers, application developers also need
to deal with inconsistencies between vendors, like the syntax that is accepted
for shaders. Aside from these new features, the past decade also saw an influx
of mobile devices with powerful graphics hardware. These mobile GPUs have
different architectures based on their energy and space requirements. One such
example is tiled rendering, which would benefit from improved performance by
offering the programmer more control over this functionality. Another limita-
tion originating from the age of these APIs is limited multi-threading support,
which can result in a bottleneck on the CPU side.
Vulkan solves these problems by being designed from scratch for modern graph-
ics architectures. It reduces driver overhead by allowing programmers to clearly
specify their intent using a more verbose API, and allows multiple threads to

9
create and submit commands in parallel. It reduces inconsistencies in shader
compilation by switching to a standardized byte code format with a single com-
piler. Lastly, it acknowledges the general purpose processing capabilities of
modern graphics cards by unifying the graphics and compute functionality into
a single API.

What it takes to draw a triangle


We’ll now look at an overview of all the steps it takes to render a triangle in
a well-behaved Vulkan program. All of the concepts introduced here will be
elaborated on in the next chapters. This is just to give you a big picture to
relate all of the individual components to.

Step 1 - Instance and physical device selection


A Vulkan application starts by setting up the Vulkan API through a VkInstance.
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 sup-
ported hardware and select one or more VkPhysicalDevices to use for opera-
tions. You can query for properties like VRAM size and device capabilities to
select desired devices, for example to prefer using dedicated graphics cards.

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 VkPhysicalDevice-
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 opera-
tions performed with Vulkan, like draw commands and memory operations, are
asynchronously executed by submitting them to a VkQueue. Queues are allo-
cated 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 and memory transfer operations. The availability of queue
families could also be used as a distinguishing factor in physical device selection.
It is possible for a device with Vulkan support to not offer any graphics func-
tionality, however all graphics cards with Vulkan support today will generally
support all queue operations that we’re interested in.

Step 3 - Window surface and swap chain


Unless you’re only interested in offscreen rendering, you will need to create a
window to present rendered images to. Windows can be created with the native
platform APIs or libraries like GLFW and SDL. We will be using GLFW in this
tutorial, but more about that in the next chapter.

10
We need two more components to actually render to a window: a window surface
(VkSurfaceKHR) and a swap chain (VkSwapchainKHR). 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 to interact with the
window manager. The surface is a cross-platform abstraction over windows to
render to and is generally instantiated by providing a reference to the native
window handle, for example HWND on Windows. Luckily, the GLFW library has
a built-in function to deal with the platform specific details of this.
The swap chain is a collection of render targets. Its basic purpose is to ensure
that the image that we’re currently rendering to is different from the one that
is currently on the screen. This is important to make sure that only complete
images are shown. Every time we want to draw a frame we have to ask the
swap chain to provide us with an image to render to. When we’ve finished
drawing a frame, the image is returned to the swap chain for it to be presented
to the screen at some point. The number of render targets and conditions for
presenting finished images to the screen depends on the present mode. Common
present modes are double buffering (vsync) and triple buffering. We’ll look into
these in the swap chain creation chapter.
Some platforms allow you to render directly to a display without in-
teracting with any window manager through the VK_KHR_display and
VK_KHR_display_swapchain extensions. 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


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.

Step 5 - Render passes


Render passes in Vulkan describe the type of images that are used during render-
ing 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 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.

11
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. The
driver also needs to know which render targets will be used in the pipeline,
which we specify by referencing the render pass.
One of the most distinctive features of Vulkan compared to existing APIs, is
that almost all configuration of the graphics pipeline needs to be set in advance.
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
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. All of the state also needs to be described explicitly, there is no
default color blend state, for example.
The good news is that because you’re doing the equivalent of ahead-of-time
compilation versus just-in-time compilation, there are more optimization oppor-
tunities for the driver and runtime performance is more predictable, because
large state changes like switching to a different graphics pipeline are made very
explicit.

Step 7 - Command pools and command buffers


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 they can be submitted.
These command buffers are allocated from a VkCommandPool that is associated
with a specific queue family. 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 3 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.

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

12
swap chain with vkAcquireNextImageKHR. We can then select the appropri-
ate command buffer for that image and execute it with vkQueueSubmit. Fi-
nally, we return the image to the swap chain for presentation to the screen with
vkQueuePresentKHR.
Operations that are submitted to queues are executed asynchronously. There-
fore 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 ren-
dering 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 finished, for
which we’ll use a second semaphore that is signaled after rendering completes.

Summary
This whirlwind tour should give you a basic understanding of the work ahead
for drawing the first triangle. A real-world program contains more steps, like
allocating vertex buffers, creating uniform buffers and uploading texture im-
ages that will be covered in subsequent chapters, but we’ll start simple because
Vulkan has enough of a steep learning curve as it is. Note that we’ll cheat a
bit by initially embedding the vertex coordinates in the vertex shader instead
of using a vertex buffer. That’s because managing vertex buffers requires some
familiarity with 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 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 the graphics pipeline
• Allocate and record a command buffer with the draw commands for every
possible swap chain image
• Draw frames by acquiring images, submitting the right draw command
buffer and returning the images back to the swap chain
It’s a lot of steps, but the purpose of each individual step will be made very
simple and clear in the upcoming chapters. If you’re confused about the relation
of a single step compared to the whole program, you should refer back to this
chapter.

13
API concepts
This chapter will conclude with a short overview of how the Vulkan API is
structured at a lower level.

Coding conventions
All of the Vulkan functions, enumerations and structs are defined in the
vulkan.h header, which is included in the 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 gener-
ally follows this pattern:
1 VkXXXCreateInfo createInfo{};
2 createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
3 createInfo.pNext = nullptr;
4 createInfo.foo = ...;
5 createInfo.bar = ...;
6
7 VkXXX object;
8 if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) {
9 std::cerr << "failed to create object" << std::endl;
10 return false;
11 }

Many structures in Vulkan require you to explicitly specify the type of structure
in the sType member. The pNext member can point to an extension structure
and will always be nullptr in this tutorial. 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. The specification describes which error codes each function can return
and what they mean.

Validation layers
As mentioned earlier, Vulkan is designed for high performance and low driver
overhead. Therefore it will include very limited error checking and debugging
capabilities by default. The driver will often crash instead of returning an error
code if you do something wrong, or worse, it will appear to work on your graphics
card and completely fail on others.

14
Vulkan allows you to enable extensive checks through a feature known as vali-
dation layers. Validation layers are pieces of code that can be inserted between
the API and the graphics driver to do things like running extra checks on func-
tion parameters and tracking memory management problems. The nice thing
is that you can enable them during development and then completely disable
them when releasing your application for zero overhead. Anyone can write their
own validation layers, but the Vulkan SDK by LunarG provides a standard set
of validation layers that we’ll be using in this tutorial. You also need to register
a callback function to receive debug messages from the layers.
Because Vulkan is so explicit about every operation and the validation layers
are so extensive, it can actually be a lot easier to find out why your screen is
black compared to OpenGL and Direct3D!
There’s only one more step before we’ll start writing code and that’s setting up
the development environment.

15
Development environment

In this chapter we’ll set up your environment for developing Vulkan applications
and install some useful libraries. All of the tools we’ll use, with the exception of
the compiler, are compatible with Windows, Linux and MacOS, but the steps
for installing them differ a bit, which is why they’re described separately here.

Windows
If you’re developing for Windows, then I will assume that you are using Visual
Studio to compile your code. For complete C++17 support, you need to use
either Visual Studio 2017 or 2019. The steps outlined below were written for
VS 2017.

Vulkan SDK
The most important component you’ll need for developing Vulkan applications
is the SDK. It includes the headers, standard validation layers, debugging tools
and a loader for the Vulkan functions. The loader looks up the functions in the
driver at runtime, similarly to GLEW for OpenGL - if you’re familiar with that.
The SDK can be downloaded from the LunarG website using the buttons at the
bottom of the page. You don’t have to create an account, but it will give you
access to some additional documentation that may be useful to you.

Proceed through the installation and pay attention to the install location of
the SDK. The first thing we’ll do is verify that your graphics card and driver
properly support Vulkan. Go to the directory where you installed the SDK,

16
open the Bin directory and run the vkcube.exe demo. You should see the
following:

If you receive an error message then ensure that your drivers are up-to-date,
include the Vulkan runtime and that your graphics card is supported. See the
introduction chapter for links to drivers from the major vendors.
There is another program in this directory that will be useful for development.
The glslangValidator.exe and glslc.exe programs will be used to compile
shaders from the human-readable GLSL to bytecode. We’ll cover this in depth
in the shader modules chapter. The Bin directory also contains the binaries of
the Vulkan loader and the validation layers, while the Lib directory contains
the libraries.
Lastly, there’s the Include directory that contains the Vulkan headers. Feel
free to explore the other files, but we won’t need them for this tutorial.

17
GLFW
As mentioned before, Vulkan by itself is a platform agnostic API and does not
include tools for creating a window to display the rendered results. To benefit
from the cross-platform advantages of Vulkan and to avoid the horrors of Win32,
we’ll use the GLFW library to create a window, which supports Windows, Linux
and MacOS. There are other libraries available for this purpose, like SDL, but
the advantage of GLFW is that it also abstracts away some of the other platform-
specific things in Vulkan besides just window creation.
You can find the latest release of GLFW on the official website. In this tutorial
we’ll be using the 64-bit binaries, but you can of course also choose to build in
32 bit mode. In that case make sure to link with the Vulkan SDK binaries in
the Lib32 directory instead of Lib. After downloading it, extract the archive to
a convenient location. I’ve chosen to create a Libraries directory in the Visual
Studio directory under documents.

GLM
Unlike DirectX 12, Vulkan does not include a library for linear algebra opera-
tions, so we’ll have to download one. GLM is a nice library that is designed for
use with graphics APIs and is also commonly used with OpenGL.
GLM is a header-only library, so just download the latest version and store it
in a convenient location. You should have a directory structure similar to the
following now:

18
Setting up Visual Studio
Now that you’ve installed all of the dependencies we can set up a basic Vi-
sual Studio project for Vulkan and write a little bit of code to make sure that
everything works.
Start Visual Studio and create a new Windows Desktop Wizard project by en-
tering a name and pressing OK.

19
Make sure that Console Application (.exe) is selected as application type
so that we have a place to print debug messages to, and check Empty Project
to prevent Visual Studio from adding boilerplate code.

20
Press OK to create the project and add a C++ source file. You should already
know how to do that, but the steps are included here for completeness.

Now add the following code to the file. Don’t worry about trying to understand
it right now; we’re just making sure that you can compile and run Vulkan
applications. We’ll start from scratch in the next chapter.
1 #define GLFW_INCLUDE_VULKAN
2 #include <GLFW/glfw3.h>
3
4 #define GLM_FORCE_RADIANS
5 #define GLM_FORCE_DEPTH_ZERO_TO_ONE
6 #include <glm/vec4.hpp>
7 #include <glm/mat4x4.hpp>
8
9 #include <iostream>
10

21
11 int main() {
12 glfwInit();
13
14 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
15 GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window",
nullptr, nullptr);
16
17 uint32_t extensionCount = 0;
18 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
nullptr);
19
20 std::cout << extensionCount << " extensions supported\n";
21
22 glm::mat4 matrix;
23 glm::vec4 vec;
24 auto test = matrix * vec;
25
26 while(!glfwWindowShouldClose(window)) {
27 glfwPollEvents();
28 }
29
30 glfwDestroyWindow(window);
31
32 glfwTerminate();
33
34 return 0;
35 }

Let’s now configure the project to get rid of the errors. Open the project prop-
erties dialog and ensure that All Configurations is selected, because most of
the settings apply to both Debug and Release mode.

22
Go to C++ -> General -> Additional Include Directories and press
<Edit...> in the dropdown box.

Add the header directories for Vulkan, GLFW and GLM:

23
Next, open the editor for library directories under Linker -> General:

And add the locations of the object files for Vulkan and GLFW:

Go to Linker -> Input and press <Edit...> in the Additional


Dependencies dropdown box.

24
Enter the names of the Vulkan and GLFW object files:

And finally change the compiler to support C++17 features:

You can now close the project properties dialog. If you did everything right
then you should no longer see any more errors being highlighted in the code.
Finally, ensure that you are actually compiling in 64 bit mode:

Press F5 to compile and run the project and you should see a command prompt
and a window pop up like this:

25
The number of extensions should be non-zero. Congratulations, you’re all set
for playing with Vulkan!

Linux
These instructions will be aimed at Ubuntu, Fedora and Arch Linux users, but
you may be able to follow along by changing the package manager-specific com-
mands to the ones that are appropriate for you. You should have a compiler
that supports C++17 (GCC 7+ or Clang 5+). You’ll also need make.

Vulkan Packages
The most important components you’ll need for developing Vulkan applications
on Linux are the Vulkan loader, validation layers, and a couple of command-line
utilities to test whether your machine is Vulkan-capable:
• sudo apt install vulkan-tools or sudo dnf install vulkan-tools:
Command-line utilities, most importantly vulkaninfo and vkcube. Run
these to confirm your machine supports Vulkan.
• sudo apt install libvulkan-dev or sudo dnf install vulkan-loader-devel
: Installs Vulkan loader. The loader looks up the functions in the driver
at runtime, similarly to GLEW for OpenGL - if you’re familiar with that.
• sudo apt install vulkan-validationlayers-dev spirv-tools or
sudo dnf install mesa-vulkan-devel vulkan-validation-layers-devel:
Installs the standard validation layers and required SPIR-V tools. These
are crucial when debugging Vulkan applications, and we’ll discuss them
in the upcoming chapter.
On Arch Linux, you can run sudo pacman -S vulkan-devel to install all the
required tools above.
If installation was successful, you should be all set with the Vulkan portion.
Remember to run vkcube and ensure you see the following pop up in a window:

26
If you receive an error message then ensure that your drivers are up-to-date,
include the Vulkan runtime and that your graphics card is supported. See the
introduction chapter for links to drivers from the major vendors.

GLFW
As mentioned before, Vulkan by itself is a platform agnostic API and does not
include tools for creation a window to display the rendered results. To benefit
from the cross-platform advantages of Vulkan and to avoid the horrors of X11,
we’ll use the GLFW library to create a window, which supports Windows, Linux
and MacOS. There are other libraries available for this purpose, like SDL, but
the advantage of GLFW is that it also abstracts away some of the other platform-
specific things in Vulkan besides just window creation.
We’ll be installing GLFW from the following command:
1 sudo apt install libglfw3-dev

27
or
1 sudo dnf install glfw-devel

or
1 sudo pacman -S glfw-wayland # glfw-x11 for X11 users

GLM
Unlike DirectX 12, Vulkan does not include a library for linear algebra opera-
tions, so we’ll have to download one. GLM is a nice library that is designed for
use with graphics APIs and is also commonly used with OpenGL.
It is a header-only library that can be installed from the libglm-dev or
glm-devel package:
1 sudo apt install libglm-dev

or
1 sudo dnf install glm-devel

or
1 sudo pacman -S glm

Shader Compiler
We have just about all we need, except we’ll want a program to compile shaders
from the human-readable GLSL to bytecode.
Two popular shader compilers are Khronos Group’s glslangValidator and
Google’s glslc. The latter has a familiar GCC- and Clang-like usage, so we’ll
go with that: on Ubuntu, download Google’s unofficial binaries and copy glslc
to your /usr/local/bin. Note you may need to sudo depending on your per-
missions. On Fedora use sudo dnf install glslc, while on Arch Linux run
sudo pacman -S shaderc. To test, run glslc and it should rightfully complain
we didn’t pass any shaders to compile:
glslc: error: no input files
We’ll cover glslc in depth in the shader modules chapter.

Setting up a makefile project


Now that you have installed all of the dependencies, we can set up a basic
makefile project for Vulkan and write a little bit of code to make sure that
everything works.

28
Create a new directory at a convenient location with a name like VulkanTest.
Create a source file called main.cpp and insert the following code. Don’t worry
about trying to understand it right now; we’re just making sure that you can
compile and run Vulkan applications. We’ll start from scratch in the next
chapter.
1 #define GLFW_INCLUDE_VULKAN
2 #include <GLFW/glfw3.h>
3
4 #define GLM_FORCE_RADIANS
5 #define GLM_FORCE_DEPTH_ZERO_TO_ONE
6 #include <glm/vec4.hpp>
7 #include <glm/mat4x4.hpp>
8
9 #include <iostream>
10
11 int main() {
12 glfwInit();
13
14 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
15 GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window",
nullptr, nullptr);
16
17 uint32_t extensionCount = 0;
18 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
nullptr);
19
20 std::cout << extensionCount << " extensions supported\n";
21
22 glm::mat4 matrix;
23 glm::vec4 vec;
24 auto test = matrix * vec;
25
26 while(!glfwWindowShouldClose(window)) {
27 glfwPollEvents();
28 }
29
30 glfwDestroyWindow(window);
31
32 glfwTerminate();
33
34 return 0;
35 }

Next, we’ll write a makefile to compile and run this basic Vulkan code. Create
a new empty file called Makefile. I will assume that you already have some

29
basic experience with makefiles, like how variables and rules work. If not, you
can get up to speed very quickly with this tutorial.
We’ll first define a couple of variables to simplify the remainder of the file. Define
a CFLAGS variable that will specify the basic compiler flags:
1 CFLAGS = -std=c++17 -O2

We’re going to use modern C++ (-std=c++17), and we’ll set optimization level
to O2. We can remove -O2 to compile programs faster, but we should remember
to place it back for release builds.
Similarly, define the linker flags in a LDFLAGS variable:
1 LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr
-lXi

The flag -lglfw is for GLFW, -lvulkan links with the Vulkan function loader
and the remaining flags are low-level system libraries that GLFW needs. The
remaining flags are dependencies of GLFW itself: the threading and window
management.
It is possible that the Xxf68vm and Xi libraries are not yet installed on your
system. You can find them in the following packages:
1 sudo apt install libxxf86vm-dev libxi-dev

or
1 sudo dnf install libXi-devel libXxf86vm-devel

or
1 sudo pacman -S libxi libxxf86vm

Specifying the rule to compile VulkanTest is straightforward now. Make sure


to use tabs for indentation instead of spaces.
1 VulkanTest: main.cpp
2 g++ $(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS)

Verify that this rule works by saving the makefile and running make in the
directory with main.cpp and Makefile. This should result in a VulkanTest
executable.
We’ll now define two more rules, test and clean, where the former will run the
executable and the latter will remove a built executable:
1 .PHONY: test clean
2
3 test: VulkanTest

30
4 ./VulkanTest
5
6 clean:
7 rm -f VulkanTest

Running make test should show the program running successfully, and dis-
playing the number of Vulkan extensions. The application should exit with the
success return code (0) when you close the empty window. You should now
have a complete makefile that resembles the following:
1 CFLAGS = -std=c++17 -O2
2 LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr
-lXi
3
4 VulkanTest: main.cpp
5 g++ $(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS)
6
7 .PHONY: test clean
8
9 test: VulkanTest
10 ./VulkanTest
11
12 clean:
13 rm -f VulkanTest

You can now use this directory as a template for your Vulkan projects. Make a
copy, rename it to something like HelloTriangle and remove all of the code in
main.cpp.
You are now all set for the real adventure.

MacOS
These instructions will assume you are using Xcode and the Homebrew package
manager. Also, keep in mind that you will need at least MacOS version 10.11,
and your device needs to support the Metal API.

Vulkan SDK
The most important component you’ll need for developing Vulkan applications
is the SDK. It includes the headers, standard validation layers, debugging tools
and a loader for the Vulkan functions. The loader looks up the functions in the
driver at runtime, similarly to GLEW for OpenGL - if you’re familiar with that.
The SDK can be downloaded from the LunarG website using the buttons at the
bottom of the page. You don’t have to create an account, but it will give you
access to some additional documentation that may be useful to you.

31
The SDK version for MacOS internally uses MoltenVK. There is no native sup-
port for Vulkan on MacOS, so what MoltenVK does is actually act as a layer
that translates Vulkan API calls to Apple’s Metal graphics framework. With
this you can take advantage of debugging and performance benefits of Apple’s
Metal framework.
After downloading it, simply extract the contents to a folder of your choice
(keep in mind you will need to reference it when creating your projects on
Xcode). Inside the extracted folder, in the Applications folder you should
have some executable files that will run a few demos using the SDK. Run the
vkcube executable and you will see the following:

32
GLFW
As mentioned before, Vulkan by itself is a platform agnostic API and does not
include tools for creation a window to display the rendered results. We’ll use the
GLFW library to create a window, which supports Windows, Linux and MacOS.
There are other libraries available for this purpose, like SDL, but the advantage
of GLFW is that it also abstracts away some of the other platform-specific things
in Vulkan besides just window creation.
To install GLFW on MacOS we will use the Homebrew package manager to get
the glfw package:
1 brew install glfw

GLM
Vulkan does not include a library for linear algebra operations, so we’ll have
to download one. GLM is a nice library that is designed for use with graphics
APIs and is also commonly used with OpenGL.
It is a header-only library that can be installed from the glm package:
1 brew install glm

Setting up Xcode
Now that all the dependencies are installed we can set up a basic Xcode project
for Vulkan. Most of the instructions here are essentially a lot of “plumbing” so
we can get all the dependencies linked to the project. Also, keep in mind that
during the following instructions whenever we mention the folder vulkansdk we
are refering to the folder where you extracted the Vulkan SDK.
Start Xcode and create a new Xcode project. On the window that will open
select Application > Command Line Tool.

33
Select Next, write a name for the project and for Language select C++.

Press Next and the project should have been created. Now, let’s change the
code in the generated main.cpp file to the following code:
1 #define GLFW_INCLUDE_VULKAN
2 #include <GLFW/glfw3.h>
3
4 #define GLM_FORCE_RADIANS

34
5 #define GLM_FORCE_DEPTH_ZERO_TO_ONE
6 #include <glm/vec4.hpp>
7 #include <glm/mat4x4.hpp>
8
9 #include <iostream>
10
11 int main() {
12 glfwInit();
13
14 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
15 GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window",
nullptr, nullptr);
16
17 uint32_t extensionCount = 0;
18 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
nullptr);
19
20 std::cout << extensionCount << " extensions supported\n";
21
22 glm::mat4 matrix;
23 glm::vec4 vec;
24 auto test = matrix * vec;
25
26 while(!glfwWindowShouldClose(window)) {
27 glfwPollEvents();
28 }
29
30 glfwDestroyWindow(window);
31
32 glfwTerminate();
33
34 return 0;
35 }

Keep in mind you are not required to understand all this code is doing yet, we
are just setting up some API calls to make sure everything is working.
Xcode should already be showing some errors such as libraries it cannot find.
We will now start configuring the project to get rid of those errors. On the
Project Navigator panel select your project. Open the Build Settings tab and
then:
• Find the Header Search Paths field and add a link to /usr/local/include
(this is where Homebrew installs headers, so the glm and glfw3 header
files should be there) and a link to vulkansdk/macOS/include for the
Vulkan headers.
• Find the Library Search Paths field and add a link to /usr/local/lib

35
(again, this is where Homebrew installs libraries, so the glm and glfw3 lib
files should be there) and a link to vulkansdk/macOS/lib.
It should look like so (obviously, paths will be different depending on where you
placed on your files):

Now, in the Build Phases tab, on Link Binary With Libraries we will add
both the glfw3 and the vulkan frameworks. To make things easier we will be
adding the dynamic libraries in the project (you can check the documentation
of these libraries if you want to use the static frameworks).
• For glfw open the folder /usr/local/lib and there you will find a file
name like libglfw.3.x.dylib (“x” is the library’s version number, it
might be different depending on when you downloaded the package from
Homebrew). Simply drag that file to the Linked Frameworks and Libraries
tab on Xcode.
• For vulkan, go to vulkansdk/macOS/lib. Do the same for the both files
libvulkan.1.dylib and libvulkan.1.x.xx.dylib (where “x” will be
the version number of the the SDK you downloaded).
After adding those libraries, in the same tab on Copy Files change
Destination to “Frameworks”, clear the subpath and deselect “Copy only
when installing”. Click on the “+” sign and add all those three frameworks
here aswell.
Your Xcode configuration should look like:

36
The last thing you need to setup are a couple of environment variables. On
Xcode toolbar go to Product > Scheme > Edit Scheme..., and in the
Arguments tab add the two following environment variables:
• VK_ICD_FILENAMES = vulkansdk/macOS/share/vulkan/icd.d/MoltenVK_icd.json
• VK_LAYER_PATH = vulkansdk/macOS/share/vulkan/explicit_layer.d
It should look like so:

Finally, you should be all set! Now if you run the project (remembering to set-
ting the build configuration to Debug or Release depending on the configuration

37
you chose) you should see the following:

The number of extensions should be non-zero. The other logs are from the
libraries, you might get different messages from those depending on your con-
figuration.
You are now all set for the real thing.

38
Drawing a triangle

Setup
Base code
General structure
In the previous chapter you’ve created a Vulkan project with all of the proper
configuration and tested it with the sample code. In this chapter we’re starting
from scratch with the following code:
1 #include <vulkan/vulkan.h>
2
3 #include <iostream>
4 #include <stdexcept>
5 #include <cstdlib>
6
7 class HelloTriangleApplication {
8 public:
9 void run() {
10 initVulkan();
11 mainLoop();
12 cleanup();
13 }
14
15 private:
16 void initVulkan() {
17
18 }
19
20 void mainLoop() {
21
22 }
23
24 void cleanup() {
25

39
26 }
27 };
28
29 int main() {
30 HelloTriangleApplication app;
31
32 try {
33 app.run();
34 } catch (const std::exception& e) {
35 std::cerr << e.what() << std::endl;
36 return EXIT_FAILURE;
37 }
38
39 return EXIT_SUCCESS;
40 }

We first include the Vulkan header from the LunarG SDK, which provides the
functions, structures and enumerations. The stdexcept and iostream headers
are included for reporting and propagating errors. The cstdlib header provides
the EXIT_SUCCESS and EXIT_FAILURE macros.
The program itself is wrapped into a class where we’ll store the Vulkan objects
as private class members and add functions to initiate each of them, which will
be called from the initVulkan function. Once everything has been prepared,
we enter the main loop to start rendering frames. We’ll fill in the mainLoop
function to include a loop that iterates until the window is closed in a moment.
Once the window is closed and mainLoop returns, we’ll make sure to deallocate
the resources we’ve used in the cleanup function.
If any kind of fatal error occurs during execution then we’ll throw a
std::runtime_error exception with a descriptive message, which will propa-
gate back to the main function and be printed to the command prompt. To
handle a variety of standard exception types as well, we catch the more general
std::exception. One example of an error that we will deal with soon is
finding out that a certain required extension is not supported.
Roughly every chapter that follows after this one will add one new function
that will be called from initVulkan and one or more new Vulkan objects to the
private class members that need to be freed at the end in cleanup.

Resource management
Just like each chunk of memory allocated with malloc requires a call to free,
every Vulkan object that we create needs to be explicitly destroyed when we no
longer need it. In C++ it is possible to perform automatic resource management
using RAII or smart pointers provided in the <memory> header. However, I’ve
chosen to be explicit about allocation and deallocation of Vulkan objects in this

40
tutorial. After all, Vulkan’s niche is to be explicit about every operation to
avoid mistakes, so it’s good to be explicit about the lifetime of objects to learn
how the API works.
After following this tutorial, you could implement automatic resource manage-
ment by writing C++ classes that acquire Vulkan objects in their constructor
and release them in their destructor, or by providing a custom deleter to either
std::unique_ptr or std::shared_ptr, depending on your ownership require-
ments. RAII is the recommended model for larger Vulkan programs, but for
learning purposes it’s always good to know what’s going on behind the scenes.
Vulkan objects are either created directly with functions like vkCreateXXX, or
allocated through another object with functions like vkAllocateXXX. After
making sure that an object is no longer used anywhere, you need to destroy
it with the counterparts vkDestroyXXX and vkFreeXXX. The parameters for
these functions generally vary for different types of objects, but there is one
parameter that they all share: pAllocator. This is an optional parameter that
allows you to specify callbacks for a custom memory allocator. We will ignore
this parameter in the tutorial and always pass nullptr as argument.

Integrating GLFW
Vulkan works perfectly fine without creating a window if you want to use it for
off-screen rendering, but it’s a lot more exciting to actually show something!
First replace the #include <vulkan/vulkan.h> line with
1 #define GLFW_INCLUDE_VULKAN
2 #include <GLFW/glfw3.h>

That way GLFW will include its own definitions and automatically load the
Vulkan header with it. Add a initWindow function and add a call to it from
the run function before the other calls. We’ll use that function to initialize
GLFW and create a window.
1 void run() {
2 initWindow();
3 initVulkan();
4 mainLoop();
5 cleanup();
6 }
7
8 private:
9 void initWindow() {
10
11 }

The very first call in initWindow should be glfwInit(), which initializes the
GLFW library. Because GLFW was originally designed to create an OpenGL

41
context, we need to tell it to not create an OpenGL context with a subsequent
call:
1 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

Because handling resized windows takes special care that we’ll look into later,
disable it for now with another window hint call:
1 glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

All that’s left now is creating the actual window. Add a GLFWwindow* window;
private class member to store a reference to it and initialize the window with:
1 window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);

The first three parameters specify the width, height and title of the window.
The fourth parameter allows you to optionally specify a monitor to open the
window on and the last parameter is only relevant to OpenGL.
It’s a good idea to use constants instead of hardcoded width and height num-
bers because we’ll be referring to these values a couple of times in the future.
I’ve added the following lines above the HelloTriangleApplication class defi-
nition:
1 const uint32_t WIDTH = 800;
2 const uint32_t HEIGHT = 600;

and replaced the window creation call with


1 window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);

You should now have a initWindow function that looks like this:
1 void initWindow() {
2 glfwInit();
3
4 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
5 glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
6
7 window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr,
nullptr);
8 }

To keep the application running until either an error occurs or the window is
closed, we need to add an event loop to the mainLoop function as follows:
1 void mainLoop() {
2 while (!glfwWindowShouldClose(window)) {
3 glfwPollEvents();
4 }
5 }

42
This code should be fairly self-explanatory. It loops and checks for events like
pressing the X button until the window has been closed by the user. This is
also the loop where we’ll later call a function to render a single frame.
Once the window is closed, we need to clean up resources by destroying it and
terminating GLFW itself. This will be our first cleanup code:
1 void cleanup() {
2 glfwDestroyWindow(window);
3
4 glfwTerminate();
5 }

When you run the program now you should see a window titled Vulkan show
up until the application is terminated by closing the window. Now that we have
the skeleton for the Vulkan application, let’s create the first Vulkan object!
C++ code

Instance
Creating an instance
The very first thing you need to do is initialize the Vulkan library by creat-
ing an instance. The instance is the connection between your application and
the Vulkan library and creating it involves specifying some details about your
application to the driver.
Start by adding a createInstance function and invoking it in the initVulkan
function.
1 void initVulkan() {
2 createInstance();
3 }

Additionally add a data member to hold the handle to the instance:


1 private:
2 VkInstance instance;

Now, to create an instance we’ll first have to fill in a struct with some information
about our application. This data is technically optional, but it may provide some
useful information to the driver in order to optimize our specific application
(e.g. because it uses a well-known graphics engine with certain special behavior).
This struct is called VkApplicationInfo:
1 void createInstance() {
2 VkApplicationInfo appInfo{};
3 appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
4 appInfo.pApplicationName = "Hello Triangle";

43
5 appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
6 appInfo.pEngineName = "No Engine";
7 appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
8 appInfo.apiVersion = VK_API_VERSION_1_0;
9 }

As mentioned before, many structs in Vulkan require you to explicitly specify


the type in the sType member. This is also one of the many structs with a
pNext member that can point to extension information in the future. We’re
using value initialization here to leave it as nullptr.
A lot of information in Vulkan is passed through structs instead of function
parameters and we’ll have to fill in one more struct to provide sufficient in-
formation for creating an instance. This next struct is not optional and tells
the Vulkan driver which global extensions and validation layers we want to use.
Global here means that they apply to the entire program and not a specific
device, which will become clear in the next few chapters.
1 VkInstanceCreateInfo createInfo{};
2 createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
3 createInfo.pApplicationInfo = &appInfo;

The first two parameters are straightforward. The next two layers specify the
desired global extensions. As mentioned in the overview chapter, Vulkan is a
platform agnostic API, which means that you need an extension to interface
with the window system. GLFW has a handy built-in function that returns the
extension(s) it needs to do that which we can pass to the struct:
1 uint32_t glfwExtensionCount = 0;
2 const char** glfwExtensions;
3
4 glfwExtensions =
glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
5
6 createInfo.enabledExtensionCount = glfwExtensionCount;
7 createInfo.ppEnabledExtensionNames = glfwExtensions;

The last two members of the struct determine the global validation layers to
enable. We’ll talk about these more in-depth in the next chapter, so just leave
these empty for now.
1 createInfo.enabledLayerCount = 0;

We’ve now specified everything Vulkan needs to create an instance and we can
finally issue the vkCreateInstance call:
1 VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);

44
As you’ll see, the general pattern that object creation function parameters in
Vulkan follow is:
• Pointer to struct with creation info
• Pointer to custom allocator callbacks, always nullptr in this tutorial
• Pointer to the variable that stores the handle to the new object
If everything went well then the handle to the instance was stored in the
VkInstance class member. Nearly all Vulkan functions return a value of type
VkResult that is either VK_SUCCESS or an error code. To check if the instance
was created successfully, we don’t need to store the result and can just use a
check for the success value instead:
1 if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS)
{
2 throw std::runtime_error("failed to create instance!");
3 }

Now run the program to make sure that the instance is created successfully.

Encountered VK_ERROR_INCOMPATIBLE_DRIVER:
If using MacOS with the latest MoltenVK sdk, you may get VK_ERROR_INCOMPATIBLE_DRIVER
returned from vkCreateInstance. According to the Getting Start Notes. Be-
ginning with the 1.3.216 Vulkan SDK, the VK_KHR_PORTABILITY_subset
extension is mandatory.
To get over this error, first add the VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR
bit to VkInstanceCreateInfo struct’s flags, then add VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME
to instance enabled extension list.
Typically the code could be like this:
1 ...
2
3 std::vector<const char*> requiredExtensions;
4
5 for(uint32_t i = 0; i < glfwExtensionCount; i++) {
6 requiredExtensions.emplace_back(glfwExtensions[i]);
7 }
8
9 requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
10
11 createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
12
13 createInfo.enabledExtensionCount = (uint32_t)
requiredExtensions.size();
14 createInfo.ppEnabledExtensionNames = requiredExtensions.data();
15

45
16 if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS)
{
17 throw std::runtime_error("failed to create instance!");
18 }

Checking for extension support


If you look at the vkCreateInstance documentation then you’ll see that one
of the possible error codes is VK_ERROR_EXTENSION_NOT_PRESENT. We could
simply specify the extensions we require and terminate if that error code comes
back. That makes sense for essential extensions like the window system interface,
but what if we want to check for optional functionality?
To retrieve a list of supported extensions before creating an instance,
there’s the vkEnumerateInstanceExtensionProperties function. It takes
a pointer to a variable that stores the number of extensions and an array
of VkExtensionProperties to store details of the extensions. It also takes
an optional first parameter that allows us to filter extensions by a specific
validation layer, which we’ll ignore for now.
To allocate an array to hold the extension details we first need to know how
many there are. You can request just the number of extensions by leaving the
latter parameter empty:
1 uint32_t extensionCount = 0;
2 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
nullptr);

Now allocate an array to hold the extension details (include <vector>):


1 std::vector<VkExtensionProperties> extensions(extensionCount);

Finally we can query the extension details:


1 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
extensions.data());

Each VkExtensionProperties struct contains the name and version of an ex-


tension. We can list them with a simple for loop (\t is a tab for indentation):
1 std::cout << "available extensions:\n";
2
3 for (const auto& extension : extensions) {
4 std::cout << '\t' << extension.extensionName << '\n';
5 }

You can add this code to the createInstance function if you’d like
to provide some details about the Vulkan support. As a challenge, try
to create a function that checks if all of the extensions returned by

46
glfwGetRequiredInstanceExtensions are included in the supported ex-
tensions list.

Cleaning up
The VkInstance should be only destroyed right before the program exits. It
can be destroyed in cleanup with the vkDestroyInstance function:
1 void cleanup() {
2 vkDestroyInstance(instance, nullptr);
3
4 glfwDestroyWindow(window);
5
6 glfwTerminate();
7 }

The parameters for the vkDestroyInstance function are straightforward. As


mentioned in the previous chapter, the allocation and deallocation functions in
Vulkan have an optional allocator callback that we’ll ignore by passing nullptr
to it. All of the other Vulkan resources that we’ll create in the following chapters
should be cleaned up before the instance is destroyed.
Before continuing with the more complex steps after instance creation, it’s time
to evaluate our debugging options by checking out validation layers.
C++ code

Validation layers
What are validation layers?
The Vulkan API is designed around the idea of minimal driver overhead and one
of the manifestations of that goal is that there is very limited error checking in
the API by default. Even mistakes as simple as setting enumerations to incorrect
values or passing null pointers to required parameters are generally not explicitly
handled and will simply result in crashes or undefined behavior. Because Vulkan
requires you to be very explicit about everything you’re doing, it’s easy to make
many small mistakes like using a new GPU feature and forgetting to request it
at logical device creation time.
However, that doesn’t mean that these checks can’t be added to the API. Vulkan
introduces an elegant system for this known as validation layers. Validation
layers are optional components that hook into Vulkan function calls to apply
additional operations. Common operations in validation layers are:
• Checking the values of parameters against the specification to detect mis-
use
• Tracking creation and destruction of objects to find resource leaks
• Checking thread safety by tracking the threads that calls originate from

47
• Logging every call and its parameters to the standard output
• Tracing Vulkan calls for profiling and replaying
Here’s an example of what the implementation of a function in a diagnostics
validation layer could look like:
1 VkResult vkCreateInstance(
2 const VkInstanceCreateInfo* pCreateInfo,
3 const VkAllocationCallbacks* pAllocator,
4 VkInstance* instance) {
5
6 if (pCreateInfo == nullptr || instance == nullptr) {
7 log("Null pointer passed to required parameter!");
8 return VK_ERROR_INITIALIZATION_FAILED;
9 }
10
11 return real_vkCreateInstance(pCreateInfo, pAllocator, instance);
12 }

These validation layers can be freely stacked to include all the debugging func-
tionality that you’re interested in. You can simply enable validation layers for
debug builds and completely disable them for release builds, which gives you
the best of both worlds!
Vulkan does not come with any validation layers built-in, but the LunarG
Vulkan SDK provides a nice set of layers that check for common errors. They’re
also completely open source, so you can check which kind of mistakes they check
for and contribute. Using the validation layers is the best way to avoid your
application breaking on different drivers by accidentally relying on undefined
behavior.
Validation layers can only be used if they have been installed onto the system.
For example, the LunarG validation layers are only available on PCs with the
Vulkan SDK installed.
There were formerly two different types of validation layers in Vulkan: instance
and device specific. The idea was that instance layers would only check calls
related to global Vulkan objects like instances, and device specific layers would
only check calls related to a specific GPU. Device specific layers have now been
deprecated, which means that instance validation layers apply to all Vulkan calls.
The specification document still recommends that you enable validation layers at
device level as well for compatibility, which is required by some implementations.
We’ll simply specify the same layers as the instance at logical device level, which
we’ll see later on.

Using validation layers


In this section we’ll see how to enable the standard diagnostics layers provided
by the Vulkan SDK. Just like extensions, validation layers need to be enabled

48
by specifying their name. All of the useful standard validation is bundled into
a layer included in the SDK that is known as VK_LAYER_KHRONOS_validation.
Let’s first add two configuration variables to the program to specify the layers
to enable and whether to enable them or not. I’ve chosen to base that value
on whether the program is being compiled in debug mode or not. The NDEBUG
macro is part of the C++ standard and means “not debug”.
1 const uint32_t WIDTH = 800;
2 const uint32_t HEIGHT = 600;
3
4 const std::vector<const char*> validationLayers = {
5 "VK_LAYER_KHRONOS_validation"
6 };
7
8 #ifdef NDEBUG
9 const bool enableValidationLayers = false;
10 #else
11 const bool enableValidationLayers = true;
12 #endif

We’ll add a new function checkValidationLayerSupport that checks if all of


the requested layers are available. First list all of the available layers using
the vkEnumerateInstanceLayerProperties function. Its usage is identical to
that of vkEnumerateInstanceExtensionProperties which was discussed in
the instance creation chapter.
1 bool checkValidationLayerSupport() {
2 uint32_t layerCount;
3 vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
4
5 std::vector<VkLayerProperties> availableLayers(layerCount);
6 vkEnumerateInstanceLayerProperties(&layerCount,
availableLayers.data());
7
8 return false;
9 }

Next, check if all of the layers in validationLayers exist in the availableLayers


list. You may need to include <cstring> for strcmp.
1 for (const char* layerName : validationLayers) {
2 bool layerFound = false;
3
4 for (const auto& layerProperties : availableLayers) {
5 if (strcmp(layerName, layerProperties.layerName) == 0) {
6 layerFound = true;
7 break;

49

You might also like