VkInstance is the very first object you must create. It represents the connection between your application and the Vulkan driver. Creating an instance initializes the Vulkan library and allows you to specify global configurations, such as which validation layers and instance-level extensions you want to enable. Validation layers are crucial for debugging, as they check for API misuse and provide detailed error messages. Extensions are used to enable functionality not included in the core Vulkan specification, such as support for drawing to a window surface, which is essential for any graphical application. Without an instance, you can’t query for physical devices or do anything else in Vulkan.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| package main
import "vendor:vulkan"
import "core:runtime" // for ffi
import "core:fmt"
main :: proc() {
// 1. Define Application Information
// ------------------------------------------------------------------
// This struct provides some basic metadata about your application to the driver.
// It's good practice to fill this out.
app_info := vulkan.ApplicationInfo{
sType = .APPLICATION_INFO,
pApplicationName = "My Odin Vulkan App",
applicationVersion = vulkan.MAKE_API_VERSION(0, 1, 0, 0),
pEngineName = "No Engine",
engineVersion = vulkan.MAKE_API_VERSION(0, 1, 0, 0),
apiVersion = vulkan.API_VERSION_1_3, // Specify Vulkan 1.3
}
// 2. Specify Validation Layers
// ------------------------------------------------------------------
// For debugging, we enable the standard Khronos validation layer.
// The layer names are C-style null-terminated strings.
validation_layers := []cstring{"VK_LAYER_KHRONOS_validation"}
// 3. Specify Required Extensions
// ------------------------------------------------------------------
// To draw to a window, you need extensions. Libraries like SDL3 provide
// a function to get the exact list of extension names you need for the
// current platform. For this example, we'll list them manually.
// A real app would call something like `SDL_Vulkan_GetInstanceExtensions`.
required_extensions := []cstring{
"VK_KHR_surface",
// Platform-specific extension, you'd only enable one of these.
// On Windows:
"VK_KHR_win32_surface",
// On Linux with X11:
// "VK_KHR_xlib_surface",
// On macOS (via MoltenVK):
// "VK_EXT_metal_surface",
}
// Add debug utils extension if validation layers are enabled
append(&required_extensions, "VK_EXT_debug_utils")
// 4. Create the Instance Create Info Struct
// ------------------------------------------------------------------
// This is the main struct that brings everything together for the instance creation.
create_info := vulkan.InstanceCreateInfo{
sType = .INSTANCE_CREATE_INFO,
pApplicationInfo = &app_info,
enabledLayerCount = u32(len(validation_layers)),
ppEnabledLayerNames = raw_data(validation_layers),
enabledExtensionCount = u32(len(required_extensions)),
ppEnabledExtensionNames = raw_data(required_extensions),
}
// 5. Create the Vulkan Instance
// ------------------------------------------------------------------
instance: vulkan.Instance
result := vulkan.CreateInstance(&create_info, nil, &instance)
if result != .SUCCESS {
// Handle error: instance creation failed.
fmt.eprintln("Failed to create Vulkan instance:", result)
return
}
// `instance` is now a valid handle and you can proceed to enumerate physical devices.
// Don't forget to destroy the instance when your application closes.
// vulkan.DestroyInstance(instance, nil)
}
|
Here are the most important functions related to managing a VkInstance
:
vkCreateInstance
: The function used to create the instance object, as shown in the code above.vkDestroyInstance
: Destroys an instance that you previously created, freeing all associated resources. This should be one of the last Vulkan functions you call before your application exits.vkEnumeratePhysicalDevices
: As seen previously, this function is called on an existing instance to discover the GPUs available on the system.vkEnumerateInstanceExtensionProperties
: Used to query the available instance-level extensions supported by the Vulkan driver. You can use this to check if the extensions you need (e.g., for windowing or debugging) are actually available before you try to enable them.vkEnumerateInstanceLayerProperties
: Similar to the extension query, this function lists the available validation and API layers on the system. It’s how you can confirm that layers like VK_LAYER_KHRONOS_validation
are installed and can be enabled.vkGetInstanceProcAddr
: A function pointer lookup. It’s used to get the address of Vulkan core and extension functions. While you often don’t call this directly when using a library or binding like Odin’s, the binding itself uses this function “under the hood” to load all the other Vulkan functions.
A VkPhysicalDevice
is a handle that represents a single piece of physical hardware in your system capable of running Vulkan, which is almost always a GPU. You don’t “create” a physical device; instead, you enumerate the ones present on the system and select the one you want to use. This object is your interface for querying the hardware’s capabilities. You’ll use it to check its properties (like its name and type), its features (like whether it supports geometry shaders), its memory types, and its queue families (the channels for submitting work). After choosing the best VkPhysicalDevice
for your app’s needs, you’ll then use it to create a VkDevice
(a logical device), which you’ll interact with for all rendering operations.
You don’t create a VkPhysicalDevice
, you enumerate and select one. The process assumes you have already created a VkInstance
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `instance` is a valid, previously created vulkan.Instance
instance: vulkan.Instance
// You would have code here to create the instance...
if instance == nil {
fmt.println("Instance is nil, cannot proceed.")
return
}
// 1. Get the number of available physical devices
// ------------------------------------------------------------------
physical_device_count: u32
vulkan.EnumeratePhysicalDevices(instance, &physical_device_count, nil)
if physical_device_count == 0 {
fmt.eprintln("Error: Failed to find GPUs with Vulkan support!")
return
}
// 2. Allocate memory and retrieve the list of physical devices
// ------------------------------------------------------------------
physical_devices := make([]vulkan.PhysicalDevice, physical_device_count)
vulkan.EnumeratePhysicalDevices(instance, &physical_device_count, raw_data(physical_devices))
// 3. Select a suitable physical device
// ------------------------------------------------------------------
// A real app would loop through `physical_devices` and check properties
// using `vkGetPhysicalDeviceProperties` to pick the best one (e.g., a discrete GPU).
// For simplicity, we'll just pick the first one.
physical_device: vulkan.PhysicalDevice = nil
for device in physical_devices {
// Here you would implement your selection logic.
// For now, we just take the first one available.
physical_device = device
break
}
if physical_device == nil {
fmt.eprintln("Error: Could not select a suitable physical device!")
return
}
// `physical_device` is now a valid handle to your chosen GPU.
fmt.println("Successfully selected a physical device.")
delete(physical_devices)
// Now you can query this device's properties and create a logical VkDevice from it.
}
|
These are the essential functions you’ll use to inspect a VkPhysicalDevice
before creating a logical device from it.
vkEnumeratePhysicalDevices
: The entry point function, called on a VkInstance
to get a list of all available VkPhysicalDevice
handles.vkGetPhysicalDeviceProperties
: Retrieves the fundamental properties of the device, including its name, type (discrete, integrated, etc.), vendor, and supported API version.vkGetPhysicalDeviceFeatures
: Reports on the availability of fine-grained hardware features, such as geometry shaders, wide lines, or logic operations. You must check for features your application needs and explicitly enable them later when creating the logical device.vkGetPhysicalDeviceQueueFamilyProperties
: Gets a list of the queue families supported by the device. This is crucial for finding queues that support graphics, compute, or data transfer operations.vkGetPhysicalDeviceMemoryProperties
: Provides details about the memory heaps and types available on the device. This is fundamental for understanding how to allocate memory efficiently, whether it’s fast on-device VRAM or host-visible RAM.vkGetPhysicalDeviceFormatProperties
: Lets you query how a specific data format is supported by the device (e.g., can an R8G8B8A8_UNORM
format be used as a color attachment for rendering?).vkCreateDevice
: While not a “get” or “query” function, this is the ultimate consumer of the VkPhysicalDevice
. It takes the physical device and a description of the features and queues you want to use, and it produces the VkDevice
(logical device) that you’ll use for rendering.
A VkSurfaceKHR
is an object that provides a bridge between Vulkan and a platform’s native windowing system. ↔️ Vulkan itself is platform-agnostic and has no concept of a “window.” The surface, which is part of a Khronos extension (hence the KHR
suffix), is an abstraction for a renderable surface like a window on your desktop. It’s the “canvas” that Vulkan will ultimately draw on. To create one, you use a platform-specific function that connects to your window, and libraries like SDL or GLFW typically handle this for you. Once created, you use the surface to query a physical device for its presentation capabilities, which is a required step before creating a swapchain.
You don’t use a generic vkCreate...
function for surfaces. Instead, you use a platform-specific one. Since you are using SDL3, the library provides a convenient function that handles all the platform-specific details for you.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| package main
// Import the Odin bindings for SDL3 and Vulkan
import "vendor:sdl3"
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// This example assumes you have already:
// 1. Initialized SDL and created an SDL_Window with the VULKAN flag.
// 2. Created a VkInstance with the extensions required by SDL.
// Placeholder variables for what you would have already created.
window: ^sdl3.Window
instance: vulkan.Instance
if window == nil || instance == nil { return }
// 1. Create the Surface using the SDL helper function
// ------------------------------------------------------------------
// Instead of calling a platform-specific function like vkCreateWin32SurfaceKHR,
// SDL provides a cross-platform way to do it. It takes your Vulkan instance
// and your SDL window and returns a VkSurfaceKHR handle.
surface: vulkan.SurfaceKHR
// Note: The Odin bindings for SDL3 might have a slightly different function signature.
// This reflects the C function `SDL_Vulkan_CreateSurface`.
// We cast the window pointer to match the expected type.
if !sdl3.Vulkan_CreateSurface(window, instance, &surface) {
// Handle error from SDL_GetError()
fmt.eprintln("Failed to create Vulkan surface via SDL.")
return
}
fmt.Println("Successfully created VkSurfaceKHR using SDL.")
// `surface` is now a valid handle that connects Vulkan to your window.
// The next step is to query a physical device to see if it can render
// to this surface before creating a swapchain.
// ... your application logic ...
// When you're done, destroy the surface.
// vulkan.DestroySurfaceKHR(instance, surface, nil)
}
|
The main purpose of a surface is to be queried for its presentation capabilities with a specific GPU.
- Platform-Specific Creation Functions (e.g.,
vkCreateWin32SurfaceKHR
, vkCreateXlibSurfaceKHR
, vkCreateMetalSurfaceEXT
): These are the underlying functions used to create the surface. You typically don’t call them directly when using a library like SDL. vkDestroySurfaceKHR
: Destroys the surface object. This should be done before destroying the instance.vkGetPhysicalDeviceSurfaceSupportKHR
: A critical query function. You must call this to ask if a specific queue family on a physical device is capable of presenting images to this particular surface.vkGetPhysicalDeviceSurfaceCapabilitiesKHR
: Retrieves the capabilities of a surface on a specific GPU, such as the minimum/maximum number of images for a swapchain, the current resolution, and supported transforms.vkGetPhysicalDeviceSurfaceFormatsKHR
: Retrieves the list of supported pixel formats and color spaces that the surface can use.vkGetPhysicalDeviceSurfacePresentModesKHR
: Retrieves the list of supported presentation modes (e.g., FIFO
for vsync, MAILBOX
for low-latency, IMMEDIATE
for no vsync).
A VkSwapchainKHR
is an object that manages a queue of images to be presented to a display. 🎞️ It’s the core component for getting your rendered frames onto the screen. A swapchain typically contains two or more images (a technique called double or triple buffering). While your application is busy drawing to one of the images, another image is being displayed on the screen. Once you’re done drawing, you “present” your image, and the swapchain “swaps” it with the one on screen, giving you a new blank image to draw on. This process is fundamental for producing smooth, tear-free animation. A swapchain is created from your VkDevice
and is directly tied to a VkSurfaceKHR
.
Creating a swapchain is a multi-step process. You must first query the capabilities of the VkSurfaceKHR
to determine the supported settings (like formats and presentation modes) and then use those settings to create the swapchain.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// This example assumes you have:
// - A valid `device` and `physical_device`.
// - A valid `surface`.
// - The `graphics_family_index`.
device: vulkan.Device
physical_device: vulkan.PhysicalDevice
surface: vulkan.SurfaceKHR
graphics_family_index: u32
if device == nil { return }
// 1. Query for surface capabilities, formats, and present modes
// ------------------------------------------------------------------
// (You would have functions here to call vkGetPhysicalDeviceSurfaceCapabilitiesKHR,
// vkGetPhysicalDeviceSurfaceFormatsKHR, etc., and store the results.)
capabilities: vulkan.SurfaceCapabilitiesKHR
// ... query capabilities ...
// Choose our settings from the query results.
chosen_format: vulkan.SurfaceFormatKHR = // ... choose from available formats ...
chosen_present_mode: vulkan.PresentModeKHR = // ... choose from available present modes ...
chosen_extent: vulkan.Extent2D = capabilities.currentExtent
// Determine the number of images in the swapchain.
image_count := capabilities.minImageCount + 1
if capabilities.maxImageCount > 0 && image_count > capabilities.maxImageCount {
image_count = capabilities.maxImageCount
}
// 2. Create the Swapchain
// ------------------------------------------------------------------
create_info := vulkan.SwapchainCreateInfoKHR{
sType = .SWAPCHAIN_CREATE_INFO_KHR,
surface = surface,
minImageCount = image_count,
imageFormat = chosen_format.format,
imageColorSpace = chosen_format.colorSpace,
imageExtent = chosen_extent,
imageArrayLayers = 1,
imageUsage = {.COLOR_ATTACHMENT_BIT},
imageSharingMode = .EXCLUSIVE,
queueFamilyIndexCount = 0,
pQueueFamilyIndices = nil,
preTransform = capabilities.currentTransform,
compositeAlpha = .OPAQUE_BIT_KHR,
presentMode = chosen_present_mode,
clipped = true,
oldSwapchain = nil, // Used for recreating/resizing a swapchain
}
swapchain: vulkan.SwapchainKHR
result := vulkan.CreateSwapchainKHR(device, &create_info, nil, &swapchain)
if result != .SUCCESS {
fmt.eprintln("Failed to create swapchain:", result)
return
}
fmt.Println("Successfully created swapchain.")
// 3. Retrieve the swapchain images
// ------------------------------------------------------------------
// The images are created by the implementation, we just need to get their handles.
vulkan.GetSwapchainImagesKHR(device, swapchain, &image_count, nil)
swapchain_images := make([]vulkan.Image, image_count)
vulkan.GetSwapchainImagesKHR(device, swapchain, &image_count, raw_data(swapchain_images))
// Now you must create a VkImageView for each VkImage in `swapchain_images`.
// ... your application logic ...
// vulkan.DestroySwapchainKHR(device, swapchain, nil)
}
|
These functions manage the lifecycle of the swapchain and are central to every frame of your render loop.
vkCreateSwapchainKHR
: Creates the swapchain object itself.vkDestroySwapchainKHR
: Destroys the swapchain. You must ensure the device is idle before calling this.vkGetSwapchainImagesKHR
: Retrieves the handles to the VkImage
objects that are owned by the swapchain.vkAcquireNextImageKHR
: This is the first step in your render loop. You call this to acquire an available image from the swapchain to render into. It returns the index of the image and signals a semaphore or fence when the image is ready for you to use.vkQueuePresentKHR
: This is the last step in your render loop. After you have finished rendering to an image (and have signaled a semaphore to indicate this), you call this function to return the image to the swapchain so it can be presented to the screen.
A VkDevice
, or logical device, is your primary interface for interacting with a GPU. 💻 While a VkPhysicalDevice
represents the actual hardware, the VkDevice
is a software abstraction you create from it. When you create a logical device, you are essentially opening a session with the physical GPU and specifying exactly which features (like geometry shaders) and queue families you plan to use. All major Vulkan objects—such as memory buffers, images, pipelines, and command buffers—are created from a VkDevice
. All the work you submit to a VkQueue
originates from commands recorded on objects tied to that logical device.
Creating a VkDevice
is the culmination of your setup process. It requires the VkPhysicalDevice
you selected and information about the queues and features you want to enable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `physical_device` is a valid, selected VkPhysicalDevice
// and `graphics_family_index` has been found.
physical_device: vulkan.PhysicalDevice
graphics_family_index: u32
if physical_device == nil { return }
// 1. Specify which queue(s) to create
// ------------------------------------------------------------------
queue_priority: f32 = 1.0
queue_create_info := vulkan.DeviceQueueCreateInfo {
sType = .DEVICE_QUEUE_CREATE_INFO,
queueFamilyIndex = graphics_family_index,
queueCount = 1,
pQueuePriorities = &queue_priority,
}
// 2. Specify which device features to enable
// ------------------------------------------------------------------
// First, query for available features using vkGetPhysicalDeviceFeatures.
// For this example, we'll just create an empty features struct,
// meaning we are not enabling any optional features.
// A real app would query features and set required ones to true.
// For example: `device_features.geometryShader = true;`
device_features := vulkan.PhysicalDeviceFeatures{}
// 3. Specify device extensions to enable
// ------------------------------------------------------------------
// If you want to draw to a window, you MUST enable the swapchain extension.
device_extensions := []cstring {
"VK_KHR_swapchain",
}
// 4. Assemble the main create info struct
// ------------------------------------------------------------------
create_info := vulkan.DeviceCreateInfo {
sType = .DEVICE_CREATE_INFO,
pQueueCreateInfos = &queue_create_info,
queueCreateInfoCount = 1,
pEnabledFeatures = &device_features,
ppEnabledExtensionNames = raw_data(device_extensions),
enabledExtensionCount = u32(len(device_extensions)),
// ppEnabledLayerNames is deprecated for VkDeviceCreateInfo,
// validation layers are set at the instance level.
}
// 5. Create the logical device
// ------------------------------------------------------------------
device: vulkan.Device
result := vulkan.CreateDevice(physical_device, &create_info, nil, &device)
if result != .SUCCESS {
fmt.eprintln("Failed to create logical device:", result)
return
}
fmt.Println("Successfully created logical device.")
// `device` is now your handle to the GPU, used for almost all other Vulkan calls.
// ... your application logic ...
// vulkan.DestroyDevice(device, nil)
}
|
A huge number of Vulkan functions use a VkDevice
handle as their first parameter. Here are some of the most fundamental ones for creation and synchronization.
vkCreateDevice
: The function that creates the logical device from a physical device.vkDestroyDevice
: Destroys the logical device, freeing all resources created with it. This must be called before destroying the VkInstance
.vkGetDeviceQueue
: As seen previously, this retrieves the handle to a device queue that you requested during creation.vkDeviceWaitIdle
: This is a powerful, heavyweight synchronization function. It blocks the CPU until the entire device has finished all pending operations on all of its queues. It’s most commonly used right before shutting down your application to ensure all resources can be safely destroyed.vkCreateSwapchainKHR
, vkCreateImageView
, vkCreateShaderModule
, vkCreateRenderPass
, vkCreateGraphicsPipelines
, vkCreateFramebuffer
, vkCreateCommandPool
, vkCreateBuffer
, vkCreateImage
, vkAllocateMemory
, vkCreateSemaphore
, vkCreateFence
: This is not a single function, but a representative list showing that almost every object in Vulkan is created from the logical device. The vkCreate...
pattern is ubiquitous.vkAllocateCommandBuffers
: Allocates the command buffers you use to record rendering commands.vkFreeMemory
: Frees device memory that was allocated with vkAllocateMemory
.
A VkQueue
is a command queue that you use to submit work to the GPU. ⚙️ Think of it as a conveyor belt leading into a factory (the GPU). You package your instructions—like rendering commands, compute tasks, or memory transfers—into a VkCommandBuffer
and then submit that “package” to a queue. The GPU then picks up the work from the queue and executes it. You don’t create queues directly; you request them when you create your logical device (VkDevice
) from a specific queue family (a category of queues with certain capabilities, like graphics or compute). After the device is created, you retrieve a handle to the queue you requested.
You get a VkQueue
handle after creating a VkDevice
. The process involves first finding a suitable queue family on your VkPhysicalDevice
and then requesting a queue from that family during device creation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `physical_device` is a valid, selected VkPhysicalDevice
physical_device: vulkan.PhysicalDevice
if physical_device == nil { return }
// 1. Find a suitable queue family
// ------------------------------------------------------------------
// We need to find a queue family that supports graphics operations.
queue_family_count: u32
vulkan.GetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, nil)
queue_families := make([]vulkan.QueueFamilyProperties, queue_family_count)
vulkan.GetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, raw_data(queue_families))
graphics_family_index: u32 = vulkan.QUEUE_FAMILY_IGNORED
for props, i in queue_families {
if .GRAPHICS_BIT in props.queueFlags {
graphics_family_index = u32(i)
break
}
}
delete(queue_families)
if graphics_family_index == vulkan.QUEUE_FAMILY_IGNORED {
fmt.eprintln("Could not find a queue family with graphics support!")
return
}
// 2. Request the queue during device creation
// ------------------------------------------------------------------
// We fill out a struct to tell Vulkan we want one queue from our chosen family.
queue_priority: f32 = 1.0
queue_create_info := vulkan.DeviceQueueCreateInfo {
sType = .DEVICE_QUEUE_CREATE_INFO,
queueFamilyIndex = graphics_family_index,
queueCount = 1,
pQueuePriorities = &queue_priority,
}
// This would be part of your larger VkDeviceCreateInfo struct
device_create_info := vulkan.DeviceCreateInfo {
sType = .DEVICE_CREATE_INFO,
pQueueCreateInfos = &queue_create_info,
queueCreateInfoCount = 1,
// ... other properties like enabled features would be set here
}
// 3. Create the logical device
// ------------------------------------------------------------------
device: vulkan.Device
vulkan.CreateDevice(physical_device, &device_create_info, nil, &device)
// 4. Retrieve the queue handle
// ------------------------------------------------------------------
// Now that the device exists, we can finally get the handle to our queue.
graphics_queue: vulkan.Queue
vulkan.GetDeviceQueue(device, graphics_family_index, 0, &graphics_queue)
// `graphics_queue` is now a valid handle you can use to submit command buffers.
fmt.Println("Successfully retrieved graphics queue handle.")
// ... your application logic ...
// vulkan.DestroyDevice(device, nil)
}
|
These are the primary functions you’ll use to interact with a VkQueue
object.
vkGetDeviceQueue
: The function used to retrieve the handle to a queue after its VkDevice
has been created.vkQueueSubmit
: This is the most important queue function. You call it to submit one or more command buffers to the queue for the GPU to execute. This is how you tell the GPU to do work.vkQueuePresentKHR
: Used to present a rendered image from a swapchain to the screen. This function is part of the VK_KHR_swapchain
extension and is essential for displaying anything in a window.vkQueueWaitIdle
: A simple and powerful synchronization command. It pauses the CPU thread and waits until all previously submitted work on this queue has been fully completed by the GPU. This is useful for safely shutting down or when you need to be certain a task is finished before proceeding.vkQueueSubmit2
: A newer, more extensible version of vkQueueSubmit
that offers better performance and more features for batching submissions and managing synchronization primitives. It’s generally recommended over the original if your driver supports it.
A VkCommandPool
is a memory manager for command buffers. 📋 Command buffers are objects that you record GPU commands into, but allocating and deallocating them one by one can be inefficient. Instead, you create a command pool tied to a specific queue family (like your graphics queue). This pool pre-allocates and manages the memory for all command buffers you’ll need for that queue family. This makes the allocation of individual command buffers a very fast, low-overhead operation. You can also easily reset the entire pool, which simultaneously resets all command buffers allocated from it, allowing for efficient reuse of memory frame after frame.
You create a VkCommandPool
from your VkDevice
. You must specify which queue family the pool will be associated with, as command buffers allocated from this pool can only be submitted to queues of that family.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `device` is a valid VkDevice and `graphics_family_index` has been found.
device: vulkan.Device
graphics_family_index: u32
if device == nil { return }
// 1. Specify the Command Pool Create Info
// ------------------------------------------------------------------
// We need to tell Vulkan which queue family this pool will serve.
// We also set a flag that allows individual command buffers to be reset.
pool_info := vulkan.CommandPoolCreateInfo{
sType = .COMMAND_POOL_CREATE_INFO,
flags = .CREATE_RESET_COMMAND_BUFFER_BIT,
queueFamilyIndex = graphics_family_index,
}
// 2. Create the Command Pool
// ------------------------------------------------------------------
command_pool: vulkan.CommandPool
result := vulkan.CreateCommandPool(device, &pool_info, nil, &command_pool)
if result != .SUCCESS {
fmt.eprintln("Failed to create command pool:", result)
return
}
fmt.Println("Successfully created command pool.")
// `command_pool` is now a valid handle you can use to allocate command buffers.
// ... your application logic ...
// When you're done, destroy the pool. This also frees all command buffers
// allocated from it.
// vulkan.DestroyCommandPool(device, command_pool, nil)
}
|
These functions cover the lifecycle and management of command pools and the buffers within them.
vkCreateCommandPool
: The function that creates the command pool object from a VkDevice
.vkDestroyCommandPool
: Destroys the command pool and automatically frees all VkCommandBuffer
s that were allocated from it.vkAllocateCommandBuffers
: This is the primary purpose of a command pool. You call this function on a pool to get one or more fresh VkCommandBuffer
handles, ready for you to record commands into.vkFreeCommandBuffers
: Frees specific command buffers, returning their memory to the pool for reuse.vkResetCommandPool
: A highly efficient function that resets the entire pool, effectively resetting all command buffers allocated from it back to their initial state. This is often faster than resetting them one by one and is a common strategy in render loops.vkTrimCommandPool
: An optional memory-saving function. It allows the driver to release any unused memory held by the command pool back to the system.
A VkCommandBuffer
is an object that you use to record commands for the GPU to execute. Think of it as a cassette tape or a digital recording. You “begin recording” on a command buffer, list all your instructions (bind a pipeline, set a viewport, draw vertices, dispatch compute, etc.), and then “stop recording.” Once recording is finished, the command buffer is a self-contained package of work that you can submit to a VkQueue
to be executed by the GPU. Command buffers are allocated from a VkCommandPool
, which manages their memory.
You don’t “create” command buffers directly; you allocate them from a VkCommandPool
. This is a lightweight operation. The main work is recording commands into it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `device` is a valid VkDevice and `command_pool` is a valid VkCommandPool.
device: vulkan.Device
command_pool: vulkan.CommandPool
if device == nil || command_pool == nil { return }
// 1. Allocate the Command Buffer
// ------------------------------------------------------------------
// We request one primary command buffer from our pool.
alloc_info := vulkan.CommandBufferAllocateInfo{
sType = .COMMAND_BUFFER_ALLOCATE_INFO,
commandPool = command_pool,
level = .PRIMARY,
commandBufferCount = 1,
}
command_buffer: vulkan.CommandBuffer
result := vulkan.AllocateCommandBuffers(device, &alloc_info, &command_buffer)
if result != .SUCCESS {
fmt.eprintln("Failed to allocate command buffer:", result)
return
}
fmt.Println("Successfully allocated a command buffer.")
// 2. Record Commands (Simplified Example)
// ------------------------------------------------------------------
// Begin recording
begin_info := vulkan.CommandBufferBeginInfo {
sType = .COMMAND_BUFFER_BEGIN_INFO,
flags = .ONE_TIME_SUBMIT_BIT, // This is a hint to the driver
}
vulkan.BeginCommandBuffer(command_buffer, &begin_info)
// --- Record your commands here ---
// Example: vkCmdBindPipeline(command_buffer, ...);
// Example: vkCmdDraw(command_buffer, ...);
// ---------------------------------
// End recording
vulkan.EndCommandBuffer(command_buffer)
fmt.Println("Successfully recorded commands to the command buffer.")
// The command buffer is now ready to be submitted to a VkQueue.
// To reuse this command buffer later, you would call:
// vulkan.ResetCommandBuffer(command_buffer, 0)
// and then begin recording again.
}
|
These functions manage the lifecycle and recording state of a command buffer. Note how most command-recording functions are prefixed with vkCmd
.
vkAllocateCommandBuffers
: Allocates one or more command buffers from a VkCommandPool
.vkFreeCommandBuffers
: Frees command buffers, returning their memory to the parent VkCommandPool
.vkBeginCommandBuffer
: Puts a command buffer into the “recording” state, making it ready to accept commands. You must call this before any vkCmd...
functions.vkEndCommandBuffer
: Puts a command buffer into the “executable” state, finalizing the recording process. After this, it can be submitted to a queue.vkResetCommandBuffer
: Resets an individual command buffer back to its initial state, allowing you to record new commands into it without having to free and reallocate it.vkCmd...
(e.g., vkCmdBindPipeline
, vkCmdDraw
, vkCmdDispatch
, vkCmdCopyBuffer
): This is a large family of functions that can only be called between vkBeginCommandBuffer
and vkEndCommandBuffer
. They are the actual instructions (draw calls, state changes, memory operations) that you record for the GPU to perform.
Dynamic Rendering Commands:
vkCmdBeginRenderingKHR
: Begins a dynamic rendering operation with specified attachments. Replaces vkCmdBeginRenderPass
.vkCmdEndRenderingKHR
: Ends the current dynamic rendering operation. Replaces vkCmdEndRenderPass
.vkCmdSetViewportWithCountEXT
: Dynamically sets viewports without specifying count at pipeline creation (useful with dynamic rendering).vkCmdSetScissorWithCountEXT
: Dynamically sets scissor rectangles without specifying count at pipeline creation.
A VkBuffer
is a handle to a linear, unstructured region of memory that can be accessed by the GPU. 💾 Think of it as a generic block of data, like a C-style array. Buffers are the fundamental objects used to store data for your graphics pipeline, such as vertex positions, indices for indexed drawing, uniform data (UBOs) for shader constants, and general-purpose storage data (SSBOs). Creating a VkBuffer
object itself does not allocate any physical memory; it only defines the buffer’s size and intended usage. You must then separately allocate a block of VkDeviceMemory
and bind it to the buffer object to give it actual storage.
The process involves three main steps: creating the buffer object, allocating a suitable block of memory for it, and then binding the two together.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| package main
import "vendor:vulkan"
import "core:fmt"
// Helper function to find a suitable memory type. (This is a simplified example).
find_memory_type :: proc(physical_device: vulkan.PhysicalDevice, type_filter: u32, properties: vulkan.MemoryPropertyFlags) -> u32 {
mem_properties: vulkan.PhysicalDeviceMemoryProperties
vulkan.GetPhysicalDeviceMemoryProperties(physical_device, &mem_properties)
for i := u32(0); i < mem_properties.memoryTypeCount; i += 1 {
if (type_filter & (1 << i)) != 0 && (mem_properties.memoryTypes[i].propertyFlags & properties) == properties {
return i
}
}
return ~u32(0) // Should not happen
}
main :: proc() {
// Assume `device`, `physical_device` are valid.
device: vulkan.Device
physical_device: vulkan.PhysicalDevice
if device == nil { return }
BUFFER_SIZE :: 1024 // 1 KB example size
// 1. Create the VkBuffer object
// ------------------------------------------------------------------
buffer_info := vulkan.BufferCreateInfo{
sType = .BUFFER_CREATE_INFO,
size = BUFFER_SIZE,
usage = {.VERTEX_BUFFER_BIT}, // Example: This buffer will hold vertex data
sharingMode = .EXCLUSIVE,
}
buffer: vulkan.Buffer
vulkan.CreateBuffer(device, &buffer_info, nil, &buffer)
// 2. Allocate memory for the buffer
// ------------------------------------------------------------------
// Get the memory requirements for this specific buffer on this GPU.
mem_requirements: vulkan.MemoryRequirements
vulkan.GetBufferMemoryRequirements(device, buffer, &mem_requirements)
// Find a memory type that is suitable (e.g., host-visible for CPU uploads).
mem_type_index := find_memory_type(physical_device, mem_requirements.memoryTypeBits, {.HOST_VISIBLE_BIT, .HOST_COHERENT_BIT})
alloc_info := vulkan.MemoryAllocateInfo{
sType = .MEMORY_ALLOCATE_INFO,
allocationSize = mem_requirements.size,
memoryTypeIndex = mem_type_index,
}
buffer_memory: vulkan.DeviceMemory
vulkan.AllocateMemory(device, &alloc_info, nil, &buffer_memory)
// 3. Bind the memory to the buffer
// ------------------------------------------------------------------
// Associate the allocated memory block with our buffer object.
vulkan.BindBufferMemory(device, buffer, buffer_memory, 0)
fmt.Println("Successfully created, allocated, and bound a VkBuffer.")
// Now you can map this memory to upload data, or use it in command buffers.
// ... your application logic ...
// Cleanup must happen in reverse: destroy the buffer, then free the memory.
// vulkan.DestroyBuffer(device, buffer, nil)
// vulkan.FreeMemory(device, buffer_memory, nil)
}
|
These functions manage the lifecycle, memory, and data of a VkBuffer
.
vkCreateBuffer
: Creates the buffer object, defining its size and usage flags.vkDestroyBuffer
: Destroys the buffer object. You must free its bound memory separately.vkGetBufferMemoryRequirements
: Queries a buffer object to find out how much memory it needs, its alignment requirements, and which memory types are compatible.vkAllocateMemory
: Allocates a block of raw VkDeviceMemory
from the GPU.vkBindBufferMemory
: Connects a VkBuffer
object to a VkDeviceMemory
block, giving it physical storage.vkMapMemory
: Gets a CPU-accessible pointer to a region of device memory. This is the primary way to upload data from your application to a buffer. The memory must have been allocated from a host-visible memory type.vkUnmapMemory
: Invalidates the CPU pointer obtained from vkMapMemory
.vkCmdCopyBuffer
: A command recorded into a VkCommandBuffer
to perform a high-speed, GPU-side copy from one buffer to another.
A VkBufferView
acts as an interpreter for the data within a VkBuffer
. 👓 A VkBuffer
on its own is just a raw, unstructured block of bytes. A VkBufferView
provides a specific format for that data, allowing a shader to access the buffer as if it were a one-dimensional texture (a texel buffer). This is useful when you want to read from a large, linear array of data in a shader using formatted lookups, which is a common technique for passing large amounts of structured data to the GPU. You are essentially telling Vulkan, “Treat this raw block of bytes as a tightly packed array of, for example, R32G32B32A32_SFLOAT
vectors.”
To create a VkBufferView
, you must have an existing VkBuffer
that was created with the UNIFORM_TEXEL_BUFFER
or STORAGE_TEXEL_BUFFER
usage flag. The view is then created from that buffer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `device` is a valid VkDevice.
// Assume `my_buffer` is a valid VkBuffer that was created with
// a usage flag like `.UNIFORM_TEXEL_BUFFER_BIT`.
device: vulkan.Device
my_buffer: vulkan.Buffer
if device == nil || my_buffer == nil { return }
// 1. Specify the Buffer View Create Info
// ------------------------------------------------------------------
// We bind the view to our existing buffer and tell Vulkan how to
// interpret the data's format.
view_info := vulkan.BufferViewCreateInfo{
sType = .BUFFER_VIEW_CREATE_INFO,
buffer = my_buffer,
format = .R8G8B8A8_UNORM, // Example: treat buffer data as 4-channel, 8-bit normalized values
offset = 0, // Start the view from the beginning of the buffer
range = vulkan.WHOLE_SIZE, // View the entire buffer
}
// 2. Create the Buffer View
// ------------------------------------------------------------------
buffer_view: vulkan.BufferView
result := vulkan.CreateBufferView(device, &view_info, nil, &buffer_view)
if result != .SUCCESS {
fmt.eprintln("Failed to create buffer view:", result)
return
}
fmt.Println("Successfully created buffer view.")
// `buffer_view` can now be bound to a descriptor set so that shaders can
// access the underlying buffer as a texel buffer.
// ... your application logic ...
// Destroy the view when you are done with it.
// vulkan.DestroyBufferView(device, buffer_view, nil)
}
|
VkBufferView
is a very simple object with a minimal API. It’s a lightweight “wrapper” and doesn’t manage memory or data itself.
vkCreateBufferView
: The function that creates the buffer view object, linking it to a VkBuffer
and defining its data format.vkDestroyBufferView
: Destroys the buffer view. This does not affect the underlying VkBuffer
or its memory, which must be managed separately.
The primary use of a VkBufferView
is not through direct API calls, but by including it in a VkWriteDescriptorSet
structure. This makes the view accessible to shaders as a uniform samplerBuffer
or uniform imageBuffer
, allowing you to fetch formatted data (texels) from the buffer.
A VkImage
is an object that contains multi-dimensional data, typically used for textures or render targets. 🖼️ Unlike a VkBuffer
, which is a simple linear array of bytes, a VkImage
has a defined structure, including its dimensions (width, height, depth), format (how to interpret each pixel), and mipmap levels. This structured nature allows the GPU hardware to perform highly efficient, accelerated operations like filtering and sampling. Images are the backbone of visual rendering; they are used as textures sampled by shaders, as color attachments to render into, and as depth/stencil buffers for visibility testing. Just like a buffer, creating an image object only defines its properties; you must then separately allocate and bind VkDeviceMemory
to it.
The process is nearly identical to creating a buffer: you create the image object, query its memory requirements, allocate the memory, and bind them together.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| package main
import "vendor:vulkan"
import "core:fmt"
// Assume find_memory_type helper from the vkBuffer example exists.
find_memory_type :: proc(physical_device: vulkan.PhysicalDevice, type_filter: u32, properties: vulkan.MemoryPropertyFlags) -> u32 {
// ... implementation from vkBuffer example
return 0 // Placeholder
}
main :: proc() {
// Assume `device` and `physical_device` are valid.
device: vulkan.Device
physical_device: vulkan.PhysicalDevice
if device == nil { return }
// 1. Create the VkImage object
// ------------------------------------------------------------------
image_info := vulkan.ImageCreateInfo{
sType = .IMAGE_CREATE_INFO,
imageType = ._2D,
extent = {1024, 1024, 1}, // 1024x1024 2D image
mipLevels = 1,
arrayLayers = 1,
format = .R8G8B8A8_SRGB,
tiling = .OPTIMAL, // Lets the driver arrange pixels for best performance
initialLayout = .UNDEFINED,
// Used as a texture and as a destination for a buffer copy
usage = {.TRANSFER_DST_BIT, .SAMPLED_BIT},
sharingMode = .EXCLUSIVE,
samples = ._1_BIT,
}
image: vulkan.Image
vulkan.CreateImage(device, &image_info, nil, &image)
// 2. Allocate memory for the image
// ------------------------------------------------------------------
mem_requirements: vulkan.MemoryRequirements
vulkan.GetImageMemoryRequirements(device, image, &mem_requirements)
// Find a memory type for optimal, device-local GPU memory.
mem_type_index := find_memory_type(physical_device, mem_requirements.memoryTypeBits, {.DEVICE_LOCAL_BIT})
alloc_info := vulkan.MemoryAllocateInfo{
sType = .MEMORY_ALLOCATE_INFO,
allocationSize = mem_requirements.size,
memoryTypeIndex = mem_type_index,
}
image_memory: vulkan.DeviceMemory
vulkan.AllocateMemory(device, &alloc_info, nil, &image_memory)
// 3. Bind the memory to the image
// ------------------------------------------------------------------
vulkan.BindImageMemory(device, image, image_memory, 0)
fmt.Println("Successfully created, allocated, and bound a VkImage.")
// The image is now backed by memory, but its layout is UNDEFINED.
// You must perform a layout transition before using it.
// ... cleanup ...
// vulkan.DestroyImage(device, image, nil)
// vulkan.FreeMemory(device, image_memory, nil)
}
|
These functions manage the image lifecycle, its memory, and its layout state.
vkCreateImage
: Creates the image object with its specified dimensions, format, and usage.vkDestroyImage
: Destroys the image object. Its memory must be freed separately.vkGetImageMemoryRequirements
: Queries an image to determine its memory needs, including size and alignment.vkBindImageMemory
: Associates a VkDeviceMemory
block with a VkImage
object.vkCmdPipelineBarrier
: A critical synchronization command used to perform an image layout transition. You must use this to change an image’s layout to make it suitable for a specific operation (e.g., transitioning from UNDEFINED
to TRANSFER_DST_OPTIMAL
before a copy, or to SHADER_READ_ONLY_OPTIMAL
before sampling in a shader).vkCmdCopyBufferToImage
: A command to copy pixel data from a VkBuffer
into a VkImage
. This is the standard method for uploading texture data to the GPU.vkCmdCopyImage
: A command for a direct, GPU-side copy from one VkImage
to another.vkCmdBlitImage
: A more powerful GPU-side copy command that can perform scaling, filtering, and format conversion between images.
A VkImageView
is a view or an “interpreter” for a VkImage
. 🖼️➡️ An image object can be complex, containing multiple mipmap levels, layers for a texture array, or a specific data format. You can’t use a VkImage
directly for rendering or sampling; you must first create a view of it. The image view specifies exactly how to treat the image’s data: which part of the image to look at (which mip levels or array layers), what format to interpret the pixels as, and whether to treat it as a 2D texture, a cubemap, etc. Think of the VkImage
as a raw photo file and the VkImageView
as the settings in a photo viewer that determine how you display it.
You create a VkImageView
from an existing VkImage
. The view’s configuration must be compatible with the parent image’s properties.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `device` is a valid VkDevice.
// Assume `my_image` is a valid VkImage that has been created and bound to memory.
device: vulkan.Device
my_image: vulkan.Image
image_format: vulkan.Format // The format used when creating my_image
if device == nil || my_image == nil { return }
// 1. Specify the Image View Create Info
// ------------------------------------------------------------------
view_info := vulkan.ImageViewCreateInfo{
sType = .IMAGE_VIEW_CREATE_INFO,
image = my_image,
viewType = ._2D, // Treat the image as a standard 2D texture
format = image_format, // Use the same format as the image
// `components` allows for swizzling channels, e.g., mapping R to G.
// .IDENTITY means no swizzling (R->R, G->G, etc.).
components = { .IDENTITY, .IDENTITY, .IDENTITY, .IDENTITY },
// `subresourceRange` describes which part of the image this view accesses.
subresourceRange = {
aspectMask = .COLOR_BIT, // This view looks at the color aspect of the image
baseMipLevel = 0, // Start at the first mipmap level
levelCount = 1, // Include one mipmap level
baseArrayLayer = 0, // Start at the first array layer
layerCount = 1, // Include one array layer
},
}
// 2. Create the Image View
// ------------------------------------------------------------------
image_view: vulkan.ImageView
result := vulkan.CreateImageView(device, &view_info, nil, &image_view)
if result != .SUCCESS {
fmt.eprintln("Failed to create image view:", result)
return
}
fmt.Println("Successfully created image view.")
// `image_view` can now be used as a texture in a descriptor set or as an
// attachment in a framebuffer.
// ... your application logic ...
// vulkan.DestroyImageView(device, image_view, nil)
}
|
VkImageView
is a simple object with a minimal API. It acts as a descriptor for its parent VkImage
.
vkCreateImageView
: The function that creates the image view object from a VkImage
.vkDestroyImageView
: Destroys the image view. This has no effect on the underlying VkImage
, which must be destroyed separately.
The main role of a VkImageView
is to be used by other parts of the Vulkan API. It is the object you will pass into a VkWriteDescriptorSet
to make a texture available to a shader, and it’s what you’ll provide to a VkFramebufferCreateInfo
to define the render targets (attachments) for a render pass.
A VkSampler
is an object that controls how a shader reads from an image. 🔬 It is completely separate from the VkImage
and VkImageView
. While the image view tells the shader what data to access, the sampler tells it how to access it. A sampler specifies all the filtering and addressing modes, such as whether to blend pixels smoothly (linear filtering) or keep them sharp (nearest filtering), and what to do when sampling outside the image’s boundaries (repeat, clamp, etc.). This separation is powerful because it allows you to use a single, reusable sampler configuration (like “smoothly repeating texture”) with many different image views.
A VkSampler
is created from the VkDevice
and is configured with all the texturing options you need.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `device` is a valid VkDevice.
// Assume the `samplerAnisotropy` physical device feature was enabled when creating the device.
device: vulkan.Device
if device == nil { return }
// 1. Specify the Sampler Create Info
// ------------------------------------------------------------------
sampler_info := vulkan.SamplerCreateInfo{
sType = .SAMPLER_CREATE_INFO,
magFilter = .LINEAR, // Blending for when the texture is magnified (close-up)
minFilter = .LINEAR, // Blending for when the texture is minified (far away)
addressModeU = .REPEAT, // Repeat the texture horizontally
addressModeV = .REPEAT, // Repeat the texture vertically
addressModeW = .REPEAT, // Repeat the texture in depth (for 3D textures)
anisotropyEnable = true,
maxAnisotropy = 16, // Improves quality for textures at sharp angles
borderColor = .INT_OPAQUE_BLACK,
unnormalizedCoordinates = false, // Use standard [0, 1] texture coordinates
compareEnable = false, // For percentage-closer filtering (shadow maps)
compareOp = .ALWAYS,
mipmapMode = .LINEAR, // Blend between mipmap levels
mipLodBias = 0.0,
minLod = 0.0,
maxLod = 0.0, // Can be set to a max value for a full mip chain
}
// 2. Create the Sampler
// ------------------------------------------------------------------
sampler: vulkan.Sampler
result := vulkan.CreateSampler(device, &sampler_info, nil, &sampler)
if result != .SUCCESS {
fmt.eprintln("Failed to create sampler:", result)
return
}
fmt.Println("Successfully created sampler.")
// `sampler` can now be combined with an image view in a descriptor set
// to be used by shaders.
// ... your application logic ...
// vulkan.DestroySampler(device, sampler, nil)
}
|
Like the “view” objects, a VkSampler
is a simple, self-contained object with a minimal API.
vkCreateSampler
: The function that creates the sampler object with its defined filtering and addressing properties.vkDestroySampler
: Destroys the sampler object.
A sampler’s primary role is to be paired with a VkImageView
in a VkDescriptorSet
. In a GLSL shader, this combination is typically accessed through a single sampler2D
uniform variable, which the shader uses to perform texture lookups.
A VkFence
is a synchronization primitive used to communicate from the GPU to the CPU. 🚩 When you submit work to a queue, the CPU doesn’t wait for the GPU to finish; it continues executing. A fence is an object you can submit along with that work. The GPU will “signal” the fence once the work is complete. Your CPU can then either poll the fence’s status or, more commonly, wait for it, which blocks the CPU thread until the GPU signals that it’s done. This is the primary mechanism for synchronizing your main application loop, for instance, ensuring that one frame is fully rendered before you start recording commands for the next one.
A typical fence lifecycle in a render loop involves creating it, submitting it with a command buffer, waiting on it, and then resetting it for the next frame.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| package main
import "vendor:vulkan"
import "core:fmt"
import "core:time"
main :: proc() {
// Assume `device` is a valid VkDevice and `graphics_queue` is a valid VkQueue.
device: vulkan.Device
graphics_queue: vulkan.Queue
if device == nil { return }
// 1. Create the Fence
// ------------------------------------------------------------------
// We create the fence in the "signaled" state so that the first call to
// vkWaitForFences in our render loop doesn't block forever.
fence_info := vulkan.FenceCreateInfo{
sType = .FENCE_CREATE_INFO,
flags = .CREATE_SIGNALED_BIT,
}
render_fence: vulkan.Fence
vulkan.CreateFence(device, &fence_info, nil, &render_fence)
// --- In your render loop ---
// 2. Wait for the previous frame to finish
// ------------------------------------------------------------------
// This will block until the GPU signals the fence from the *previous* frame's submission.
// The timeout is set to a very large value (effectively infinite).
vulkan.WaitForFences(device, 1, &render_fence, true, time.U64_MAX)
// 3. Reset the fence to the unsignaled state
// ------------------------------------------------------------------
// Before we can use the fence again for the current frame, it must be reset.
vulkan.ResetFences(device, 1, &render_fence)
// ... (Acquire image from swapchain, record command buffer, etc.) ...
// 4. Submit work with the fence
// ------------------------------------------------------------------
// Assume `command_buffer` has been recorded. When this submission is
// finished, the GPU will signal `render_fence`.
command_buffer: vulkan.CommandBuffer
submit_info := vulkan.SubmitInfo{
sType = .SUBMIT_INFO,
commandBufferCount = 1,
pCommandBuffers = &command_buffer,
}
vulkan.QueueSubmit(graphics_queue, 1, &submit_info, render_fence)
// --- End of render loop frame ---
fmt.Println("Fence submitted with work. The CPU can now continue until it waits again.")
// ... your application logic ...
// vulkan.DestroyFence(device, render_fence, nil)
}
|
These functions manage the lifecycle and state of fence objects.
vkCreateFence
: Creates a new fence object.vkDestroyFence
: Destroys a fence.vkWaitForFences
: This is the primary blocking function. It pauses a CPU thread and waits for one or more fences to become signaled by the GPU.vkResetFences
: Manually resets one or more fences from the signaled state back to the unsignaled state so they can be reused.vkGetFenceStatus
: This is a non-blocking check of a fence’s state. It returns immediately with either VK_SUCCESS
(if the fence is signaled) or VK_NOT_READY
(if it is unsignaled). This is useful if you want to poll the fence’s status while doing other work on the CPU.
A VkSemaphore
is a synchronization primitive used to coordinate operations between different batches of work on the GPU. 🏃♂️💨 Unlike a VkFence
which signals from GPU to CPU, a semaphore signals from GPU to GPU. Think of it like a baton in a relay race. One command submission runs its course and then signals a semaphore (hands off the baton). A second command submission can be configured to wait for that same semaphore before it begins executing (it can’t start until it receives the baton). This allows you to create dependencies between GPU tasks, such as ensuring that an image is ready from the swapchain before you start rendering to it, or ensuring rendering is finished before you present the image to the screen.
Semaphores are central to the render loop for synchronizing the swapchain, rendering, and presentation. You don’t wait on them from the CPU; you configure submissions to wait on and signal them on the GPU.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `device`, `swapchain`, `graphics_queue`, and `present_queue` are valid.
device: vulkan.Device
swapchain: vulkan.SwapchainKHR
graphics_queue: vulkan.Queue
present_queue: vulkan.Queue
if device == nil { return }
// 1. Create the Semaphores
// ------------------------------------------------------------------
// We need two for a typical render loop.
semaphore_info := vulkan.SemaphoreCreateInfo{ sType = .SEMAPHORE_CREATE_INFO }
image_available_semaphore: vulkan.Semaphore
render_finished_semaphore: vulkan.Semaphore
vulkan.CreateSemaphore(device, &semaphore_info, nil, &image_available_semaphore)
vulkan.CreateSemaphore(device, &semaphore_info, nil, &render_finished_semaphore)
// --- In your render loop ---
// 2. Acquire an image from the swapchain
// ------------------------------------------------------------------
// The GPU will signal `image_available_semaphore` when an image is ready.
image_index: u32
vulkan.AcquireNextImageKHR(device, swapchain, ~u64(0), image_available_semaphore, nil, &image_index)
// 3. Submit rendering work
// ------------------------------------------------------------------
// This submission will *wait* for the `image_available_semaphore` before it runs.
// After it finishes, it will *signal* the `render_finished_semaphore`.
command_buffer: vulkan.CommandBuffer // Assume this is recorded
wait_stages := [1]vulkan.PipelineStageFlags{.COLOR_ATTACHMENT_OUTPUT_BIT}
submit_info := vulkan.SubmitInfo {
sType = .SUBMIT_INFO,
waitSemaphoreCount = 1,
pWaitSemaphores = &image_available_semaphore,
pWaitDstStageMask = raw_data(wait_stages),
commandBufferCount = 1,
pCommandBuffers = &command_buffer,
signalSemaphoreCount = 1,
pSignalSemaphores = &render_finished_semaphore,
}
vulkan.QueueSubmit(graphics_queue, 1, &submit_info, nil) // No fence needed here
// 4. Present the image to the screen
// ------------------------------------------------------------------
// The presentation engine will *wait* for the `render_finished_semaphore`
// before showing the image.
present_info := vulkan.PresentInfoKHR {
sType = .PRESENT_INFO_KHR,
waitSemaphoreCount = 1,
pWaitSemaphores = &render_finished_semaphore,
swapchainCount = 1,
pSwapchains = &swapchain,
pImageIndices = &image_index,
}
vulkan.QueuePresentKHR(present_queue, &present_info)
// --- End of render loop frame ---
// ... cleanup ...
// vulkan.DestroySemaphore(device, render_finished_semaphore, nil)
// vulkan.DestroySemaphore(device, image_available_semaphore, nil)
}
|
A VkSemaphore
has a very simple direct API. Its power comes from how it’s used as a parameter in other functions to orchestrate GPU work.
vkCreateSemaphore
: Creates a new semaphore object.vkDestroySemaphore
: Destroys a semaphore.
Unlike a fence, a semaphore has no Wait
or Reset
functions for the CPU. Its state is managed entirely on the GPU as configured in these key function calls:
vkAcquireNextImageKHR
: Can be configured to signal a semaphore when a swapchain image is ready.vkQueueSubmit
: Can be configured to wait on one or more semaphores before executing and to signal one or more semaphores upon completion.vkQueuePresentKHR
: Can be configured to wait on one or more semaphores before presenting an image.
A VkDescriptorSetLayout
is a template or a blueprint that describes the content and layout of resources that will be accessed by a shader. 📜 Before you can tell a shader to use a specific uniform buffer or texture, you must first define the “shape” of all the inputs it expects. The layout specifies exactly what kinds of resources are needed (e.g., a uniform buffer, a combined image sampler), how many of them there are, and at which “binding” points the shader can find them. This layout is then used to create both the pipeline (so it knows what to expect) and the actual VkDescriptorSet
(which will contain the pointers to the real resources).
You create a layout by defining a collection of “bindings,” where each binding corresponds to a resource declaration in your shader code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `device` is a valid VkDevice.
device: vulkan.Device
if device == nil { return }
// 1. Define the layout bindings
// ------------------------------------------------------------------
// We'll define two bindings:
// - Binding 0: A Uniform Buffer Object (UBO) for the vertex shader.
// - Binding 1: A Combined Image Sampler for the fragment shader.
// These must match the `layout(binding = ...)` in your GLSL shader.
ubo_layout_binding := vulkan.DescriptorSetLayoutBinding{
binding = 0,
descriptorType = .UNIFORM_BUFFER,
descriptorCount = 1,
stageFlags = {.VERTEX_BIT},
pImmutableSamplers = nil,
}
sampler_layout_binding := vulkan.DescriptorSetLayoutBinding{
binding = 1,
descriptorType = .COMBINED_IMAGE_SAMPLER,
descriptorCount = 1,
stageFlags = {.FRAGMENT_BIT},
pImmutableSamplers = nil,
}
bindings := [2]vulkan.DescriptorSetLayoutBinding{ubo_layout_binding, sampler_layout_binding}
// 2. Create the Descriptor Set Layout
// ------------------------------------------------------------------
layout_info := vulkan.DescriptorSetLayoutCreateInfo{
sType = .DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
bindingCount = len(bindings),
pBindings = raw_data(bindings),
}
descriptor_set_layout: vulkan.DescriptorSetLayout
result := vulkan.CreateDescriptorSetLayout(device, &layout_info, nil, &descriptor_set_layout)
if result != .SUCCESS {
fmt.eprintln("Failed to create descriptor set layout:", result)
return
}
fmt.Println("Successfully created descriptor set layout.")
// This `descriptor_set_layout` is now a required ingredient for creating a
// pipeline layout and for allocating descriptor sets.
// ... your application logic ...
// vulkan.DestroyDescriptorSetLayout(device, descriptor_set_layout, nil)
}
|
This object is primarily a descriptor with a simple create/destroy lifecycle. Its importance lies in how it’s used to construct other objects.
vkCreateDescriptorSetLayout
: The function that creates the layout object from an array of binding descriptions.vkDestroyDescriptorSetLayout
: Destroys the layout object.
The VkDescriptorSetLayout
is a critical input for two other major operations:
- It’s used in
VkPipelineLayoutCreateInfo
to create a VkPipelineLayout
, which is then used to create a graphics or compute pipeline. This connects the shader resources to the pipeline itself. - It’s used in
VkDescriptorSetAllocateInfo
to allocate **VkDescriptorSet
**s from a VkDescriptorPool
. This ensures that the allocated descriptor sets have the correct structure defined by the layout.
A VkDescriptorPool
is an object that manages the memory for VkDescriptorSet
s. 🅿️ Think of it like reserving a section of a parking garage. Before you can “park” any cars (VkDescriptorSet
s), you must first reserve a pool of spots, specifying how many of each type you need (e.g., “10 spots for uniform buffers” and “10 spots for image samplers”). Allocating descriptor sets from this pre-allocated pool is a much faster operation than asking the driver for memory each time. You create a pool with enough capacity for all the descriptor sets you anticipate needing.
You create a pool by specifying the total number of sets it can allocate and the total number of each type of descriptor it can contain.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume `device` is a valid VkDevice.
device: vulkan.Device
if device == nil { return }
// 1. Define the pool sizes
// ------------------------------------------------------------------
// We specify how many of each descriptor type this pool can allocate in total.
// Let's say we need enough for 10 frames, each with one UBO and one sampler.
pool_sizes := [2]vulkan.DescriptorPoolSize{
{
type = .UNIFORM_BUFFER,
descriptorCount = 10,
},
{
type = .COMBINED_IMAGE_SAMPLER,
descriptorCount = 10,
},
}
// 2. Create the Descriptor Pool
// ------------------------------------------------------------------
pool_info := vulkan.DescriptorPoolCreateInfo{
sType = .DESCRIPTOR_POOL_CREATE_INFO,
flags = 0, // Set to .FREE_DESCRIPTOR_SET_BIT to allow freeing individual sets
maxSets = 10, // The maximum number of descriptor sets that can be allocated
poolSizeCount = len(pool_sizes),
pPoolSizes = raw_data(pool_sizes),
}
descriptor_pool: vulkan.DescriptorPool
result := vulkan.CreateDescriptorPool(device, &pool_info, nil, &descriptor_pool)
if result != .SUCCESS {
fmt.eprintln("Failed to create descriptor pool:", result)
return
}
fmt.Println("Successfully created descriptor pool.")
// This `descriptor_pool` can now be used to allocate descriptor sets.
// ... your application logic ...
// Destroying the pool automatically frees all descriptor sets allocated from it.
// vulkan.DestroyDescriptorPool(device, descriptor_pool, nil)
}
|
These functions manage the pool’s lifecycle and the descriptor sets within it.
vkCreateDescriptorPool
: The function that creates the descriptor pool object.vkDestroyDescriptorPool
: Destroys the pool, which also implicitly frees all VkDescriptorSet
s that were allocated from it.vkAllocateDescriptorSets
: This is the main purpose of a pool. You call this function to allocate one or more VkDescriptorSet
s that conform to a specific VkDescriptorSetLayout
.vkFreeDescriptorSets
: Frees one or more descriptor sets, returning their memory to the pool. This requires the pool to have been created with the VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
flag.vkResetDescriptorPool
: A more efficient way to reclaim all memory from a pool. It resets the pool back to its initial state, implicitly freeing or invalidating all descriptor sets that had been allocated from it.
A VkDescriptorSet
is the object that actually holds the “pointers” to the resources your shaders will use. 📝 If a VkDescriptorSetLayout
is the blueprint, the VkDescriptorSet
is the concrete instance built from that blueprint. It’s the “filled-out form” where you specify exactly which VkBuffer
, VkImageView
, and VkSampler
will be used for a particular draw call. You first allocate an empty descriptor set from a VkDescriptorPool
, then you update it with your resource handles. Finally, you bind this descriptor set to a command buffer, making all its resources available to the shaders used in subsequent draw commands.
The lifecycle involves three key steps: allocating the set from a pool, updating it with your resource info, and binding it to a command buffer before a draw call.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume you have:
// - A valid `device`.
// - A `descriptor_pool` with enough capacity.
// - A `descriptor_set_layout` that defines a UBO at binding 0 and a sampler at binding 1.
// - A `my_buffer` for the UBO and an `my_image_view` and `my_sampler` for the texture.
device: vulkan.Device
descriptor_pool: vulkan.DescriptorPool
descriptor_set_layout: vulkan.DescriptorSetLayout
my_buffer: vulkan.Buffer
my_image_view: vulkan.ImageView
my_sampler: vulkan.Sampler
if device == nil { return }
// 1. Allocate the Descriptor Set
// ------------------------------------------------------------------
alloc_info := vulkan.DescriptorSetAllocateInfo{
sType = .DESCRIPTOR_SET_ALLOCATE_INFO,
descriptorPool = descriptor_pool,
descriptorSetCount = 1,
pSetLayouts = &descriptor_set_layout,
}
descriptor_set: vulkan.DescriptorSet
vulkan.AllocateDescriptorSets(device, &alloc_info, &descriptor_set)
// 2. Update the Descriptor Set with resource info
// ------------------------------------------------------------------
// First, create the info structs that point to our resources.
buffer_info := vulkan.DescriptorBufferInfo{
buffer = my_buffer,
offset = 0,
range = vulkan.WHOLE_SIZE,
}
image_info := vulkan.DescriptorImageInfo{
imageLayout = .SHADER_READ_ONLY_OPTIMAL,
imageView = my_image_view,
sampler = my_sampler,
}
// Next, create the "write" operations that link these infos to the set's bindings.
descriptor_writes := [2]vulkan.WriteDescriptorSet{
{
sType = .WRITE_DESCRIPTOR_SET,
dstSet = descriptor_set,
dstBinding = 0, // Matches binding 0 in the layout
dstArrayElement = 0,
descriptorType = .UNIFORM_BUFFER,
descriptorCount = 1,
pBufferInfo = &buffer_info,
},
{
sType = .WRITE_DESCRIPTOR_SET,
dstSet = descriptor_set,
dstBinding = 1, // Matches binding 1 in the layout
dstArrayElement = 0,
descriptorType = .COMBINED_IMAGE_SAMPLER,
descriptorCount = 1,
pImageInfo = &image_info,
},
}
// Perform the update
vulkan.UpdateDescriptorSets(device, len(descriptor_writes), raw_data(descriptor_writes), 0, nil)
fmt.Println("Successfully allocated and updated descriptor set.")
// 3. Bind the Descriptor Set in a command buffer
// ------------------------------------------------------------------
command_buffer: vulkan.CommandBuffer // Assume this is in a recording state
pipeline_layout: vulkan.PipelineLayout // The pipeline layout created from the descriptor set layout
// vkCmdBindDescriptorSets(command_buffer, .GRAPHICS, pipeline_layout, 0, 1, &descriptor_set, 0, nil)
// Now any subsequent vkCmdDraw calls will use the resources from this descriptor set.
}
|
These functions cover the allocation, updating, and usage of descriptor sets.
vkAllocateDescriptorSets
: Allocates one or more VkDescriptorSet
s from a VkDescriptorPool
.vkUpdateDescriptorSets
: This is the crucial function for populating a descriptor set. You use it to write the handles of your buffers and images into the allocated set at the correct binding points.vkCmdBindDescriptorSets
: A command recorded into a VkCommandBuffer
. It binds one or more descriptor sets, making their resources active for use by subsequent draw or dispatch commands.vkFreeDescriptorSets
: Frees descriptor sets, returning their memory to the parent VkDescriptorPool
. The pool must have been created with the VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
flag to allow this.
A VkShaderModule
is a thin wrapper around compiled shader bytecode. 📄 Vulkan doesn’t work with high-level shader languages like GLSL directly. Instead, you must first compile your shaders into a standardized intermediate format called SPIR-V. The VkShaderModule
takes this raw SPIR-V bytecode and prepares it for the driver. Think of it like a .o
object file in C++; it’s a compiled, ready-to-use piece of code, but it’s not a complete program yet. You create a shader module for each shader stage (vertex, fragment, etc.), and these modules are then linked together during the creation of a VkPipeline
.
You create a shader module by providing a slice of SPIR-V bytecode, which you would typically load from a .spv
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| package main
import "vendor:vulkan"
import "core:fmt"
import "core:os" // For reading a file
main :: proc() {
// Assume `device` is a valid VkDevice.
device: vulkan.Device
if device == nil { return }
// 1. Load the SPIR-V bytecode from a file
// ------------------------------------------------------------------
// In a real application, you would read the contents of a compiled .spv file.
// The shader code must be aligned to 4 bytes (u32).
shader_code, ok := os.read_entire_file("path/to/shader.spv")
if !ok {
fmt.eprintln("Failed to read shader file.")
return
}
defer delete(shader_code)
// 2. Create the Shader Module
// ------------------------------------------------------------------
module_info := vulkan.ShaderModuleCreateInfo{
sType = .SHADER_MODULE_CREATE_INFO,
codeSize = len(shader_code),
// The API expects a pointer to u32, so we cast our byte slice pointer.
pCode = transmute(^u32)raw_data(shader_code),
}
shader_module: vulkan.ShaderModule
result := vulkan.CreateShaderModule(device, &module_info, nil, &shader_module)
if result != .SUCCESS {
fmt.eprintln("Failed to create shader module:", result)
return
}
fmt.Println("Successfully created shader module.")
// This `shader_module` is now ready to be used in a
// `VkPipelineShaderStageCreateInfo` struct when creating a pipeline.
// ... your application logic ...
// After a pipeline is created using this module, the module itself can be
// destroyed immediately to free up memory.
// vulkan.DestroyShaderModule(device, shader_module, nil)
}
|
This is one of the simplest Vulkan objects, with a minimal API focused solely on its creation and destruction.
vkCreateShaderModule
: The function that creates the shader module object from a buffer of SPIR-V bytecode.vkDestroyShaderModule
: Destroys the shader module object.
The only purpose of a VkShaderModule
is to be assigned to the module
member of a VkPipelineShaderStageCreateInfo
structure. This structure is then passed into the vkCreateGraphicsPipelines
or vkCreateComputePipelines
function. Once the pipeline has been successfully created, the VkShaderModule
is no longer needed by the driver and can (and should) be destroyed.
Dynamic Rendering is the modern way to perform rendering operations without pre-declaring render passes. 🎯 Instead of creating VkRenderPass
and VkFramebuffer
objects upfront, you specify the rendering attachments directly when you begin rendering. This dramatically simplifies the API and makes rendering more flexible. With dynamic rendering, you use vkCmdBeginRenderingKHR
with a VkRenderingInfoKHR
structure that describes your color and depth attachments right when you need them. This extension became part of core Vulkan 1.3 and is the recommended approach for new applications.
Dynamic rendering eliminates the need for render passes and framebuffers. You begin rendering directly with attachment information.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume you have:
// - A valid `command_buffer` in recording state.
// - `swapchain_image_view` for the color attachment.
// - `depth_image_view` for the depth attachment.
// - The `render_extent` (width and height).
command_buffer: vulkan.CommandBuffer
swapchain_image_view: vulkan.ImageView
depth_image_view: vulkan.ImageView
render_extent: vulkan.Extent2D
if command_buffer == nil { return }
// 1. Define Color Attachment Info
// ------------------------------------------------------------------
clear_color_value := vulkan.ClearValue{
color = {float32 = {0.0, 0.0, 0.0, 1.0}},
}
color_attachment := vulkan.RenderingAttachmentInfoKHR{
sType = .RENDERING_ATTACHMENT_INFO_KHR,
imageView = swapchain_image_view,
imageLayout = .COLOR_ATTACHMENT_OPTIMAL,
loadOp = .CLEAR,
storeOp = .STORE,
clearValue = clear_color_value,
}
// 2. Define Depth Attachment Info
// ------------------------------------------------------------------
clear_depth_value := vulkan.ClearValue{
depthStencil = {depth = 1.0, stencil = 0},
}
depth_attachment := vulkan.RenderingAttachmentInfoKHR{
sType = .RENDERING_ATTACHMENT_INFO_KHR,
imageView = depth_image_view,
imageLayout = .DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
loadOp = .CLEAR,
storeOp = .STORE,
clearValue = clear_depth_value,
}
// 3. Begin Dynamic Rendering
// ------------------------------------------------------------------
rendering_info := vulkan.RenderingInfoKHR{
sType = .RENDERING_INFO_KHR,
renderArea = {extent = render_extent},
layerCount = 1,
colorAttachmentCount = 1,
pColorAttachments = &color_attachment,
pDepthAttachment = &depth_attachment,
pStencilAttachment = nil, // We're not using stencil
}
vulkan.CmdBeginRenderingKHR(command_buffer, &rendering_info)
// --- Your draw commands go here ---
// vulkan.CmdBindPipeline(command_buffer, ...);
// vulkan.CmdDraw(command_buffer, ...);
// 4. End Dynamic Rendering
// ------------------------------------------------------------------
vulkan.CmdEndRenderingKHR(command_buffer)
fmt.Println("Dynamic rendering commands recorded successfully.")
// No render pass or framebuffer objects needed!
}
|
Dynamic rendering uses a much simpler set of functions compared to traditional render passes:
vkCmdBeginRenderingKHR
: Begins a dynamic rendering instance with the specified attachments. This replaces both vkCmdBeginRenderPass
and the need for a framebuffer.vkCmdEndRenderingKHR
: Ends the current dynamic rendering instance.vkGetPhysicalDeviceDynamicRenderingFeaturesKHR
: Query if dynamic rendering is supported on the physical device (part of core in Vulkan 1.3).VkPipelineRenderingCreateInfoKHR
: Structure used during pipeline creation to specify the attachment formats the pipeline will be used with, replacing the render pass reference.
A VkPipelineLayout
defines the interface between a pipeline and the resources used by its shaders. ↔️ Think of it as a function signature for your entire pipeline. In modern Vulkan, pipeline layouts support several resource binding methods:
- Traditional Descriptor Sets: The classic approach using
VkDescriptorSetLayout
s - Push Constants: Small amounts of very fast memory for frequently updated uniform data
- Push Descriptors (VK_KHR_push_descriptor): Allows updating descriptors directly in command buffers without descriptor sets
- Descriptor Buffers (VK_EXT_descriptor_buffer): Enables bindless resources by storing descriptors in GPU buffers
For bindless rendering, you typically use descriptor buffers or large descriptor arrays with UPDATE_AFTER_BIND flags, allowing shaders to dynamically index into thousands of resources without rebinding.
You create a VkPipelineLayout
by providing an array of the VkDescriptorSetLayout
s that the pipeline will use.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume you have:
// - A valid `device`.
// - A `descriptor_set_layout` for bindless resources (optional).
device: vulkan.Device
bindless_descriptor_layout: vulkan.DescriptorSetLayout // For bindless textures/buffers
if device == nil { return }
// 1. Specify the Pipeline Layout
// ------------------------------------------------------------------
// For bindless rendering, you might have one global descriptor set
// containing all your resources
set_layouts := [1]vulkan.DescriptorSetLayout{bindless_descriptor_layout}
// Define push constants for per-draw data (very common with bindless)
// This avoids updating descriptors for frequently changing data
push_constant_range := vulkan.PushConstantRange{
stageFlags = {.VERTEX_BIT, .FRAGMENT_BIT},
offset = 0,
size = 128, // 128 bytes for transform matrices, material indices, etc.
}
layout_info := vulkan.PipelineLayoutCreateInfo{
sType = .PIPELINE_LAYOUT_CREATE_INFO,
setLayoutCount = len(set_layouts),
pSetLayouts = raw_data(set_layouts),
pushConstantRangeCount = 1,
pPushConstantRanges = &push_constant_range,
}
// 2. Create the Pipeline Layout
// ------------------------------------------------------------------
pipeline_layout: vulkan.PipelineLayout
result := vulkan.CreatePipelineLayout(device, &layout_info, nil, &pipeline_layout)
if result != .SUCCESS {
fmt.eprintln("Failed to create pipeline layout:", result)
return
}
fmt.Println("Successfully created pipeline layout.")
// `pipeline_layout` is now a required parameter for creating a graphics pipeline.
// ... your application logic ...
// vulkan.DestroyPipelineLayout(device, pipeline_layout, nil)
}
|
A pipeline layout is a configuration object with a simple API. Its importance comes from how it connects other parts of the API.
vkCreatePipelineLayout
: The function that creates the pipeline layout object.vkDestroyPipelineLayout
: Destroys the pipeline layout.
The VkPipelineLayout
is a required parameter for these critical operations:
vkCreateGraphicsPipelines
/ vkCreateComputePipelines
: You must provide a pipeline layout when creating any pipeline.vkCmdBindDescriptorSets
: When binding descriptor sets, you must provide the pipeline layout to tell Vulkan how they are structured.vkCmdPushConstants
: If you defined push constant ranges, you use this command and the pipeline layout to send that data to the shaders.
A VkPipeline
(specifically a graphics pipeline) is a massive object that encapsulates nearly the entire state of the GPU for a rendering operation. 🏭 Think of it as the fully constructed GPU assembly line. It includes which shader programs to use, how to interpret vertex data, how to assemble vertices into shapes, how to convert those shapes into pixels (rasterization), how to handle depth and color blending, and much more. With modern Vulkan and dynamic rendering, pipelines are no longer tied to a specific render pass. Instead, you specify the attachment formats using VkPipelineRenderingCreateInfo
, making pipelines more flexible and reusable across different rendering scenarios.
Creating a graphics pipeline is the most complex object creation in Vulkan, as it requires configuring numerous fixed-function and programmable stages. The main VkGraphicsPipelineCreateInfo
struct points to many other configuration structs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// This is a highly complex setup. Assume you have:
// - A valid `device`.
// - `vertex_shader_module` and `fragment_shader_module`.
// - A `pipeline_layout`.
// - `swapchain_extent` for the viewport size.
device: vulkan.Device
vertex_shader_module: vulkan.ShaderModule
fragment_shader_module: vulkan.ShaderModule
pipeline_layout: vulkan.PipelineLayout
swapchain_extent: vulkan.Extent2D
if device == nil { return }
// 1. Define Shader Stages
vert_stage_info := vulkan.PipelineShaderStageCreateInfo{
sType = .PIPELINE_SHADER_STAGE_CREATE_INFO,
stage = .VERTEX_BIT,
module = vertex_shader_module,
pName = "main",
}
frag_stage_info := vulkan.PipelineShaderStageCreateInfo{
sType = .PIPELINE_SHADER_STAGE_CREATE_INFO,
stage = .FRAGMENT_BIT,
module = fragment_shader_module,
pName = "main",
}
shader_stages := [2]vulkan.PipelineShaderStageCreateInfo{vert_stage_info, frag_stage_info}
// --- Define Fixed-Function Stages ---
// 2. Vertex Input (we'll leave this empty, for now)
vertex_input_info := vulkan.PipelineVertexInputStateCreateInfo{ sType = .PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }
// 3. Input Assembly (draw triangles)
input_assembly := vulkan.PipelineInputAssemblyStateCreateInfo{
sType = .PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
topology = .TRIANGLE_LIST,
}
// 4. Viewport and Scissor (often set dynamically)
viewport := vulkan.Viewport{ width = f32(swapchain_extent.width), height = f32(swapchain_extent.height), maxDepth = 1.0 }
scissor := vulkan.Rect2D{ extent = swapchain_extent }
viewport_state := vulkan.PipelineViewportStateCreateInfo{
sType = .PIPELINE_VIEWPORT_STATE_CREATE_INFO,
viewportCount = 1, pViewports = &viewport,
scissorCount = 1, pScissors = &scissor,
}
// 5. Rasterizer
rasterizer := vulkan.PipelineRasterizationStateCreateInfo{
sType = .PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
polygonMode = .FILL,
lineWidth = 1.0,
cullMode = {.BACK_BIT},
frontFace = .CLOCKWISE,
}
// 6. Multisampling (disabled for now)
multisampling := vulkan.PipelineMultisampleStateCreateInfo{
sType = .PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
rasterizationSamples = ._1_BIT,
}
// 7. Depth/Stencil (disabled for now)
depth_stencil := vulkan.PipelineDepthStencilStateCreateInfo{ sType = .PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, }
// 8. Color Blending (disabled for now)
color_blend_attachment := vulkan.PipelineColorBlendAttachmentState{ colorWriteMask = {.R_BIT, .G_BIT, .B_BIT, .A_BIT} }
color_blending := vulkan.PipelineColorBlendStateCreateInfo{
sType = .PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
attachmentCount = 1,
pAttachments = &color_blend_attachment,
}
// 9. Specify Rendering Info for Dynamic Rendering
// ------------------------------------------------------------------
// Instead of a render pass, we specify the attachment formats
color_format := vulkan.Format.R8G8B8A8_UNORM // Your swapchain format
rendering_info := vulkan.PipelineRenderingCreateInfoKHR{
sType = .PIPELINE_RENDERING_CREATE_INFO_KHR,
colorAttachmentCount = 1,
pColorAttachmentFormats = &color_format,
depthAttachmentFormat = .D32_SFLOAT,
stencilAttachmentFormat = .UNDEFINED,
}
// 10. Create the Graphics Pipeline
pipeline_info := vulkan.GraphicsPipelineCreateInfo{
sType = .GRAPHICS_PIPELINE_CREATE_INFO,
pNext = &rendering_info, // Chain the rendering info
stageCount = len(shader_stages),
pStages = raw_data(shader_stages),
pVertexInputState = &vertex_input_info,
pInputAssemblyState = &input_assembly,
pViewportState = &viewport_state,
pRasterizationState = &rasterizer,
pMultisampleState = &multisampling,
pDepthStencilState = &depth_stencil,
pColorBlendState = &color_blending,
pDynamicState = nil, // Can be used to make viewport/scissor dynamic
layout = pipeline_layout,
renderPass = nil, // No render pass needed!
subpass = 0,
}
graphics_pipeline: vulkan.Pipeline
result := vulkan.CreateGraphicsPipelines(device, nil, 1, &pipeline_info, nil, &graphics_pipeline)
if result != .SUCCESS {
fmt.eprintln("Failed to create graphics pipeline:", result)
return
}
fmt.Println("Successfully created graphics pipeline.")
// ... cleanup ...
// vulkan.DestroyPipeline(device, graphics_pipeline, nil)
}
|
A pipeline is a massive, usually immutable state object. Its API is focused on creation and usage.
vkCreateGraphicsPipelines
/ vkCreateComputePipelines
: The functions that compile and link all the state and shader modules into a final, highly optimized pipeline object. You can create multiple pipelines in a single call.vkDestroyPipeline
: Destroys the pipeline object.vkCmdBindPipeline
: A command recorded into a VkCommandBuffer
. This is a crucial step in rendering. It sets the active pipeline for all subsequent draw (vkCmdDraw
) or dispatch (vkCmdDispatch
) calls. You cannot draw anything without first binding a pipeline.
Descriptor Buffers are the modern way to implement bindless rendering in Vulkan. 🚀 Instead of traditional descriptor sets, descriptors are stored directly in GPU buffers that can be indexed by shaders. This eliminates the descriptor set binding overhead and allows shaders to access thousands or millions of resources dynamically. With descriptor buffers, you write descriptor data directly into memory and provide buffer offsets when binding. This is perfect for bindless architectures where all textures, buffers, and samplers are accessible through indices rather than explicit bindings.
Descriptor buffers replace traditional descriptor sets with direct memory management of descriptors.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
| package main
import "vendor:vulkan"
import "core:fmt"
main :: proc() {
// Assume you have:
// - A valid `device` with descriptor buffer extension enabled
// - Multiple textures and buffers to make bindless
device: vulkan.Device
physical_device: vulkan.PhysicalDevice
if device == nil { return }
// 1. Query Descriptor Buffer Properties
// ------------------------------------------------------------------
db_props := vulkan.PhysicalDeviceDescriptorBufferPropertiesEXT{
sType = .PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_PROPERTIES_EXT,
}
device_props := vulkan.PhysicalDeviceProperties2{
sType = .PHYSICAL_DEVICE_PROPERTIES_2,
pNext = &db_props,
}
vulkan.GetPhysicalDeviceProperties2(physical_device, &device_props)
// 2. Create Descriptor Set Layout with DESCRIPTOR_BUFFER flag
// ------------------------------------------------------------------
// This layout describes a bindless array of textures
binding := vulkan.DescriptorSetLayoutBinding{
binding = 0,
descriptorType = .COMBINED_IMAGE_SAMPLER,
descriptorCount = 10000, // Large array for bindless
stageFlags = {.FRAGMENT_BIT},
}
binding_flags := vulkan.DescriptorBindingFlags.UPDATE_AFTER_BIND_BIT |
.PARTIALLY_BOUND_BIT |
.VARIABLE_DESCRIPTOR_COUNT_BIT
extended_info := vulkan.DescriptorSetLayoutBindingFlagsCreateInfo{
sType = .DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO,
bindingCount = 1,
pBindingFlags = &binding_flags,
}
layout_info := vulkan.DescriptorSetLayoutCreateInfo{
sType = .DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
pNext = &extended_info,
flags = {.DESCRIPTOR_BUFFER_BIT_EXT},
bindingCount = 1,
pBindings = &binding,
}
descriptor_layout: vulkan.DescriptorSetLayout
vulkan.CreateDescriptorSetLayout(device, &layout_info, nil, &descriptor_layout)
// 3. Get the size and create the descriptor buffer
// ------------------------------------------------------------------
layout_size: vulkan.DeviceSize
vulkan.GetDescriptorSetLayoutSizeEXT(device, descriptor_layout, &layout_size)
buffer_info := vulkan.BufferCreateInfo{
sType = .BUFFER_CREATE_INFO,
size = layout_size,
usage = {.RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT, .SHADER_DEVICE_ADDRESS_BIT},
}
descriptor_buffer: vulkan.Buffer
vulkan.CreateBuffer(device, &buffer_info, nil, &descriptor_buffer)
// 4. Map the buffer and write descriptors directly
// ------------------------------------------------------------------
// (After allocating and binding memory to the buffer)
mapped_memory: rawptr
vulkan.MapMemory(device, buffer_memory, 0, layout_size, 0, &mapped_memory)
// Get descriptor offsets and write image descriptors
for i in 0..<num_textures {
offset: vulkan.DeviceSize
vulkan.GetDescriptorEXT(device, &vulkan.DescriptorGetInfoEXT{
sType = .DESCRIPTOR_GET_INFO_EXT,
type = .COMBINED_IMAGE_SAMPLER,
data = {pCombinedImageSampler = &image_infos[i]},
}, db_props.combinedImageSamplerDescriptorSize,
cast(^u8)mapped_memory + offset)
}
vulkan.UnmapMemory(device, buffer_memory)
// 5. Bind descriptor buffer in command buffer
// ------------------------------------------------------------------
// In your command buffer recording:
buffer_info_bind := vulkan.DescriptorBufferBindingInfoEXT{
sType = .DESCRIPTOR_BUFFER_BINDING_INFO_EXT,
address = vulkan.GetBufferDeviceAddress(device, &vulkan.BufferDeviceAddressInfo{
sType = .BUFFER_DEVICE_ADDRESS_INFO,
buffer = descriptor_buffer,
}),
usage = .RESOURCE_BIT,
}
vulkan.CmdBindDescriptorBuffersEXT(command_buffer, 1, &buffer_info_bind)
// Set buffer offsets instead of binding descriptor sets
buffer_indices: u32 = 0
offsets: vulkan.DeviceSize = 0
vulkan.CmdSetDescriptorBufferOffsetsEXT(command_buffer,
.GRAPHICS_BIT,
pipeline_layout,
0, 1, &buffer_indices, &offsets)
// Now shaders can index into thousands of textures dynamically!
}
|
Descriptor buffers provide a more direct and efficient way to manage descriptors:
vkGetDescriptorSetLayoutSizeEXT
: Gets the size required for a descriptor set layout in a buffervkGetDescriptorSetLayoutBindingOffsetEXT
: Gets the offset of a specific binding within the layoutvkGetDescriptorEXT
: Writes descriptor data directly into mapped buffer memoryvkCmdBindDescriptorBuffersEXT
: Binds descriptor buffers for use in subsequent draw/dispatch commandsvkCmdSetDescriptorBufferOffsetsEXT
: Sets the offsets into bound descriptor buffersVK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT
: Allows descriptors to be updated after they’re boundVK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT
: Allows uninitialized descriptors in the array
With descriptor buffers, you achieve true bindless rendering where:
- All resources are accessible via indices
- No descriptor set allocation or updates during rendering
- Minimal CPU overhead for resource management
- Dynamic resource selection in shaders based on push constants or buffer data
Modern Vulkan applications typically combine several advanced features for optimal performance:
Buffer Device Address allows shaders to directly access buffers through GPU pointers rather than descriptors. This enables data structures like linked lists and trees on the GPU and is essential for ray tracing acceleration structures.
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Get buffer's GPU address
buffer_address := vulkan.GetBufferDeviceAddress(device, &vulkan.BufferDeviceAddressInfo{
sType = .BUFFER_DEVICE_ADDRESS_INFO,
buffer = my_buffer,
})
// Pass address via push constants or another buffer
push_data := struct {
vertex_buffer_addr: vulkan.DeviceAddress,
index_buffer_addr: vulkan.DeviceAddress,
}{buffer_address, index_address}
vulkan.CmdPushConstants(cmd, layout, {.VERTEX_BIT}, 0, size_of(push_data), &push_data)
|
Mesh Shaders replace the traditional vertex/geometry pipeline with a more flexible model. They generate primitives directly from compute-like shaders, enabling GPU-driven rendering and better culling.
1
2
3
4
5
6
7
| // Pipeline with mesh shaders instead of vertex shader
mesh_stage := vulkan.PipelineShaderStageCreateInfo{
sType = .PIPELINE_SHADER_STAGE_CREATE_INFO,
stage = .MESH_BIT_NV,
module = mesh_shader_module,
pName = "main",
}
|
Timeline Semaphores provide CPU-GPU and GPU-GPU synchronization with monotonically increasing values, replacing complex fence/semaphore combinations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Create timeline semaphore
timeline_info := vulkan.SemaphoreTypeCreateInfo{
sType = .SEMAPHORE_TYPE_CREATE_INFO,
semaphoreType = .TIMELINE,
initialValue = 0,
}
semaphore_info := vulkan.SemaphoreCreateInfo{
sType = .SEMAPHORE_CREATE_INFO,
pNext = &timeline_info,
}
timeline_semaphore: vulkan.Semaphore
vulkan.CreateSemaphore(device, &semaphore_info, nil, &timeline_semaphore)
|
With these modern features combined, you can implement GPU-driven rendering where:
- The GPU decides what to render using indirect draw commands
- Culling happens on the GPU using compute shaders
- Resources are accessed bindlessly
- No CPU intervention during frame rendering
This architecture minimizes CPU-GPU communication and enables rendering of massive scenes with millions of objects efficiently.