Home

This tutorial is in open beta. There may be bugs in the code and misinformation and inaccuracies in the text. If you find any, feel free to open a ticket on the repo of the code samples.

Introduction

This is a computer graphics tutorial that teaches you the basics of the Vulkan API, 3D graphics and physically based rendering in the Rust programming language. In other words how to write something like this from scratch:

Screenshot of the application at the end of the tutorial.
Figure 1: Screenshot of the application at the end of the tutorial.

Prerequisites

This tutorial assumes that you can confidently write Rust code. The tutorial applications will be single threaded for the sake of simplicity, but Vulkan has some multithreaded behavior that will be mentioned when appropriate. Basic knowledge of multithreading is useful for understanding them.

This tutorial assumes no prior knowledge of Vulkan.

Tutorial structure

The tutorials are divided into two categories:

  1. Basic Vulkan API usage from drawing a triangle to 3D graphics
  2. The basics of Physically Based Rendering

The Vulkan basics part covers an introduction to rendering with Vulkan and the basic mathematics behind 3D rendering. The discussed Vulkan features will include the graphics pipeline, vertex and index buffers, uniform buffers, texturing and depth buffering. It is inspired by the Vulkan tutorial of Alexander Overvoorde.

The Physically Based Rendering part covers the theory and basic implementation of modern real time rendering techniques. Topics will include the rendering equation, diffuse and specular lighting and global illumination with environment mapping. The most influental works this part is based on are Moving Frostbite to Physically Based Rendering 3.0 by Sébastien Lagarde, Real Shading in Unreal Engine 4 by Brian Karis, and Physically Based Shading at Disney by Brent Burley, with a few stolen pieces of shader code from learnopengl.com's PBR tutorials. (Proper links will be available in the relevant chapters.)

The tutorials are not bite sized and may introduce large pieces of information at once. Every chapter will contain all the information needed to extend the previous chapter's working application to another working application. The focus is on modern desktop and mobile hardware, so the tutorial uses some modern features that may not be supported on certain low end hardware.

Code organization

The sample applications will be "single main function" applications that can be read from beginning to end.

The goal is to illustrate the Vulkan API usage as plainly as possible, without added complexity such as any kind of tutorial framework. The sample applications will have a simple lifecycle, with main functions containing setup code at the beginning, a main loop in the middle, and cleanup at the end. I hope this makes the application's Vulkan API usage easy to comprehend.

Only a handful of functionality will be extracted into functions. This either means tiny utility functions, or a few select functionality where I deemed that the added nontriviality does not add significant cognitive load when reading and the avoided code duplication is valueable enough.

This tutorial does not try to invent any safe abstractions above Vulkan. When rendering needs to be fast, advanced rendering techniques such as GPU driven renderingí (compute based mesh cluster and triangle culling with subgroup operations and multi draw indirect), "bindless" resources and so on can become a necessity, and the difficulties of implementing these techniques with any kind of safe abstractions is not a well researched area.

Interfacing Rust with Vulkan

This tutorial uses rust-bindgen to generate bindings from the Vulkan headers. This way the structs and functions used for calling into Vulkan will match the C API the vast majority of times. The benefits are the following:

There are drawbacks as well:

The tutorial will show you how to handle these when appropriate.

There are existing Rust libraries for interfacing with Vulkan. This tutorial does not use those for the following reasons:

Other third party libraries

This tutorial will not use third party libraries for maths. Instead it teaches you how to write math utils for the necessary matrix operations and matrices for yourself for the following reasons.

For basic windowing the tutorial will use the sdl2 crate. Thanks to SDL2 the same code can compile on both Windows and Linux. Creating windows and handling events will take only a small amount of code and you can swap it out if you don't like it.

Why would you use Vulkan and Rust?

Rust

Rust is a memory safe systems programming language. It achieves memory safety with lifetimes and borrow checking. Rust references are like pointers in C++, so they are addresses that can be used to access data, but the language enforces some rules that help with memory safety:

With these rules in place rust references always point to valid memory, and data modifications get easier to trace, which helps preventing a large class of bugs resulting in hard to trace incorrect functionality, crashes and security holes.

These rules are restrictive, and getting comfortable with it takes time and fight with the borrow checker, but following them helps with unlearning twisted code and data organization practices that can easily get buggy, hard to read and hard to parallelize.

Safe Rust eliminates memory related bugs, and the performance is at least on par with and sometimes faster than C or C++. When interfacing with C APIs is necessary, unsafe Rust is available, but there you need to know what you are doing.

Vulkan

Vulkan is an explicit API for GPU programming that gives you control over some low level constructs, memory management, and synchronization. Since several AAA games were built using it, such as Doom 2016, Doom Eternal, and the Linux port of Rise of the Tomb Raider and Shadow of the Tomb Raider, Vulkan is a proven technology for game development.

In previous APIs such as OpenGL and DirectX 11, the driver had to handle memory management and synchronization in a very conservative way in order to fulfill certain API contracts that many applications do not need for correct behavior, based on partial information reconstructed from the API usage.

This results in weaker and driver specific/unpredictable performance. Applications were forced to use these old APIs in very specific ways to prevent the driver from unwanted synchronizations, resource duplications, shader recompilations, etc.

Old graphics APIs also did not lend themselves well (or at all) to multithreading.

Vulkan offers low level building blocks to utilize GPU functionality and handle memory management and synchronization, pushing more of that responsibility to the application. This way developers can implement well informed application specific multithreading, memory management and synchronization logic that is simpler and faster than the heuristics of a generic one size fits all driver.