I use this map when a bug moves from a Vulkan call into Mesa, the kernel, or generated GPU code.

The compressed version

For the questions I usually care about, I reduce the stack to three pieces: a kernel driver, a userspace API driver, and a shader compiler.

Vulkan application
       │ SPIR-V + pipeline state
       ▼
Mesa Vulkan driver
       ├─ SPIR-V → NIR → target backend → GPU machine code
       └─ commands and resources → DRM kernel driver → GPU

Kernel and userspace

The kernel driver owns work that requires kernel authority: memory management, command submission from multiple processes, display, power management, and recovery after a hang. On Linux these drivers live in DRM and differ by hardware family.

A Mesa userspace driver implements an API such as Vulkan or OpenGL. It consumes application state, builds pipelines and command buffers, manages resources, and eventually talks to the matching kernel driver through ioctls when work is submitted. The API-facing driver and kernel driver are separate pieces even when they support the same GPU.

The compiler path

Vulkan applications normally provide shaders as SPIR-V. Mesa drivers translate SPIR-V to NIR, run shared and driver-specific lowerings and optimizations, then hand the result to a target backend. That backend selects instructions, allocates registers, schedules instructions, and emits machine code for the selected GPU.

The shared IR is important, but the end of the pipeline is hardware-specific. GPU generations differ in instructions, encodings, hazards, register files, and execution details, so machine code for one target is not a portable GPU binary.

Why compilation happens at runtime

With a microcontroller I can cross-compile for a fixed target and flash the result. Vulkan keeps SPIR-V portable and lets the driver compile for the GPU and driver implementation actually present on the machine.

Applications can build pipelines ahead of their first draw, reuse pipeline caches, or compile work in the background. When compilation still lands on a latency-sensitive path, users see the familiar shader or pipeline-compilation stutter. “The first time a shader is seen” is a useful approximation, not the whole lifecycle.

Where I stop the map

I leave out display, WSI, firmware, synchronization, memory residency, and shader caches here. They matter, but they are not needed to locate the boundary between an API driver, its compiler, and the kernel.