executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
vulkanExpert low-level GPU rendering with Vulkan - instance setup, swapchain management, render passes, command buffers, synchronization, memory management, compute shaders, and ray tracing.
Independent GitHub snapshot · Automated classification · Original documentation governs
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.
| 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 |
CommandBuffer (CPU records) -> Submit to Queue -> GPU executes -> Swapchain present
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.).
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)
}
A GPU exposes queue families (graphics, compute, transfer, present):
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)
}
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);
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
}
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);
| 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 |
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 }
Handle window resize: recreate swapchain (images, framebuffers, at minimum image views). Query VK_ERROR_OUT_OF_DATE_KHR from present/acquire and recreate.
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);
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
VkAccessFlags srcMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkAccessFlags dstMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
// The driver handles implicit layout transitions between subpasses.
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]);
Vulkan pre-bakes all state into a VkPipeline:
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.
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);
Mark viewport/scissor as VK_DYNAMIC_STATE_VIEWPORT + VK_DYNAMIC_STATE_SCISSOR to change them per draw without a new pipeline.
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:
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);
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 |
// 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;
vkQueuePresentKHR(graphicsQueue, &pi);
Needed to transition image layouts between usages:
VkImageMemoryBarrier barrier{};
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
vkCmdPipelineBarrier(cmdBuffer,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, // before any work
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, // before render
0, 0, nullptr, 0, nullptr, 1, &barrier);
VkMemoryRequirements req;
vkGetBufferMemoryRequirements(device, buffer, &req);
VkMemoryAllocateInfo mai{};
mai.allocationSize = req.size;
mai.memoryTypeIndex = findMemoryType(req.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VkDeviceMemory mem;
vkAllocateMemory(device, &mai, nullptr, &mem);
vkBindBufferMemory(device, buffer, mem, 0);
| Flag | Location | Usage |
|---|---|---|
DEVICE_LOCAL | VRAM | GPU-only buffers/textures |
| `HOST_VISIBLE | HOST_COHERENT` | System RAM mapped |
| `HOST_VISIBLE | HOST_COHERENT | DEVICE_LOCAL` |
memcpy CPU data into mapped pointer.vkCmdCopyBuffer staging -> device-local buffer inside a command buffer.// Host-visible buffer
VkBuffer staging;
vkMapMemory(device, stagingMem, 0, size, 0, &ptr);
memcpy(ptr, cpuData, size);
vkUnmapMemory(device, stagingMem);
// Copy in a one-time command buffer + fence
vkCmdCopyBuffer(cmd, staging, deviceBuffer, 1, ®ion);
// run fence + wait before first device use
VkDescriptorPoolCreateInfo dpci{};
dpci.maxSets = 256;
VkDescriptorPoolSize poolSize{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 256 };
dpci.poolSizeCount = 1; dpci.pPoolSizes = &poolSize;
vkCreateDescriptorPool(device, &dpci, nullptr, &descPool);
VkDescriptorSetAllocateInfo dsai{};
dsai.descriptorPool = descPool;
dsai.descriptorSetCount = 1;
dsai.pSetLayouts = &dslSetLayout;
vkAllocateDescriptorSets(device, &dsai, &descriptorSet);
VkWriteDescriptorSet wds{};
wds.dstSet = descriptorSet;
wds.descriptorCount = 1;
wds.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
wds.pBufferInfo = &bufferInfo; // { buffer, offset, size }
vkUpdateDescriptorSets(device, 1, &wds, 0, nullptr);
Compute is Vulkan's workhorse for GPU-driven rendering, particles, physics, image processing.
// Pipeline: pipelineBindPoint = VK_PIPELINE_BIND_POINT_COMPUTE
// shaderStages = { compute shader }
VkComputePipelineCreateInfo cpci{};
cpci.stage = computeStage; // VkPipelineShaderStageCreateInfo
cpci.layout = computePipelineLayout;
vkCreateComputePipelines(device, pool, 1, &cpci, nullptr, &computePipeline);
// Dispatch
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, computePipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, computeLayout, 0, 1, &ds, 0, nullptr);
vkCmdDispatch(cmd, workgroupX, workgroupY, workgroupZ);
#version 450
layout(local_size_x = 256) in; // workgroup = 256 threads
layout(std430, binding = 0) buffer Particle { vec4 pos[]; } p;
void main() {
uint i = gl_GlobalInvocationID.x;
p.pos[i] += vec4(0.01, 0.0, 0.0, 1.0);
}
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER.Rays are traced against an acceleration structure built from BLAS (Bottom-Level, geometry) and TLAS (Top-Level, instances):
Build BLAS per mesh -> Instance refers BLAS -> Build TLAS from instances
| Stage | Purpose |
|---|---|
| RayGen | Fires the rays, writes the output |
| ClosestHit | Closest intersection -> shade |
| Miss | Rays that hit nothing (skybox) |
| AnyHit | Alpha-testing / transparency |
| Intersection | Custom primitive intersection |
Use VK_KHR_ray_tracing_pipeline + VK_KHR_acceleration_structure. Building TLAS per frame is a well-known cost; batch instance updates.
flowchart TD
A{VR / head-locked?} -->|Yes| B[IMMEDIATE]
A -->|No| C{Benchmark?} -->|Yes| D[IMMEDIATE]
C -->|No| E{Want lowest latency + no tear?} -->|Yes| F[MAILBOX]
E -->|No| G[FIFO vsync]
flowchart TD
A{Accessed by CPU every frame?} -->|Yes| B{HOST_VISIBLE}
A -->|No| C{Accessed only by GPU?} -->|Yes| D{DEVICE_LOCAL}
C -->|No| E[Staging + DEVICE_LOCAL]
D --> F[Upload once via staging]
B --> G[Map + memcpy per frame]
| Anti-pattern | Why it hurts | Fix |
|---|---|---|
vkDeviceWaitIdle() per frame | Stalls the GPU, kills pipelining | Use fences/semaphores per frame index |
| One big uniform buffer for everything | Driver stumbles, redundant uploads | Split UBOs by update frequency |
| Allocating device memory per object | Thousands of vkAllocateMemory | Suballocate from big pools |
| Ignoring image layout transitions | Garbage / artifacts / validation errors | Always place barriers |
| Synchronizing too much | Everything serialized | Use async queues, minimal barriers |
| Creating pipelines at runtime per material | Pipeline cache misses, stalls | Bake upfront; use pipeline cache |
| Mapping/unmapping every frame | Mapping cost + driver sync | Keep persistent mappings |
| Single global command buffer | No multi-thread recording | Per-thread pools + secondary buffers |
| Foregoing validation layers in dev | Silent corruption, hard bugs | Always enable in debug |
VK_KHR_driver_properties + vendor handling.VkResult from every call in debug builds.VK_PIPELINE_CACHE_CREATE_READ_ONLY_BIT).vkAllocateMemory.waitIdle).references/vulkan-initialization.md - instance, device, queue, validation detailreferences/swapchain-and-present.md - swapchain creation, present modes, recreationreferences/render-passes-and-pipelines.md - subpasses, dynamic state, pipeline cachingreferences/synchronization-deep-dive.md - barriers, fences, semaphores, timeline semaphoresreferences/memory-and-upload.md - allocators, staging, image/buffer resourcesreferences/compute-and-ray-tracing.md - compute dispatch, RT pipelines, TLAS/BLAS