- name
- vulkan
- description
- Expert low-level GPU rendering with Vulkan - instance setup, swapchain management, render passes, command buffers, synchronization, memory management, compute shaders, and ray tracing.
# Vulkan Deep Engineering Guide
Vulkan is a low-overhead, explicit GPU API. It gives fine control over every GPU step: no hidden state, no driver magic, no automatic synchronization. Performance is yours to manage - and so is every bug that comes with it.
## 1. Vulkan & The Explicit Model
### 1.1 Explicit vs Implicit (OpenGL/WebGL)
| Aspect | OpenGL | Vulkan |
|--------|--------|--------|
| State | Hidden global state machine | Everything explicit (pipeline state objects) |
| Threads | Single-threaded context | Record command buffers on any thread |
| Synchronization | Driver syncs | **You** place barriers/fences/semaphores |
| Memory | Driver allocates VRAM | **You** allocate `VkDeviceMemory` |
| Validation | Undefined behavior | Validation layers in debug builds |
| Errors | Some checks | `vkGetDeviceProcAddr` + validation callbacks |
### 1.2 The GPU Pipeline (Conceptual)
```
CommandBuffer (CPU records) -> Submit to Queue -> GPU executes -> Swapchain present
```
### 1.3 When to Use Vulkan
- Building a custom engine or renderer where OpenGL/WebGL abstraction is too slow.
- You need access to compute shaders, mesh shaders, ray tracing, async compute.
- Target PC/console/mobile where very low overhead matters.
- Portability across Windows/Linux/Android/macOS via the same API.
## 2. Initialization: Instance -> Device -> Queue
### 2.1 Instance
```cpp
VkInstanceCreateInfo ci{};
ci.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
ci.enabledLayerCount = (uint32_t)debugLayers.size(); // "VK_LAYER_KHRONOS_validation"
ci.ppEnabledLayerNames = debugLayers.data();
ci.enabledExtensionCount = (uint32_t)instanceExtensions.size();
ci.ppEnabledExtensionNames = instanceExtensions.data();
VkInstance instance;
vkCreateInstance(&ci, nullptr, &instance);
```
Required instance extensions: `VK_KHR_surface` (swapchain present), `VK_EXT_debug_utils` (debug callbacks). For windowing you also need the platform surface extension (`VK_KHR_win32_surface`, `VK_KHR_xcb_surface`, etc.).
### 2.2 Physical Device (GPU Selection)
```cpp
uint32_t count = 0;
vkEnumeratePhysicalDevices(instance, &count, nullptr);
std::vector<VkPhysicalDevice> devices(count);
vkEnumeratePhysicalDevices(instance, &count, devices.data());
for (VkPhysicalDevice dev : devices) {
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(dev, &props);
// score by: VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, queue family support,
// feature support (geometryShader, tessellationShader, rayTracingPipeline)
}
```
### 2.3 Queue Families
A GPU exposes queue families (graphics, compute, transfer, present):
```cpp
uint32_t familyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physDev, &familyCount, nullptr);
std::vector<VkQueueFamilyProperties> families(familyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physDev, &familyCount, families.data());
int gfxFamily = -1, computeFamily = -1, presentFamily = -1;
for (uint32_t i = 0; i < families.size(); ++i) {
if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) gfxFamily = i;
if (families[i].queueFlags & VK_QUEUE_COMPUTE_BIT) computeFamily = i;
// presentFamily: vkGetPhysicalDeviceSurfaceSupportKHR(dev, i, surface, &supported)
}
```
### 2.4 Logical Device
```cpp
VkDeviceCreateInfo dci{};
dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
dci.queueCreateInfoCount = 1;
dci.pQueueCreateInfos = &queueCreateInfo; // { familyIndex = gfxFamily, queueCount = 1 }
dci.enabledExtensionCount = (uint32_t)deviceExtensions.size();
dci.ppEnabledExtensionNames = deviceExtensions.data();
// deviceExtensions: VK_KHR_swapchain, optional: VK_KHR_ray_tracing_pipeline,
// VK_EXT_descriptor_indexing, VK_KHR_buffer_device_address
VkDevice device;
vkCreateDevice(physDev, &dci, nullptr, &device);
vkGetDeviceQueue(device, gfxFamily, 0, &graphicsQueue);
```
### 2.5 Validation Layers
```cpp
if (enableValidation) {
layers.emplace_back("VK_LAYER_KHRONOS_validation");
VkDebugUtilsMessengerCreateInfoEXT msgCI{};
msgCI.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT;
msgCI.pfnUserCallback = debugCallback;
// set on instance create AND device create via pNext chain
}
```
## 3. The Swapchain
### 3.1 Create
```cpp
VkSwapchainCreateInfoKHR sci{};
sci.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
sci.surface = surface;
sci.minImageCount = 2; // double-buffered; 3 for triple buffering
sci.imageFormat = VK_FORMAT_B8G8R8A8_SRGB; // common window format
sci.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
sci.imageExtent = { windowWidth, windowHeight };
sci.imageArrayLayers = 1;
sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
sci.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; // vsync
sci.clipped = VK_TRUE;
VkSwapchainKHR swapchain;
vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain);
// Get images: vkGetSwapchainImagesKHR(device, swapchain, &count, nullptr);
```
### 3.2 Present Modes
| Mode | Description | Use case |
|------|-------------|----------|
| `FIFO` | Queue-based vsync (tears nothing) | Default, console-like |
| `MAILBOX` | Triple-buffer vsync, lowest latency | Fast games, high-refresh |
| `IMMEDIATE` | No vsync, tear possible | Benchmarking, VR head-locked |
| `FIFO_RELAXED` | Late swap shows last frame | Rarely used |
### 3.3 Image Acquire -> Render -> Present Flow
```cpp
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, // wait forever
imageAvailableSemaphore, // signalled when image ready
VK_NULL_HANDLE, &imageIndex);
// record + submit command buffer that renders to this image
vkQueueSubmit(graphicsQueue, 1, &submitInfo, frameFence);
vkQueuePresentKHR(graphicsQueue, &presentInfo); // { swapchain, imageIndex }
```
### 3.4 Swapchain Recreation
Handle window resize: recreate swapchain (images, framebuffers, at minimum image views). Query `VK_ERROR_OUT_OF_DATE_KHR` from present/acquire and recreate.
## 4. Render Passes & Framebuffers
### 4.1 Render Pass
```cpp
VkAttachmentDescription color{};
color.format = swapchainFormat;
color.samples = VK_SAMPLE_COUNT_1_BIT;
color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference colorRef{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
VkRenderPassCreateInfo rpci{};
rpci.attachmentCount = 1;
rpci.pAttachments = &color;
rpci.subpassCount = 1;
rpci.pSubpasses = &subpass;
VkRenderPass renderPass;
vkCreateRenderPass(device, &rpci, nullptr, &renderPass);
```
### 4.2 Subpasses & Input Attachments
A render pass can hold multiple subpasses. Output of one subpass can be consumed by the next as an input attachment — this avoids intermediate memory round-trips:
```
Subpass 0: draw geometry -> GBuffer (Color + Depth)
Subpass 1: lighting pass reads GBuffer via input attachment
```
```cpp
VkAccessFlags srcMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkAccessFlags dstMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
// The driver handles implicit layout transitions between subpasses.
```
### 4.3 Framebuffer (one per swapchain image)
```cpp
VkFramebufferCreateInfo fci{};
fci.renderPass = renderPass;
fci.attachmentCount = 1;
fci.pAttachments = &swapchainImageView[i]; // image view for image i
fci.width = windowWidth; fci.height = windowHeight; fci.layers = 1;
vkCreateFramebuffer(device, &fci, nullptr, &framebuffers[i]);
```
## 5. Pipelines
### 5.1 The Graphics Pipeline - Monolithic Bake
Vulkan pre-bakes all state into a `VkPipeline`:
- Shader stages (VS + FS, optionally TS/GS/MS)
- Vertex input layout
- Input assembly
- Viewport + Scissor
- Rasterization
- Multisampling
- Depth/stencil
- Color blending
- Dynamic state (viewport/scissor if dynamic)
```cpp
VkGraphicsPipelineCreateInfo gpci{};
gpci.stageCount = 2; // VS + FS
gpci.pStages = shaderStages; // VkPipelineShaderStageCreateInfo
gpci.pVertexInputState = &vertexInput;
gpci.pInputAssemblyState = &inputAssembly;
gpci.pViewportState = &viewportState;
gpci.pRasterizationState = &rasterState;
gpci.pMultisampleState = &msState;
gpci.pDepthStencilState = &depthState;
gpci.pColorBlendState = &blendState;
gpci.layout = pipelineLayout;
gpci.renderPass = renderPass;
gpci.subpass = 0;
vkCreateGraphicsPipelines(device, pool, 1, &gpci, nullptr, &pipeline);
```
**Key insight**: pipelines are immutable once created, so switching between a small set of pre-baked pipelines is cheap. Cache pipelines per unique state set.
### 5.2 Pipeline Layout & Descriptor Set Layout
```cpp
VkDescriptorSetLayoutBinding binding{};
binding.binding = 0;
binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
binding.descriptorCount = 1;
VkDescriptorSetLayoutCreateInfo dslci{};
dslci.bindingCount = 1;
dslci.pBindings = &binding;
vkCreateDescriptorSetLayout(device, &dslci, nullptr, &dslSetLayout);
VkPipelineLayoutCreateInfo plci{};
plci.setLayoutCount = 1;
plci.pSetLayouts = &dslSetLayout;
// push constants: plci.pushConstantRangeCount = 1; plci.pPushConstantRanges = &pcRange;
vkCreatePipelineLayout(device, &plci, nullptr, &pipelineLayout);
```
### 5.3 Dynamic State
Mark viewport/scissor as `VK_DYNAMIC_STATE_VIEWPORT` + `VK_DYNAMIC_STATE_SCISSOR` to change them per draw without a new pipeline.
## 6. Command Buffers & Recording
```cpp
VkCommandPoolCreateInfo poolCI{};
poolCI.queueFamilyIndex = gfxFamily;
poolCI.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
vkCreateCommandPool(device, &poolCI, nullptr, &cmdPool);
VkCommandBufferAllocateInfo cbai{};
cbai.commandPool = cmdPool;
cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cbai.commandBufferCount = 1;
vkAllocateCommandBuffers(device, &cbai, &cmdBuffer);
```
Record per frame:
```cpp
vkResetCommandBuffer(cmdBuffer, 0);
VkCommandBufferBeginInfo begin{};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(cmdBuffer, &begin);
VkRenderPassBeginInfo rpbi{};
rpbi.renderPass = renderPass;
rpbi.framebuffer = framebuffers[imageIndex];
rpbi.renderArea.extent = { width, height };
VkClearValue clear = {{{0.1f, 0.2f, 0.3f, 1.f}}};
rpbi.clearValueCount = 1; rpbi.pClearValues = &clear;
vkCmdBeginRenderPass(cmdBuffer, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdBindDescriptorSets(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &ds, 0, nullptr);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmdBuffer, 0, 1, &vertexBuffer, &offset);
vkCmdBindIndexBuffer(cmdBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmdBuffer, indexCount, 1, 0, 0, 0);
vkCmdEndRenderPass(cmdBuffer);
vkEndCommandBuffer(cmdBuffer);
```
### Why Command Buffers Are Powerful
- Recorded once, submitted many times (cached geometry draws).
- Recorded from multiple threads and submitted to one queue.
- Secondary command buffers nest inside render passes to render in parallel chunks.
## 7. Synchronization - The Hardest Part
Vulkan never syncs for you. Five primitives:
| Primitive | Scope | Purpose |
|-----------|-------|---------|
| **Fence** | CPU<->GPU | CPU waits for GPU work completion |
| **Semaphore** | GPU->GPU | Order queue submissions on different queues |
| **Timeline Semaphore** | GPU->GPU | Ordered signal/wait across frames (extension) |
| **Event** | In-batch | GPU-internal stage-to-stage wait |
| **Pipeline Barrier** | Memory | Image/buffer layout + flush/invalidate |
### 7.1 The Classic Frame Skeleton
```cpp
// Frame loop (double buffering)
vkAcquireNextImageKHR(device, swapchain, UINT64_MAX,
imageAvailableSem[idx], VK_NULL_HANDLE, &idx);
vkWaitForFences(device, 1, &frameFences[idx], VK_TRUE, UINT64_MAX);
recordCommandBuffer(frameBuffers[idx], imageAvailableSem[idx], renderFinishedSem[idx]);
VkSubmitInfo si{};
VkSemaphore wait[] = { imageAvailableSem[idx] }; // wait for acquire
VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
si.waitSemaphoreCount = 1; si.pWaitSemaphores = wait; si.pWaitDstStageMask = &waitStage;
VkSemaphore signal[] = { renderFinishedSem[idx] };
si.signalSemaphoreCount = 1; si.pSignalSemaphores = signal;
vkResetFences(device, 1, &frameFences[idx]);
vkQueueSubmit(graphicsQueue, 1, &si, frameFences[idx]);
VkPresentInfoKHR pi{};
pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = signal;
pi.swapchainCount = 1; pi.pSwapchains = &swapchain; pi.pImageIndices = &idx;
View on GitHub