Back to Blog

The Vulkan Kitchen: A Visual Tour of the Graphics Pipeline

July 10, 20261h20 readBy Tom Salembien
VulkanComputer GraphicsGPURendering
Start here
Vocabulary, a quick reference

Terms the article uses, defined once for context. Skip it and refer back when a word is unfamiliar.

Attachment
A slot the pipeline draws into: an image view used as a color or depth target for a render.
BufferVkBuffer
A described linear region of GPU data such as vertices, indices, or uniforms. It holds no memory until you bind it to device memory.
Command bufferVkCommandBuffer
A recorded list of GPU commands (bind pipeline, bind descriptors, draw). Nothing runs until it is submitted to a queue.
Command poolVkCommandPool
An allocator that hands out command buffers, tied to one queue family.
Descriptor
A small reference to one resource (a buffer, an image view, or a sampler) that a shader is allowed to access.
Descriptor poolVkDescriptorPool
The allocator that hands out descriptor sets.
Descriptor setVkDescriptorSet
A tray of descriptors bound to a shader, organized by set and binding number. It points at resources; it holds no memory itself.
Device memoryVkDeviceMemory
An actual GPU (or host-visible) memory allocation. Buffers and images bind to a region of it.
Dynamic rendering
The modern path, core in Vulkan 1.3, where you name attachments inline at draw time, with no render pass or framebuffer object.
FenceVkFence
A GPU-to-CPU signal. The CPU waits on it to know work finished, so it can safely reuse a command buffer or resource.
FIFO
First In First Out. A present mode: images are shown in order on the display refresh beat (vsync), so no tearing. Mailbox and immediate are the other common modes.
FramebufferVkFramebuffer
In the classic path, the object that binds concrete image views to a render pass's attachments.
GLSL / HLSL
High-level shader languages. You compile them ahead of time to SPIR-V for Vulkan.
ImageVkImage
Pixel storage with a format and size (1D, 2D, or 3D). The same image is transitioned between layouts for whatever job it is doing now.
Image viewVkImageView
The serving order for an image: which format to read it as, and which mip levels and array layers to expose.
InstanceVkInstance
Your application's connection to the Vulkan library. It lists the GPUs and enables global extensions and validation layers.
Logical deviceVkDevice
Your working connection to one physical device, created with the features, extensions, and queues you ask for.
Mip levels
Precomputed smaller copies of an image (half, quarter, and so on) used when it is drawn small or far away, which reduces aliasing and helps cache use.
Physical deviceVkPhysicalDevice
A GPU present in the machine, with fixed capabilities, memory types, and queue families you inspect before choosing one.
PipelineVkPipeline
A baked configuration of the whole graphics process (fixed-function state plus shader stages), compiled up front so the GPU runs flat out.
Pool
An allocator that hands out objects of one kind. Vulkan has command pools (for command buffers) and descriptor pools (for descriptor sets).
QueueVkQueue
A lane you submit recorded command buffers to. Families are typed by capability (graphics, compute, transfer); some can also present.
Render passVkRenderPass
The classic path: attachments and subpasses declared up front, with each pipeline built for a specific render pass.
SemaphoreVkSemaphore
A GPU-to-GPU ordering primitive. It makes one batch of work wait for another on the device. This article uses the classic binary kind; Vulkan 1.2 added timeline semaphores, a more general counting variant.
Shader
A small program for one programmable pipeline stage. The vertex shader places each corner; the fragment shader colors each pixel.
Shader moduleVkShaderModule
A wrapper around a chunk of compiled SPIR-V; a pipeline stage points at a named entry point inside it.
SPIR-V
A standard bytecode the driver loads directly. You compile GLSL or HLSL to it ahead of time; the driver still turns it into native GPU code at load.
SurfaceVkSurfaceKHR
A platform-neutral handle to the window you present into. It bridges Vulkan and the operating system windowing.
SwapchainVkSwapchainKHR
A ring of images you render into and hand to the display in turn. Double or triple buffering keeps one on screen while another is drawn.
Uniform
Constant data the CPU supplies to shaders that the shaders cannot change, such as the model, view, and projection matrices. It is read-only in the shader and usually updated once per frame; modern code stores it in a uniform buffer (a UBO) so several shaders can share it with fewer commands.
Validation layers
Optional development-only layers that check your API usage and report mistakes, since core Vulkan does almost no error checking.

What Vulkan is, and why it is everywhere

Vulkan is a modern, cross-platform way to talk to a GPU. It is an open standard from the Khronos Group, the same people behind OpenGL, and it grew out of AMD's Mantle, handed to Khronos to seed one low-level standard for the whole industry. Version 1.0 arrived in 2016.

Its defining trait is that it is explicit. Where older APIs quietly decide things for you, Vulkan hands you the controls: more work, in exchange for very low CPU overhead, honest use of many threads at once, and one model that runs the same way on desktop, mobile, and console. Section 1 unpacks that trade. Shaders are compiled ahead of time to a standard bytecode called SPIR-V, so the driver loads them directly.

You are probably already running it. Vulkan is the primary low-level graphics API on Android, where modern versions of Unity and Unreal default to it. It powers Linux and Steam Deck gaming through Proton and DXVK, which translate DirectX to Vulkan. It runs on the Nintendo Switch, and on Apple hardware through MoltenVK, which maps Vulkan onto Metal. The map below shows the spread.

One API, almost everywhere

Vulkan

runs on

Androidprimary 3D API; Unity and Unreal default here
Steam Deck and LinuxProton and DXVK translate DirectX to Vulkan
Nintendo Switchsupports Vulkan natively
Applevia MoltenVK, mapped onto Metal
Windows and Linuxdesktop games and pro tools
The same explicit API runs across phones, handhelds, consoles, and desktops. You write to Vulkan once and the work follows the hardware, which is a large part of why it has spread so far.

1. Why Vulkan asks so much of you

Older graphics APIs like OpenGL work like ordering at a counter. You make one call, and the driver quietly decides almost everything else: which memory to use, when to synchronize, how to schedule the work. It is convenient, and for a long time it was enough.

Vulkan hands you the kitchen instead. You pick the device, allocate the memory, build the pipeline, record the commands, and manage the timing yourself. In return you get explicit, predictable, low-overhead control that works across platforms and across vendors, scales cleanly to many threads, and covers both graphics and compute. The price is verbosity: a lot of small, deliberate decisions.

Same triangle out, very different setup

frame

One call goes in and a finished frame comes out. Behind it the driver decides the memory, the synchronization, the scheduling, the pipeline state, and the command recording, all hidden from you.

2. Meeting the kitchen: instance, devices, queues

Four objects, kitchen words first

Instance
VkInstance

registering your restaurant with the city

Physical Device
VkPhysicalDevice

surveying the kitchens available

Logical Device
VkDevice

leasing and staffing one kitchen

Queue
VkQueue, from a queue family

the submission lanes to the cooks

Same kitchen, said four ways: register it, survey it, lease it, and pick the lanes you order through.

Setup is four objects, met in order. The instance registers your application with the Vulkan loader, the way you register a restaurant with the city before cooking. From it you list the physical devices, the GPUs in the machine, and inspect what each one offers before choosing one. You then open a logical device, your working connection to that GPU, enabling only the features and queues you need. The logical device is where you ask for those queues, features, and extensions; the memory types and hardware limits themselves belong to the physical device, and you allocate memory through the logical device using them. Queues are the lanes you submit work to, and queue families are typed by capability, graphics, compute, or transfer, with some families also able to present to a surface. Once submitted, the GPU runs that work on its own schedule, and the panel below lets you step through the whole setup.

A typical machine has more than one option, say an integrated GPU and a discrete one. You enumerate them, read each one's queue families and memory heaps, and score them by suitability: does it support the extensions you need, does it have a queue family that can both render and present, how much device-local memory does it have. For rendering you usually pick the discrete GPU. At device creation you then ask for the specific queues you want, for example one queue from the graphics family (often family 0, which also supports present), and Vulkan hands back a VkQueue handle you submit work to later.

Vulkan setup floor plan: instance, physical devices, logical device, and queue familiesAn outer restaurant frame appears, labeled as the Vulkan instance, with a validation-layers inspector badge in the corner.
InstanceVkInstance
Step 1 of 4Instance VkInstance

Register the restaurant

The instance is your application's handle to the Vulkan library. It switches on the validation layers and extensions you want, then announces your app to the loader.

Nothing has been drawn yet. This is the paperwork that lets you start cooking.

Step 2 of 4Physical Device VkPhysicalDevice

Survey the kitchens

With an instance in hand you enumerate the physical devices, the GPUs present in the machine. Each one advertises fixed capabilities, memory types, and queue families, and they are not interchangeable: an integrated GPU shares system memory and sips power, a discrete GPU has its own dedicated memory and far more throughput, and a virtual or software device is mainly for headless or CI work.

You read those off and pick by suitability. For rendering that is usually the discrete GPU, the best fit here for its dedicated memory and highest throughput.

Step 3 of 4Logical Device VkDevice

Lease and staff one kitchen

You pick a physical device and create a logical device on top of it, enabling only the features and queues you actually need.

That chosen kitchen is now staffed and active. The candidates you passed over fade away.

Step 4 of 4Queue VkQueue, from a queue family

Open the submission lanes

Queues are the lanes you hand recorded work to. Queue families are typed by capability (graphics, compute, transfer), and some families can also present to a surface.

Once you submit, the GPU runs that work on its own schedule, asynchronously.

3. Getting a plate to the customer: surface, swapchain, images

The surface, and why pictures come in sizes

the windowthe surfaceVulkan

The surface is the one bridge from the kitchen to the dining room full of customers. You present through it.

Surface
VkSurfaceKHR

the one bridge from the kitchen to the window you present into

mip 0mip 1mip 2mip 3

Mip levels are the same picture saved at half-sizes, so a small or distant object reads a small copy: less shimmer, kinder to the cache. The rotation of these images, one shown while the next is drawn, is the swapchain carousel just below.

Image View
VkImageView

the serving order for an image: which portion sizes (mip levels) a station may take

The surface is a platform-neutral handle to the window you draw into. It is the bridge between Vulkan and the operating system, the serving hatch between the kitchen and the dining room.

The swapchain is a small ring of images. While one image is on screen, you render into another, then hand it to the display in turn. The present mode sets the rhythm: FIFO is vsync-smooth (vsync is the display's fixed refresh beat), immediate can tear, and mailbox keeps latency low by always presenting the freshest image.

An image is the plate with the food on it: raw pixels. An image view is the serving order for that plate: which portion size to bring out (mip level), which copy from a stack of identical plates (array layer), and which kind of dish to read it as (format). Same plate, different orders.

The same image is also stored differently depending on its job right now, whether it is a render target, a sampled texture, or the thing being shown, and you transition it between those layouts explicitly. The kitchen re-plates the same dish for each step of service.

Swapchain, render then present

queued in order

Steady and vsync paced. Finished plates wait their turn in order, so the picture stays smooth and never tears.

Image vs image view
The image bytes, unchanged by any viewThe same 40 bytes, arranged for render target. A view never changes them; it only changes how a stage reads them.
VkImageraw bytes, unchanged
VkImageViewColor, full
Read through (image view) · RGBA, all mips

Same bytes underneath, shown on the left. The view on the right only changes how a stage reads them: the format, the channels, and which mip levels (sizes) it exposes. Here mip 0 is the full-size base level.

Re-plating (image layout) · stored differently

VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, render target. Packed for fast writes as a render target. This is the layout you draw into during a render pass. A pipeline barrier. You call it, it is not free.

Plate arranged for COLOR_ATTACHMENT_OPTIMAL, render target. on the hot plate, being cooked onto.

Same plate, different orders.

4. The recipe card: pipeline and dynamic rendering

First, what is the GPU even doing?

A GPU is a pixel factory. Geometry goes in, the pipeline works out which pixels each triangle covers and what color they should be, and a finished image comes out, all of them computed at once. Vulkan's job is running this factory. The pipeline you are about to meet is how you describe it.

We have the kitchen, the counter we serve across, and clean plates waiting to be filled: the device, the surface, and the images from the last three sections. What we have not written down yet is the method for cooking a single frame. That method is the pipeline. The graphics pipeline is a baked recipe card. It fixes the sequence of steps your geometry passes through, in order, before a single frame runs. A few of those steps are programmable, the shaders you write, and the rest are fixed-function steps you only configure. The visualizer below walks the stages in turn.

Vulkan bakes the whole thing into a pipeline object up front, so the GPU can run it at full speed. The trade-off is rigidity: most recipe changes mean baking a new pipeline, though a few knobs stay adjustable at draw time, like viewport and scissor as dynamic state, and small values passed through push constants or uniforms.

An attachment is just a slot you draw into: an image view used as a color or depth target for the pipeline's output. The two approaches differ only in how you describe those slots. The classic approach bakes a render pass and a framebuffer ahead of time, and every pipeline is built for a specific render pass (formally, any compatible one), so changing the attachments can mean baking a new pipeline. Its subpasses can chain on-chip, which saves bandwidth on tile-based mobile GPUs. Dynamic rendering, core in Vulkan 1.3, drops both objects: you name the attachments inline when you begin rendering, and a pipeline only needs to know the attachment formats, not a specific pass. Far less to set up, and the recommended path for new code. It traded away subpasses, which mattered for mobile bandwidth, until a later extension (core in Vulkan 1.4) brought those on-chip reads back.

That is the recipe card in theory. The conveyor below maps the five stages in order, then lets you flip between the two ways of naming your attachments, the classic render pass and dynamic rendering.

The pipeline, baked once and run flat out

Send a triangle through the graphics pipeline

Stage 1 of 5: Input assembly, fixed function, configured not coded. Groups vertices into primitives like triangles.

1 / 5

Input assembly. Groups vertices into primitives like triangles.

All five stages are baked into one VkPipeline up front, so the GPU runs the recipe flat out. Change the recipe and you bake a new pipeline.

What an attachment is, and how you describe it

Render pass vs dynamic rendering

An attachment is a slot you draw into, the image view used as a color or depth target. These two modes differ only in how you name that slot.

extra objects up front

Classic Vulkan bakes a render pass and a framebuffer up front to describe the attachments, and every pipeline is built for one specific render pass, so changing the attachments can mean baking a new pipeline. Its subpasses can chain on-chip, which saves memory bandwidth on tile-based mobile GPUs, but it is more boilerplate and all of it is fixed before you draw.

So far the recipe is on paper. The lab below cooks it: for illustration we run a single wooden door with a deliberately simplistic texture, the same door section 8 will serve, pushed through every stage one beat at a time. Beside each station sits the real working data, the raw vertex and index buffers, the three matrices the vertex shader multiplies, the clip counts, the rasterized pixels. At the clipping stage the door is placed deliberately part-way off the screen, so you can watch how a shape that only partly fits gets cut at the edge before it ever becomes pixels.

The recipe, run for real

The pipeline lab: send the door through every stage

Stage 1 of 8: The raw ingredients. This is all the GPU receives: a vertex buffer of raw floats (position, normal, UV per vertex) and an index buffer of 36 numbers. No triangles exist yet.

1 / 8

The raw ingredients. a bag of loose ingredients, nothing assembled. This is all the GPU receives: a vertex buffer of raw floats (position, normal, UV per vertex) and an index buffer of 36 numbers. No triangles exist yet.

That is the whole recipe, run one stage at a time on twelve triangles. As a closing bonus, the film below tells the same story on a game-art example: a dragon carried from loose vertices to assembled triangles, attributes, shading, and one finished frame. Each step is a simplistic AI-generated still standing in for that phase, and I morphed between the frames to make the transition. The dragon itself is borrowed from NVIDIA's RTX Mega Geometry write-up.

Bonus: Indicative example

5. The cooks you write: shaders and shader modules

A shader is the program for one programmable station. The vertex shader transforms each corner of your geometry, mapping the position your app supplied in a vertex buffer to where it lands on screen; it does not invent the geometry. The fragment shader decides the color of each pixel that the geometry covers.

You write shaders in a language like GLSL or HLSL and compile them ahead of time to SPIR-V, a standardized bytecode the driver loads directly. The driver still turns that SPIR-V into the GPU's own machine code when it loads the shader; what Vulkan removes is the driver parsing your GLSL or HLSL source, which is where vendor-specific bugs and surprises used to creep in. That is a real difference from OpenGL, which compiled shader source at runtime and left the result up to each driver.

Concretely, the build step runs a compiler (glslangValidator for GLSL, dxc for HLSL) over your shader source and writes .spv files you ship with the app. At startup each .spv becomes a VkShaderModule, and the pipeline's stage list points one stage at the vertex module's entry point and another at the fragment module's. The same SPIR-V runs on every vendor's driver; only the final compile to native code differs between them.

One shader module can hold several related shaders: each pipeline stage names the entry point it wants inside the module.

The cooks you write

What the two cooks do

Vertex shader
shader stage

The vertex shader transforms each corner it is handed, it does not invent them. Each corner arrives from a vertex buffer; drag the top one to stand in for a different input position and watch its gl_Position change, exactly what the one line of GLSL in the compile flow below computes.

Fragment shader
shader stage
uniform · pattern
// one value, set once per draw on the CPU
layout(set = 0, binding = 0) uniform Style {
  int pattern;
} u;

void main() {
  // every covered pixel reads the same u.pattern
  outColor = shade(u.pattern, uv);
}

A uniform is one value the CPU sets once per draw; every covered pixel reads the same one. Switch u.pattern and the program recolours every pixel: blends color across the x position.

The recipe booklet

How the recipe gets loaded

SPIR-V is compiled ahead of time and loaded straight into the driver. OpenGL took your GLSL and compiled it at runtime, so results varied by vendor. Here the heavy work happens once, at pipeline creation, never mid-frame.

6. Where the ingredients live: buffers, memory, descriptors

The pipeline and its shaders fix the steps. This section is about where the data those steps read actually lives. Beginners often blur several distinct ideas together, so it is worth separating them. A buffer is a described linear region of data: vertices, indices, or uniforms. On its own it is just a description.

Device memory is the actual allocation. You bind a buffer (or an image) to a region of memory yourself, choosing device-local memory for speed or host-visible memory when the CPU needs to write into it. An image is a grid of pixels, 1D, 2D, or 3D, and an image view is the serving order you read it through.

Shaders do not hold raw pointers to any of this. Instead a descriptor set is a tray of references that says this uniform here, that texture there. It holds no memory of its own: the bytes still live in the device memory you allocated and bound, and the tray only points at them. It is not the channel between one shader stage and the next either; handing a vertex shader's output to the fragment shader is the pipeline's job, not the descriptor set's. A descriptor set layout describes the slots on the tray, and a descriptor pool allocates the trays. Validation layers, meanwhile, are the optional health inspector that catches your mistakes while you develop.

In practice the order is concrete. You create a buffer with a usage and a size, ask Vulkan for its memory requirements, choose a matching memory type, allocate a block of device memory, and bind the buffer to an offset inside it. Many engines allocate one large block and suballocate every buffer and image within it. The descriptor set is then filled in with vkUpdateDescriptorSets: binding 0 might point at the uniform buffer with an offset and a range, and binding 1 at an image view plus a sampler. The set holds those references; the data still lives in the memory you allocated.

The tray, before any code

the cookno pockets, hands full
the tray
bin
shelf
the bin of ingredients
the pantry shelf
The cook never rummages the pantry. Someone hands them a tray that says: this bin, on that shelf. The tray is the descriptor set.

The same four ingredients, now with their Vulkan names.

How a shader reaches its data

The binding chain

Supporting castVkDescriptorSetLayoutthe tray's blueprintVkDescriptorPoolthe cupboard the trays come from

A shader holds no pointers of its own. The descriptor set is the tray of references that tells each binding where its resource actually lives. It holds no memory itself; the bytes still live in the device memory the buffer is bound to.

Four objects people mix up

Buffer, memory, image, image view

Hover or focus an item to see what it is and how it differs. Buffer and device memory are not the same object, and neither are image and image view.

7. Service: command buffers, queues, synchronization

Everything is built. Now we record one frame of work and hand it to the GPU. You do not call draw directly. You record the work into a command buffer, which is allocated from a command pool: bind the pipeline, bind the descriptors, draw. The recorded ticket is then submitted to a queue, and the GPU runs it asynchronously.

The command pool and its command buffers are created once and reused. Each frame you reset a buffer and record it again. Recording just means writing that list of GPU commands onto the ticket; nothing actually runs until you submit, and the queue is only touched at that submit and at present. What changes every frame is the recorded contents, not the objects themselves.

Because the CPU and GPU run on their own clocks, you order things explicitly. Semaphores coordinate GPU-to-GPU steps: wait for the image to be acquired before rendering into it, and wait for rendering before presenting. A fence reports back to the CPU when a frame is done, so it can safely reuse that frame's command buffer and resources.

Concretely, you keep a small number of frames in flight, usually two. Each gets its own command buffer, its own uniform buffer, and its own fence, so the CPU can record the next frame into a fresh set while the GPU still works on the current one. The per-frame dance is always the same: wait on this frame's fence, acquire a swapchain image (which signals an image-available semaphore), record and submit the command buffer (waiting on image-available, signaling render-finished and the fence), then present (waiting on render-finished).

You do not call draw directly

Record a ticket, then submit it

Command pool
VkCommandPoolthe rail of blank tickets
Command buffer
VkCommandBuffer
  1. 1bind pipeline
  2. 2bind descriptors
  3. 3draw
Submit to queue
vkQueueSubmithand the ticket to the pass
GPU runs it
asynchronously, on its own clock

The GPU runs the ticket on its own clock. When it is done the frame can be presented, which is what the timeline below works through.

The CPU and GPU run in parallel

One frame on each lane, in flight

CPU and GPU timeline with semaphore and fence synchronisationA two-lane timeline. The top lane is the CPU host; the bottom lane is the GPU device. Reading left to right: the acquire call asks the swapchain for an image and returns right away; the presentation engine, on the device lane, signals the image-acquired semaphore when one is actually free. The CPU records frame N into a command buffer and submits it. The GPU renders frame N, waiting first on image-acquired. While the GPU renders frame N, the CPU is already recording frame N+1, so both lanes are busy at once: this is frames in flight. When rendering finishes the GPU signals the render-finished semaphore, which present waits on before the image is shown. The same submit signals a fence; the CPU waits on that fence, an arrow from the device lane back up to the host lane, before it reuses frame N command buffer. Semaphores order work between GPU events on the device; the fence reports completion from the GPU back to the CPU.Image readyRecord NSubmitRender NRecord N+1SubmitPresent NReuse N
frame Nframe N+1semaphore, GPU to GPUfence, GPU to CPU

Step 1 of 7: Acquire an image. Ask the swapchain for an image. When the device has one ready to draw into, it signals the image-acquired semaphore. The call itself returns right away; the semaphore fires when the image is actually free.

1 / 7

Acquire an image. Ask the swapchain for an image. When the device has one ready to draw into, it signals the image-acquired semaphore. The call itself returns right away; the semaphore fires when the image is actually free.

Two kinds of synchronization

Semaphore vs fence

Semaphore
VkSemaphore

A GPU-to-GPU ordering primitive. It makes one batch wait for another, for example render waits for image-acquired and present waits for render.

GPU to GPU

Orders work on the device. Render waits for the image, and present waits for render. The CPU is never blocked by it.

Fence
VkFence

A GPU-to-CPU signal. The CPU waits on it to know work finished, so it can safely reuse a command buffer or resource.

GPU to CPU

The device tells the host a frame is done, so the CPU can safely recycle that frame command buffer and resources.

The CPU and the GPU run on their own clocks, so every ordering has to be spelled out. Keeping a couple of frames in flight is what keeps both of them busy at once.

8. The whole kitchen, one frame

Step back and look at the global shape of the work. Everything you have met so far belongs to one of two timelines. The objects from sections 2 through 7 are built once during setup and reused every frame: that is the long setup the tutorials are famous for. (Strictly, the swapchain and the views and framebuffers derived from it are rebuilt whenever the window resizes; everything else lasts the whole program.) What remains is a short loop that runs for every single frame, and that loop is shared between two workers on their own clocks. The CPU prepares and hands off work; the GPU executes it. The map below draws that whole workflow in one picture.

The global workflow, one map

Built once, then a loop between two clocks

Built once · before any frame

  • Instance + devices§2 · setup
  • Swapchain + views§3 · plates
  • Pipeline + shaders§4-5 · recipe
  • Buffers + descriptors§6 · pantry
  • Command pool§7 · tickets
built once, reused every frame

Every frame · the service loop

two workers, two clocks

  1. 1
    cpuAcquire an image

    ask the swapchain for a plate

  2. 2
    cpuRecord the commands

    bind the pipeline, then draw

  3. 3
    cpuSubmit

    the ticket goes to the queue

  4. 4
    gpuRun the pipeline

    cook into the image view

    waits on the image-acquired semaphore

  5. 5
    gpuPresent

    plate to the surface

    waits on the render-finished semaphore

  6. 6
    cpuThe fence reports back

    frame done, the ticket is safe to reuse, the next frame begins

    fence, a GPU-to-CPU signal

Build the kitchen once, then loop. The two lanes run on their own clocks: while the cooks (GPU) plate frame N, the front of house (CPU) is already recording frame N+1, the frames in flight you watched on the section 7 timeline. The two semaphores and the fence are the only places where the lanes wait for each other.

Read it lane by lane. Each frame, the CPU acquires an image from the swapchain, records a command buffer, and submits it to the graphics queue. The GPU picks the work up but waits on the image-acquired semaphore, so it never draws on a plate that is still being shown, then runs your baked pipeline into that image's view and signals render-finished. Present waits on that signal before putting the plate on screen, and the fence reports back to the CPU that this frame's command buffer and resources are safe to reuse. That last hand-off closes the loop, and because the two lanes overlap, the CPU is usually already recording the next frame while the GPU still cooks the current one.

That was the shape in the abstract. Now watch the whole kitchen built and run for real on a single door: every object you have met, what it concretely is here, and exactly when it is created once or used on every frame.

One real frame, end to end

Rendering a single door

geometry8 vertices, 36 indices (12 triangles)uniform1 mat4 MVP matrix (64 bytes)texture512 x 512 woodswapchain3 images, FIFOin flight2 framesqueuegraphics family 0
Build once
Per frame (the loop)
The whole cast, for this doorlit when used
Set up
GPU
the GPU
Instance
app registered, validation on
Physical Device
VkPhysicalDevice
Logical Device
VkDevice
Queue
VkQueue, from a queue family
Validation Layers
on in this dev build
The canvas
Surface
VkSurfaceKHR
Swapchain
VkSwapchainKHR
Image
VkImage
Image View
VkImageView
The recipe
Pipeline
VkPipeline
Shader
shader stage
Shader Module
VkShaderModule
Ingredients
Buffer
VkBuffer
Device Memory
VkDeviceMemory
Descriptor Set
VkDescriptorSet
Service
Command Pool
VkCommandPool
Command Buffer
VkCommandBuffer
Submit
vkQueueSubmit
Sync
Semaphore
VkSemaphore
Fence
VkFence
The doorgeometry

the door we will render

Device memoryVkDeviceMemory

Hover a cell to name the resource. The whole block is one allocation, suballocated by offset.

Command bufferVkCommandBuffer
Recorded once you reach the record step. Nothing here yet.
Graphics queuesubmit + sync
CPUacquireCPUrecordCPUsubmitGPUrun pipelineGPUpresentCPUfence wait

The submit signals render-finished and a fence; the CPU waits on the fence before reusing this frame's command buffer and uniform buffer.

Step 1 of 12: Register the app. Create the instance and switch on the validation layers. This registers the app with the Vulkan loader. One VkInstance, made once.

1 / 12

Register the app. Create the instance and switch on the validation layers. This registers the app with the Vulkan loader. One VkInstance, made once.

The loop, running

Many frames: images cycle, two stay in flight

frame 0
swapchain, 3 images
surface, on screen
presented each frame
two frames in flight
Slot A
CPU recording this frame
Command bufferUniform bufferDescriptor setFence
Slot B
GPU finishing its frame
Command bufferUniform bufferDescriptor setFence
graphics queue
cmd buffer (frame 0)
GPU

Command buffer: Re-recorded each frame with this frame's draw: bind the pipeline, the vertex and index buffers and the descriptor set, then drawIndexed(36).

Uniform buffer: Holds this frame's changing values, here the door's model-view-projection matrix (its angle this frame). The CPU writes it before recording.

Descriptor set: Points the shaders at this frame's resources: binding 0 to this uniform buffer, binding 1 to the wood texture.

Fence: Raised by the GPU when this frame finishes. The CPU waits on it before reusing this slot, so the next frame never overwrites work still in use.

Three images cycle while two frames stay in flight. Each slot owns its own copy of the four resources above, so while the GPU renders frame N from one slot, the CPU is already filling the other slot for frame N+1. That overlap is what keeps both processors busy.

That is one whole frame, start to finish. If it helps to see it differently, the panel below gathers every object into one connected graph, each where its section introduced it, with the real relationships drawn as edges and the same frame walked across it.

Another way to see itThe whole framework as one connected graph

Every object, one board

The whole kitchen as one graph

How to read it: each node is one object from the article, placed in the zone of the section that introduced it (setup left, presentation right, pipeline centre, pantry lower left, service lower centre, synchronisation lower right). The lines are the real relationships, which object creates, feeds, or signals which, and the dashed arc on the right is the loop closing into the next frame.

The whole Vulkan kitchen: every object placed on one board, with the relationships that connect them.The GPU is the whole kitchen, drawn as the outer frame. Inside it, the setup objects sit on the left: instance, physical device, logical device, and queue. The pipeline runs down the centre with its shader module above and shader stage below. Presentation objects are on the right: surface, swapchain, image, and image view. The pantry is in the lower left with buffer, device memory, and descriptor set. Command recording and submission run across the lower centre with command pool, command buffer, and submit. Synchronisation primitives, fence and semaphore, sit in the lower right, and validation layers oversee from the top. Edges link related objects, for example instance to physical device to logical device, logical device to swapchain to image to image view, buffer bound to device memory, and command buffer submitted to the queue, which signals a fence and semaphores.
SwapchainImageSemaphore

One frame, start to finish

Walk a single frame through the kitchen

These are the every-frame beats from the workflow map, traced on the full board. A solid comet sends a signal forward; a hollow comet is a step waiting on a signal before it may run.

Step 1 of 5: Acquire an image. Ask the swapchain for the next image. It signals the image-acquired semaphore once that plate is free to draw on.

1 / 5

Acquire an image. Ask the swapchain for the next image. It signals the image-acquired semaphore once that plate is free to draw on.

Lights upSwapchainImageSemaphore

Quick reference

Every object and its one job

  • GPUthe GPU
    the whole kitchen
  • InstanceVkInstance
    registering your restaurant with the city
  • Physical DeviceVkPhysicalDevice
    surveying the kitchens available
  • Logical DeviceVkDevice
    leasing and staffing one kitchen
  • QueueVkQueue, from a queue family
    the submission lanes to the cooks
  • Shader ModuleVkShaderModule
    the loaded recipe booklet
  • PipelineVkPipeline
    the recipe card
  • Shadershader stage
    a cook at one station
  • SurfaceVkSurfaceKHR
    the serving hatch to the dining room
  • SwapchainVkSwapchainKHR
    the carousel of plates
  • ImageVkImage
    the plate itself
  • Image ViewVkImageView
    the serving order for the plate
  • BufferVkBuffer
    a bin of raw ingredients
  • Device MemoryVkDeviceMemory
    the pantry shelf the bin sits on
  • Descriptor SetVkDescriptorSet
    a tray of labeled references handed to a station
  • Validation Layersvalidation layers
    the health inspector
  • Command PoolVkCommandPool
    the rail of blank order tickets
  • Command BufferVkCommandBuffer
    one order ticket of recorded steps
  • SubmitvkQueueSubmit
    handing the ticket to the pass
  • FenceVkFence
    the buzzer that says the food is ready
  • SemaphoreVkSemaphore
    the hand-off between stations

9. What's next

This was the mental model: the cast of objects, what each one is for, and how they cooperate to cook and serve one frame. None of it required writing a line of code, and that was the point.

If you keep a single picture, keep the workflow map. Build the kitchen once: instance, devices, swapchain, pipeline, buffers, command pool. Then loop forever: acquire, record, submit, present, with two semaphores ordering the GPU's work and a fence reporting back to the CPU. Every Vulkan program you will ever read, however long, is that map with the details filled in.

Do you need Vulkan? Not always. If you just want triangles on screen, OpenGL, WebGPU, or a game engine will get you there with far less ceremony. Vulkan earns its keep when you need the control: predictable CPU cost, real multithreaded recording, one explicit model across platforms. And none of this machinery is graphics-only: the same queues, command buffers, and descriptors drive compute dispatches too.

The next article actually writes the code. It walks from instance creation all the way through the draw call, and ends with a triangle on screen. With the kitchen already mapped, the code reads as a checklist rather than a mystery.

Get in touch

We hope you enjoyed this article. If you have questions, found a bug, or just want to chat about AI and engineering, we'd love to hear from you.

Future articles?

We're planning more deep dives into:
• Graph Neural Networks
• Transformer Architecture
• System Design for ML

Get them in your inbox

Send feedback

Share this article