VkInstance

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)
}

Key API Methods for VkInstance

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.

VkPhysicalDevice

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.


Selecting a VkPhysicalDevice in Odin

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.
}

Key API Methods for VkPhysicalDevice

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.

vkSurfaceKHR

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.


Creating a VkSurfaceKHR in Odin (with SDL3)

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)
}

Key API Methods for VkSurfaceKHR

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).

vkSwapchainKHR

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 VkSwapchainKHR in Odin

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)
}

Key API Methods for VkSwapchainKHR

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.

vkDevice

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 in Odin

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)
}

Key API Methods for VkDevice

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.

vkQueue

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.


Getting a VkQueue in Odin

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)
}

Key API Methods for VkQueue

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.

vkCommandPool

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.


Creating a VkCommandPool in Odin

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)
}

Key API Methods for VkCommandPool

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 VkCommandBuffers 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.

vkCommandBuffer

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.


Allocating and Using a VkCommandBuffer in Odin

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.
}

Key API Methods for VkCommandBuffer

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.

vkBuffer

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.


Creating and Binding a VkBuffer in Odin

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)
}

Key API Methods for VkBuffer

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.

vkBufferView

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.”


Creating a VkBufferView in Odin

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)
}

Key API Methods for VkBufferView

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.

vkImage

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.


Creating a VkImage in Odin

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)
}

Key API Methods for VkImage

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.

vkImageView

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.


Creating a VkImageView in Odin

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)
}

Key API Methods for VkImageView

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.

vkSampler

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.


Creating a VkSampler in Odin

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)
}

Key API Methods for VkSampler

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.

vkFence

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.


Creating and Using a VkFence in Odin

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)
}

Key API Methods for VkFence

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.

vkSemaphore

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.


Creating and Using VkSemaphore in Odin

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)
}

Key API Methods for VkSemaphore

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.

vkDescriptorSetLayout

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).


Creating a VkDescriptorSetLayout in Odin

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)
}

Key API Methods for VkDescriptorSetLayout

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.

vkDescriptorPool

A VkDescriptorPool is an object that manages the memory for VkDescriptorSets. 🅿️ Think of it like reserving a section of a parking garage. Before you can “park” any cars (VkDescriptorSets), 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.


Creating a VkDescriptorPool in Odin

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)
}

Key API Methods for VkDescriptorPool

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 VkDescriptorSets that were allocated from it.
  • vkAllocateDescriptorSets: This is the main purpose of a pool. You call this function to allocate one or more VkDescriptorSets 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.

vkDescriptorSet

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.


Allocating and Using a VkDescriptorSet in Odin

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.
}

Key API Methods for VkDescriptorSet

These functions cover the allocation, updating, and usage of descriptor sets.

  • vkAllocateDescriptorSets: Allocates one or more VkDescriptorSets 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.

vkShaderModule

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.


Creating a VkShaderModule in Odin

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)
}

Key API Methods for VkShaderModule

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 (VK_KHR_dynamic_rendering)

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.


Using Dynamic Rendering in Odin

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!
}

Key API Methods for Dynamic Rendering

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.

vkPipelineLayout

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:

  1. Traditional Descriptor Sets: The classic approach using VkDescriptorSetLayouts
  2. Push Constants: Small amounts of very fast memory for frequently updated uniform data
  3. Push Descriptors (VK_KHR_push_descriptor): Allows updating descriptors directly in command buffers without descriptor sets
  4. 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.


Creating a VkPipelineLayout in Odin

You create a VkPipelineLayout by providing an array of the VkDescriptorSetLayouts 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)
}

Key API Methods for VkPipelineLayout

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.

vkPipeline

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 VkPipeline in Odin

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)
}

Key API Methods for VkPipeline

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.

VkDescriptorBuffer (VK_EXT_descriptor_buffer)

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.


Using Descriptor Buffers for Bindless in Odin

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!
}

Key API Methods for Descriptor Buffers

Descriptor buffers provide a more direct and efficient way to manage descriptors:

  • vkGetDescriptorSetLayoutSizeEXT: Gets the size required for a descriptor set layout in a buffer
  • vkGetDescriptorSetLayoutBindingOffsetEXT: Gets the offset of a specific binding within the layout
  • vkGetDescriptorEXT: Writes descriptor data directly into mapped buffer memory
  • vkCmdBindDescriptorBuffersEXT: Binds descriptor buffers for use in subsequent draw/dispatch commands
  • vkCmdSetDescriptorBufferOffsetsEXT: Sets the offsets into bound descriptor buffers
  • VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT: Allows descriptors to be updated after they’re bound
  • VK_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

Additional Modern Vulkan Features

Modern Vulkan applications typically combine several advanced features for optimal performance:

Buffer Device Address (BVA)

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

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

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)

GPU-Driven Rendering

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.