# ==Vulkan==, learning log <p class="doc-sub">// status: seedling</p> Vulkan is OpenGL's antithesis: no single global state machine, far less implicit synchronisation, explicit memory management, and SPIR-V shader modules instead of runtime GLSL strings. You write far more setup before the first triangle. In return you get explicit control, multi-threaded command recording, and fewer hidden decisions in performance-critical paths. Compare with [[OpenGL - learning log]]. ## The big mental shift OpenGL: "please draw something, driver, figure it out." Vulkan: "I'm telling you what hardware to use, which memory to allocate, when to flush caches, and when to wait for the previous frame to finish." The driver still does substantial work, but the application makes many decisions that OpenGL traditionally inferred. Concretely, you become responsible for: - Picking a physical device and queue families. - Allocating GPU memory and sub-allocating from it. - Layout transitions of images between uses. - Synchronisation between queue submissions and within them. - Tracking per-frame-in-flight resources. - Compiling shaders to SPIR-V ahead of time. This sounds terrible. It becomes satisfying once the setup is done because much less is hidden. ## The object zoo In rough order of creation for a "hello triangle": - **Instance** — the Vulkan loader handle; holds layers and extensions. - **Physical device** — a specific GPU visible to the system. - **Logical device** — your opened session with it; creates everything else. - **Queue** — where commands are submitted. Graphics, compute, transfer, present — not all on the same queue family on all hardware. - **Surface** — platform-specific window integration (via `VK_KHR_surface` + the OS-specific extension). - **Swapchain** — the set of images you render into and present. - **Render pass / dynamic rendering** — declares the attachments and subpasses used by a pipeline. Dynamic rendering (core in 1.3) removes most of this ceremony. - **Pipeline** — a baked, immutable state object: shaders, vertex input, blending, depth, the lot. Creating one is expensive; cache and reuse them. - **Descriptor sets** — how you bind resources (buffers, images) to shaders. Allocated from a descriptor pool, organised by set layouts. - **Command buffer** — recorded once, submitted N times. The thing that actually carries work to the GPU. - **Semaphores / fences / barriers** — synchronisation primitives (more below). ## Memory No more `glBufferData(... GL_STATIC_DRAW)`. You: 1. `vkCreateBuffer` / `vkCreateImage` — get a handle with no backing storage. 2. `vkGetBufferMemoryRequirements` — ask what size/alignment it needs. 3. `vkAllocateMemory` from a heap with the right properties (`DEVICE_LOCAL`, `HOST_VISIBLE`, `HOST_COHERENT`…). 4. `vkBindBufferMemory` — attach. In practice, applications rarely allocate device memory independently for every small resource. Use the [Vulkan Memory Allocator (VMA)](https://gpuopen.com/vulkan-memory-allocator/) — AMD's open-source sub-allocator — or build an allocator suited to the engine. VMA is a strong default, not an API requirement. Memory properties you'll see: - `DEVICE_LOCAL` — on the GPU, fast for GPU access, invisible to CPU. Use for most resources. - `HOST_VISIBLE | HOST_COHERENT` — mappable, coherent without manual flushes. Staging buffers. - `HOST_VISIBLE | HOST_CACHED` — mappable + CPU-cache-friendly readback. Needs `vkInvalidate...` / `vkFlush...`. - Unified-memory GPUs (integrated, Apple) expose `DEVICE_LOCAL | HOST_VISIBLE` — fast path on those. ## Synchronisation The single hardest thing about Vulkan. - **Semaphores** — GPU↔GPU synchronisation across queue submits. Present signals one, the next submit waits on it. - **Fences** — GPU→CPU; the CPU waits for a fence to know a submission finished. Used to reuse per-frame resources. - **Pipeline barriers** — in-queue ordering and cache flushes. Express "after these stages, before those stages, also transition this image's layout". - **Events / timeline semaphores** — additional synchronization tools (`VK_KHR_timeline_semaphore` is core in 1.2). Timeline semaphores simplify many GPU and host dependency chains, while presentation and some APIs still require binary semaphores. Barriers are the knife's edge. Over-synchronise and performance dies. Under-synchronise and you get undefined behaviour that looks right on your machine and flickers on every other GPU. `VK_LAYER_KHRONOS_validation`, especially synchronization validation, catches many common mistakes. Keep it enabled in development and still test across vendors. ## Shaders and pipelines Shaders are ingested as **SPIR-V** — a stable bytecode. The common flow is GLSL → `glslang` → SPIR-V, or HLSL → DXC → SPIR-V. Both are fine. HLSL has nicer ergonomics for modern features; GLSL is the Vulkan-native dialect. A graphics pipeline bakes: - All shader stages (SPIR-V modules + entry points + specialisation constants). - Vertex input layout (or none, if you pull from buffers manually). - Rasterizer state, depth/stencil, blend state. - Viewport/scissor (or mark as dynamic). - The render pass / rendering info the pipeline is compatible with. This is a _lot_ of state baked into an object. Pipeline caches can reuse implementation-specific compilation data; newer extensions such as graphics pipeline libraries and shader objects offer different ways to split or relax pipeline construction. Profile creation and cache behavior on the target drivers. ## Descriptor sets and bindless Descriptor sets bind resources to shaders. Three main approaches today: - **Traditional descriptor sets** — allocate per frame, update explicitly. Verbose but explicit. - **Push descriptors** (`VK_KHR_push_descriptor`) — small, immediate, no pool churn. Great for per-draw bindings. - **Descriptor indexing / bindless** (`VK_EXT_descriptor_indexing`, core in 1.2) — one giant descriptor set with thousands of textures, shaders index into it. How modern renderers avoid descriptor-update overhead entirely. Descriptor indexing is a common foundation for bindless-style renderers: keep large resource arrays and select entries with shader-visible indices. ## Frames in flight The canonical double/triple buffering pattern: ``` frameIndex = (frameIndex + 1) % FRAMES_IN_FLIGHT; vkWaitForFences(device, 1, &inFlight[frameIndex], VK_TRUE, UINT64_MAX); vkAcquireNextImageKHR(...); // signals imageAvailable[frameIndex] // record command buffer for frameIndex vkQueueSubmit(... waitSemaphores: imageAvailable[frameIndex], signalSemaphores: renderFinished[frameIndex], fence: inFlight[frameIndex]); vkQueuePresentKHR(... waitSemaphores: renderFinished[frameIndex]); ``` Each in-flight frame owns its own command buffer, its own descriptor resources, its own uniform buffer suballocation. The fence prevents the CPU from getting too far ahead of the GPU. ## Things that tripped me up - **Layouts** — images are in some layout (`UNDEFINED`, `COLOR_ATTACHMENT_OPTIMAL`, `SHADER_READ_ONLY_OPTIMAL`, `PRESENT_SRC_KHR`…). You transition them with barriers. Forget one and you get garbled output or a validation error novel. - **Y axis and clip space** — Vulkan's clip-space Y points down vs OpenGL's up. Either flip the viewport with a negative height (`VK_KHR_maintenance1`, core since 1.1) or flip in the shader. - **Negative viewport height is a feature, not a bug** — it's how you get OpenGL-style Y-up without touching the shader. - **Device loss happens** — on mobile and under TDR on Windows. You need a recovery path or at least a clean crash. - **`vkCmdPipelineBarrier2`** — the new barrier API (`VK_KHR_synchronization2`, core in 1.3) is clearer and more expressive. New code should use it. Old tutorials use the legacy one. - **Don't allocate inside the frame** — command pools, descriptor sets, memory. Allocate up front or in background threads; reuse ring-buffer style. - **Tooling is worth the setup** — validation layers, RenderDoc, Nsight, AMD Radeon GPU Profiler. The explicitness that makes Vulkan painful also makes the tooling unusually informative. ## A sensible starter stack - **GLFW** or **SDL2** — window + surface creation. - **VMA** — memory. - **volk** — a convenient Vulkan meta-loader for device function pointers. - **glslang** / **DXC** — shader compilation. - **Dear ImGui** + `imgui_impl_vulkan` — debug UI. - **Vulkan SDK** — includes validation layers, `vkconfig`, debug utils. - **RenderDoc** / **Nsight Graphics** — frame debuggers. ## References - [Khronos Vulkan](https://www.khronos.org/vulkan/) - [vulkan-tutorial.com](https://vulkan-tutorial.com/) — best on-ramp, slightly out of date on sync - [Vulkan Guide](https://vkguide.dev/) — modern, uses dynamic rendering and sync2 - _Writing an efficient Vulkan renderer_ — Zeux's blog post, the one to read after the hello triangle - [Sascha Willems' samples](https://github.com/SaschaWillems/Vulkan) --- Back to [[Notes/Index|Notes]] · see also [[OpenGL - learning log]] · [[Compute shaders]] · [[GPU-Driven Visibility for Large Worlds]] · [[Debugging Graphics Without Guessing]]