Skip to main content

vulkan

Expert low-level GPU rendering with Vulkan - instance setup, swapchain management, render passes, command buffers, synchronization, memory management, compute shaders, and ray tracing.

Source facts

Repository
j4flmao/agent-skills
Last source activity
September 19, 2026 at 03:19
Detected SKILL.md language
English
Stars
27
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
7 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub