Bálint's Blog
Nobody teaches Vulkan right Published: 2/7/2026
format_quote
subjective (adj.)
Influenced by or based on personal beliefs or feelings, rather than based on facts. ~ Cambridge Dictionary

Due to the circles I usually move around in (mainly graphics and gpu programming related Discord servers filled with a very wide range of differing graphics programming experience levels) I hear two very specific opinions quite often:

While both of these have some truth behind them (Vulkan is verbose, but not nearly that much and it is a hard API to master), I still consider them to be misconceptions stemming from inadequate learning materials. This is true both for tutorials like the outdated search engine favourite vulkan-tutorial or even newer ones that I personally call very decent, like the official Khronos tutorial series or HowToVulkan. My issue with the approaches these resources take is more fundamental.

Before I continue, I feel obligated to mention that I probably haven’t read all guides that teach the subject, especially if it’s in a book form. Whenever I make sweeping claims about “all tutorials”, take that with a grain of salt.

In the next section I’ll list my main problems with the existing learning resources, after that I’ll propose my preferred learning method.

Main gripes

A home cook’s guide to Vulkan

Most Vulkan learning resources are structured like recipes:

  1. First create your VkApplicationInfo struct, don’t forget to set sType.
  2. Now create the VkInstanceCreateInfo struct using the application info, sType is pretty important once again.
  3. Call vkCreateInstance and first step’s done!
  4. Let’s create the logical device, start with VkDeviceQueueCreateInfo. Did I mention sType yet?
  5. Now onto VkDeviceCreateInfo. It’s paramount that you do not forget to fill out sType.

  1. Now call vkCmdDraw(commandBuffer, 3, 1, 0, 0) and we have a colored triangle!

I think most people intuitively understand that this isn’t a good way to teach or learn a new, complex subject. The only reason actual cooking recipes are structured like this is that we don’t expect the reader to grasp the intricacies of how different ingredients affect the texture and taste of the final product, so why would we apply this style to a topic where we want the reader come out “self-sufficient” with the capacity to write their own GPU wrangling logic?

I am error

Validation layers are the main reason I never wish to write any OpenGL code again. Contrary to popular belief, they aren’t just there to catch the silly mistakes you make while you copy the vulkan-tutorial code verbatim into your text editor. They can and should lessen the amount of information you need to memorize to use the API.

Pretty much all tutorials enable validation layers in the code. This is a huge waste of time of course since vulkan configurator exists and is way more convenient in every way, but it’s definitely better than not doing anything with them. However after this step most just abandon what is arguably the best feature of the API, not even bothering to include a mini tutorial on how one can read them. In my opinion they should be more integral to the guides.

LARP-ing cavemen

Most (though definitely not all) guides still refuse to assume the reader has access to modern tools. At trade schools people use power tools, law schools ask the students to use the internet for research and computer science university courses are finally just a few years away from not making the students write their code on pen and paper, so why can I go through most Vulkan materials just as easily using notepad.exe as with Visual Studio?

People should be expected to rely on LSP-s and intellisense more, but the way most tutorials are structured simply doesn’t give them to push to experiment with this approach.

Fishing tutorial

In this chapter I’ll be sharing the way I personally approach Vulkan whenever I have to do something with it, assuming the role of someone who remembers close to nothing of it. If I were to write a tutorial for the API, that would follow a similar structure, mainly to promote self-sufficiency.

The main prerequisites one needs to get around in Vulkan are the following:

I prefer to work with Vulkan in a goal-oriented fashion, nobody gets into this hobby with the intent to create a vulkan instance object or select a physical device from a list of course, so the easiest way to achieve this requirement is to start with what they’ll actually want to do: Draw something to the screen. So the first line I’d type in the main function is

vkCmdDraw();

Of course this wouldn’t compile and that’s fine, the reason for why it fails can be the engine that moves the progress forward. The error will look something like this:

error: too few arguments to function
void vkCmdDraw(
        VkCommandBuffer commandBuffer,
        uint32_t vertexCount,
        uint32_t instanceCount,
        uint32_t baseVertexId,
        uint32_t baseInstanceId)

(I added the formatting for readability)

Alternatively one could look at the function signature popup window in the IDE to figure out what parameters it takes. The last 4 arguments are easy to find through a quick skim of the function documentation, but we’ll have to get a command buffer from somewhere for the first one.

So clearly the next step is to do just that. Just by typing in vkCommandBuffer depending on how well the fuzzy finder of the LSP works, it might suggest vkAllocateCommandBuffers, but in a tutorial context I would also mention the name of this function for the less adventurous readers. The chain would continue from here, vkAllocateCommandBuffers needs a command pool, which needs a device, which needs an instance, essentially this would be like building up a Vulkan project “backwards” compared to the traditional methods.

During this process most of the time it isn’t necessary to look anything up, if a function needs a struct instance, it can be left empty, if it needs a vulkan object, it can be created. The first roadblock is selecting the physical device for vkCreateDevice, as it is not obvious how you’d use the parameters if you don’t read the specification, but just taking the “simple” approach could be an okay stopgap:

std::vector<VkPhysicalDevice> physicalDevices(1);
vkEnumeratePhysicalDevices(instance, &physicalDevices.size(), physicalDevices.data());
VkPhysicalDevice physicalDevice = physicalDevices[0];

however I’d personally go for the usual approach in case filtering the devices on some property becomes necessary at some point.

Once the compiler errors run out, the code one would end up with would look something like the following:

int main() {
    // Doing vulkan initialization in scopes like this is just a
    // preference to avoid naming fatigue.
    VkInstance instance;
    {
        VkInstanceCreateInfo createInfo{};
        vkCreateInstance(&createInfo, nullptr, &instance);
    }

    uint32_t count;
    vkEnumeratePhysicalDevices(instance, &count, nullptr);
    std::vector<VkPhysicalDevice> physicalDevices(count);
    vkEnumeratePhysicalDevices(instance, &count, physicalDevices.data());
    // Could do something more complex with a sort or std::find_if
    auto physicalDevice = physicalDevices[0];

    VkDevice device;
    {
        VkDeviceCreateInfo createInfo{};
        vkCreateDevice(physicalDevice, &createInfo, nullptr, &device);
    }

    VkCommandPool commandPool;
    {
        VkCommandPoolCreateInfo createInfo{};
        vkCreateCommandPool(device, &createInfo, nullptr, &commandPool);
    }

    VkCommandBuffer commandBuffer;
    {
        VkCommandBufferAllocateInfo allocInfo{};
        vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
    }

    vkCmdDraw(commandBuffer, 3, 1, 0, 0);
}

At this point the code compiles and runs, but depending on the driver, it might do nothing or more likely segfault at some point. This is where using the validation layers becomes important. Launching vulkan configurator and running the program after gets us a fair number of errors, for example the first one states that:

Validation Error: [ VUID-VkInstanceCreateInfo-sType-sType ] | MessageID = 0x68cd5cb6
vkCreateInstance(): pCreateInfo->sType must be VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO.
The Vulkan spec states: sType must be VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO (https://docs.vulkan.org/spec/latest/chapters/initialization.html#VUID-VkInstanceCreateInfo-sType-sType)

The majority of the validation errors are similar to this with easily actionable information and clear instructions. After fixing this and all similar sType errors, the next one that comes up is

Validation Error: [ VUID-VkDeviceCreateInfo-None-10778 ] | MessageID = 0xec09f29e
vkCreateDevice(): pCreateInfo->queueCreateInfoCount is 0 (This is only allowed if your device supports the maintenance9 feature).
The Vulkan spec states: If the maintenance9 feature is not supported, queueCreateInfoCount must be greater than 0 (https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#VUID-VkDeviceCreateInfo-None-10778)
Objects: 1
    [0] VkInstance 0x557b5517b3b0

This one is slightly harder to interpret, but once again, going with the easy option and setting queueCreateInfoCount to 1 is enough to progress to

vkCreateDevice(): pCreateInfo->pQueueCreateInfos is NULL.
The Vulkan spec states: If queueCreateInfoCount is not 0, pQueueCreateInfos must be a valid pointer to an array of queueCreateInfoCount valid VkDeviceQueueCreateInfo structures (https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#VUID-VkDeviceCreateInfo-pQueueCreateInfos-parameter)
Objects: 1
    [0] VkPhysicalDevice 0x557b5f6899b0

And this is what the process would look like for the next 30-40 changes, the first stopping point will be around where shaders need to be first created and compiled, then somewhere around setting up uniform bindings and that is about where I stopped when doing testing for this experiment.

This method isn’t enough to get to a triangle, but with just a bit of help even that is manageable.

Pitfalls

As I alluded to it, this process sadly isn’t foolprof and it has major pitfalls.

One of such pitfalls is when Vulkan defines two different sub-APIs for achieving the same thing. Even if the older one is deprecated (as is the case with renderpasses that were replaced by dynamic rendering), it will often still be considered the “default” path in the eyes of the validation layers, so if one were to follow the process religiously without external help, they would end up writing a Vulkan 1.0 application with none of the important comfort features from modern versions.

Another pitfall is when the API is (purposefully?) underspecified. An example I know for this is within dynamic rendering, if when setting up a graphics pipeline for dynamic rendering one happens to forget to include the VkRenderingCreateInfo struct, the default behaviour is to assume the developer doesn’t want to write to any outputs. This is of course not useful in most cases with the exception of rasterization based voxelization.

And lastly the biggest problem will be complex systems that validation layers are simply too small scale to explain. There’s no way to figure out how to enable features from more modern vulkan versions or how to present an image to the screen, not to mention the mess that is WSI.

However I still do think it’s worthwhile to consider this approach as the main tool in solving the various difficulties the API puts in front of us.