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:
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:
- Basic Vulkan API usage from drawing a triangle to 3D graphics
- 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:
- The Vulkan spec refers to the C API when describing behavior and expected usage. Comparing the spec with the generated bindings and searching in the spec will be easier.
- The Validation Layers will refer to the C API when they warn you about incorrect API usage. Finding errors in the code with matching function/struct/field names will be easier.
- The expertise gained in Vulkan usage will be easier to transfer to any other programming language whose bindings mostly or exactly match the Vulkan C API.
There are drawbacks as well:
- The types of enum values generated by rust-bindgen are platform specific, so it's possible that what compiles on Linux may not compile on Windows.
- Sometimes rust-bindgen does not handle macros well, and constants defined by macros may have bad types, bad values, etc.
- There will be a huge amount of unsafe code.
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:
- The names of structs, functions, etc. often differ, because they like to adapt names to the Rust coding convention. This makes struct/field/function/parameter names harder to google and existing Vulkan resources referring to the C API are harder to adapt to these libraries.
- Libraries maintained by a handful of volunteers can become abandonware. When these get used in a professional environment, either package maintenance will fall upon the developer team, or the libraries need to be phased out.
- Libraries trying to invent safe abstractions can be restrictive in their Vulkan API usage, and the difficulties this causes when implementing advanced rendering techniques/optimizations are unknown.
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.
- Writing the math utils for yourself will force you to understand the details and the logical steps and help gain deep knowledge.
- For a custom graphics engine you will very likely need to create custom data structures following DOD practices for maximum performance. You will very likely need to follow resources such as Introduction to Data-Oriented Design - Frostbite and you will very likely need to adapt your math utils to your data structures as well, resulting in a custom math library anyway.
- Third party libraries can become abandonware.
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:
- References cannot outlive variables they refer to.
- Either there is exactly one mutable reference, or there are any number of immutable references referring to a variable.
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.