Setup and Tour

This step will get your development environment set up and discuss the overall structure of the code you'll be working on.

Setup

The nakluV tutorial uses a C++ compiler, Node.js (to run the Maekfile.js script), GLFW 3.4 (for window and event management), and the Vulkan SDK 1.4.335.[0/1] (for talking to the GPU). Compiler and library versions do (somewhat) matter here, since we're relying on a prebuilt binary blob for the refsol code.

You'll be building and running the tutorial code from the command line. On macOS and Linux this means some sh-like shell; on Windows this means cmd.exe. In all cases we expect that your C++ compiler, Node.js, and the command line git utility are available in your path. It will also be convenient if you can launch your preferred code editor from the terminal.

Get The Starter Code

The first step is the same for all OSs: clone the starter code repository. You can put this in a subdirectory if you want, just make sure that your GLFW directory ends up as a sibling of the nakluV directory (or edit the Maekfile.js).

$ git clone git@github.com:15-472/nakluV nakluV

Get the Prerequisites - (Linux, x86_x64)

Compiler and Node.js: if you don't already have them, get a recent version of GCC's g++ and Node.js's node from your OS's package manager.

For example, on Debian:

$ sudo apt install g++ nodejs

Install GLFW (from source): if your distribution doesn't provide an up-to-date GLFW, you can build it from source. Download glfw-3.4.zip from the GLFW 3.4 release page, extract it as a sibling of your nakluV directory; then use cmake to build and "install" it to glfw-3.4/out. (NOTE: you may also need to install the xorg-dev package before building. See Compiling GLFW for more details. You may also choose to build in Wayland support instead of X11 if that's your windowing system of choice.)

Putting that together:

$ sudo apt install xorg-dev #adjust for your system
$ wget https://github.com/glfw/glfw/releases/download/3.4/glfw-3.4.zip -Oglfw-3.4.zip
$ unzip glfw-3.4.zip
$ cd glfw-3.4
$ cmake -S . -B build -D GLFW_BUILD_WAYLAND=0 -D CMAKE_INSTALL_PREFIX=`pwd`/out
$ cd build
$ make install

Alternatively, install GLFW (from package manager): If your system provides a GLFW 3.4 package, you should be able to install that directly:

$ sudo apt install libglfw3-dev

Note that, at least on Debian Sid, this package installs a library -- libglfw.so -- without a 3 in the name; so you will need to edit Maekfile.js to change `-lglfw3` to `-lglfw` in the if (maek.OS === 'linux') part of the custom_flags_and_rules() function.

Install the Vulkan SDK: download vulkansdk-linux-x86_64-1.4.335.0.tar.xz from LunarG's SDK downloads page and extract it somewhere convenient (I just put it in my home directory).

Putting that together:

$ mkdir VulkanSDK
$ cd VulkanSDK
$ wget https://sdk.lunarg.com/sdk/download/1.4.335.0/linux/vulkansdk-linux-x86_64-1.4.335.0.tar.xz
$ tar xvf vulkansdk-linux-x86_64-1.4.335.0.tar.xz

Source the included setup-env.sh script in your terminal before you start a development session; this sets the VULKAN_SDK environment variable, along with the layers path.

$ source ~/VulkanSDK/1.4.355.0/setup-env.sh
$ #... now edit your code, run Maekfile.js, run your bin/main, etc ...
Before doing Vulkan development in a shell, source the setup-env.sh script to set important environment variables.

Get the Prerequisites - macOS (arm64)

C++ Compiler: we use the clang++ supplied with XCode (and available separately as a "command line tools" package). I believe that if you set up the Homebrew package manager, it will automatically install the command line tools package.

Node.js, GLFW: Homebrew's package database is nicely up-to-date, it appears. So you should be able to just do:

$ brew install node
$ brew install glfw

Vulkan SDK: there is a GUI installer available from LunarG's SDK page (scroll down). Particularly, you want vulkansdk-macos-1.4.335.1.zip. Put the VulkanSDK folder wherever is convenient for you (I put it in my home directory).

The Vulkan SDK contains a setup-env.sh script you should source in your shell before doing development work. This script will set environment variables so our build script can find the SDK (and so Vulkan apps can find, e.g., the validation layer).

$ source ~/VulkanSDK/1.4.355.1/setup-env.sh
$ #... now edit your code, run Maekfile.js, run your bin/main, etc ...
Before doing Vulkan development in a shell, source the setup-env.sh script to set important environment variables.

Get the Prerequisites - Windows (x86_x64)

C++ Compiler, Node.js, and Vulkan SDK (from the command line) Windows has a package manager called winget. You can use it to install almost everything we need (except GLFW):

> winget install -e --id OpenJS.NodeJS
> winget install -e --id Microsoft.VisualStudio.BuildTools --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
> winget install -e --id KhronosGroup.VulkanSDK

Small caveat: not sure our target version of the SDK (1.4.335.0) is one you will get this way (though it is at time of writing). The one you get might be close enough to "just work," however. If not, there are some HTML comments in this document (right click, view page source) that talk about how to do the install more manually.

Visual Studio version note: as of this writing, Visual Studio 2026 has been recently released, and the winget line above will install its build tools. The pre-built object files for this tutorial were generated with VS2022, but appear to "just work" with VS2026.

Command-Line note: on Windows, we use cmd.exe as our shell. Particularly, you'll want a shell with the visual studio build environment configured. The batch file vcvars64.bat included with the visual studio build tools (at C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat, at least in my case) provides such an environment. You can run this manually from a cmd.exe, but it's probably easier to create a shortcut to it, then edit the shortcut to run %comspec% /k "path\to\vcvars64.bat" in a working directory of your choice (I use %userprofile%\Code).

the shortcut properties for running a shell with vcvars64.bat
A shortcut that starts a cmd.exe with the visual studio build tools configured via vcvars64.bat.

GLFW: Download glfw-3.4.bin.WIN64.zip from the GLFW 3.4 github release page. Extract the contained glfw-3.4.bin.WIN64 folder as a sibling of the nakluV folder holding the starter code. If you build GLFW from source or place the library elsewhere, edit Maekfile.js and update the glfw-related `/I` and `/LIBPATH:` arguments to the compiler and linker, respectively.

Get the Prerequisites - Other

Unfortunately, this tutorial is built around the idea of having a pre-built binary blob that you can link against. If you'd like to use an OS/architecture combination not listed above it is unlikely that you'll be able to link against our pre-built refsol.o* object file. Similarly, if you are on a supported platform but using a compiler which is not compatible with the intermediate files of the compiler we have chosen, you are probably also out of luck.

Regardless, please consider filing an issue so we can track demand for different platforms.

Build the Code

Now that you have the code and all the prerequisites, you should be able to build the code and run the output.

Linux/macOS:

$ source ~/VulkanSDK/1.4.335.*/setup-env.sh
$ cd nakluV
$ node Maekfile.js
$ bin/main

Windows: (in a vcvars64.bat'd command prompt, as discussed above)

> cd nakluV
> node Maekfile.js
> bin\main

If you've got everything set up properly this will compile without errors. Running the executable will show a boring grey window:

a boring grey window
A boring grey window -- the starting point for our tutorial.

Tour

Now that the code is building, let's take a quick stroll through the files and what they do.

Structurally, you can think of this code as a three-layer cake, with each layer becoming more specific. The base layer of the cake initializes the Vulkan API and the objects needed to use the GPU; the middle ("harness") layer manages the window and inputs -- it deals with the parts of Vulkan that every real-time graphics application needs (but not every Vulkan application needs), and provides a main loop to orchestrate the handling of events and the production of frames; and the top ("application") layer actually does real-time graphics using the support from layers below.

As implemented, the base and harness both live in struct RTG (see RTG.hpp and RTG.cpp), with some additional helper functions (perhaps think of them as sprinkles on the base layer of the cake) for dealing with common tasks like memory allocation provided by struct Helpers (see Helpers.hpp and Helpers.cpp). The application layer is implemented in struct Tutorial (see Tutorial.hpp and Tutorial.cpp); this is where you'll be adding most of your new code at first, since it is where you can immediately see visible results from your changes.

To see how the layers work together, look at this (slightly simplified) version of the main function from (in main.cpp):

int main(int argc, char **argv) {
	//configure application:
	RTG::Configuration configuration;

	configuration.application_info = VkApplicationInfo{
		.pApplicationName = "nakluV Tutorial",
		.applicationVersion = VK_MAKE_VERSION(0,0,0),
		.pEngineName = "Unknown",
		.engineVersion = VK_MAKE_VERSION(0,0,0),
		.apiVersion = VK_API_VERSION_1_3
	};

	/* ...command-line parsing... */

	//loads vulkan library, creates surface, initializes helpers:
	RTG rtg(configuration);

	//initializes global (whole-life-of-application) resources:
	Tutorial application(rtg);

	//main loop -- handles events, renders frames, etc:
	rtg.run(application);
}
The main function, from main.cpp. Some configuration handling code elided for simplicity.

The configuration object sets up parameters for the bottom two layers; then the RTG structure is constructed to instantiate the base and harness layers; then the Tutorial application is constructed and handed a reference to the base and harness layers so it can use them to set up relevant GPU resources; and finally the RTG::run function is used to orchestrate calls to Tutorial's application callbacks.

The VK macro

Throughout the existing code (and the code you write in this tutorial), you'll see a macro, VK, which is defined as follows:

#define VK( FN ) \
	if (VkResult result = FN; result != VK_SUCCESS) { \
		throw std::runtime_error("Call '" #FN "' returned " + std::to_string(result) + " [" + std::string(string_VkResult(result)) + "]." ); \
	}
Definition of the VK macro, from VK.hpp.

This code calls the function, and -- if the result isn't VK_SUCCESS -- reports the result along with the function call and the error constant (converted to a string).

Note, particularly, the string_VkResult function from the vulkan/vk_enum_string_helper.h header. This header has lots of helpers for converting Vulkan enums into readable strings for debug output -- worth looking at when next you want to (e.g.) print out the current layout of an image.

We wrap almost every Vulkan function call in a VK( ) so that if it returns an error that error will get thrown and reported.

In a production, go-as-fast-as-possible codebase we'd probably turn off this error checker -- i.e., #define VK( FN ) FN. But for our learning environment we want to catch problems as quickly as possible instead of only observing downstream consequences of a failure later.

The refsol::

As you look around in the code you'll probably notice a lot of functions don't seem to actually do much on their own, instead passing their parameters onward to a function in the refsol:: namespace:

void Tutorial::destroy_framebuffers() {
	refsol::Tutorial_destroy_framebuffers(rtg, &swapchain_depth_image, &swapchain_depth_image_view, &swapchain_framebuffers);
}
The Tutorial::destroy_framebuffers function from Tutorial.cpp.

Usage of refsol:: functions is the key to the "inside-out and backwards" strategy of this tutorial. When you see a refsol:: function it means that we think this code is important to write and understand eventually, but we also know that writing it before you get to doing something actually interesting would be a huge drag on your enthusiasm.

The implementations of these boring-at-the-start functions reside in a binary blob (one of pre/*/refsol.*, depending on platform). By the end of this tutorial you will have replaced all the refsol:: calls with your own code.