Vulkan Tutorial in C - 003 - Initialization Part 1

Setting Up Debugging

Before we dive into Vulkan initialization, let's define a debug callback. This function will be called whenever a validation layer detects an issue or warning in our Vulkan code. Having this in place will help us catch mistakes early and understand what's going wrong.
static VKAPI_ATTR VkBool32 VKAPI_CALL
vulkan_debug_callback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
                      VkDebugUtilsMessageTypeFlagsEXT messageType,
                      const VkDebugUtilsMessengerCallbackDataEXT *callbackData,
                      void *userData)
{
    char buffer[4096] = {0};
    sprintf_s(buffer, sizeof(buffer), "Vulkan Validation layer: %s\n",
              callbackData->pMessage);
    OutputDebugString(buffer);
    
    return VK_FALSE;
}
The VKAPI_ATTR and VKAPI_CALL macros ensure compatibility across different platforms and calling conventions. Since Vulkan is a cross-platform API, different operating systems may use different calling conventions. While we could remove these macros, doing so might cause issues if we ever decide to port our code to another OS.

Don't worry too much about the function and its parameters—just know that whenever Vulkan's validation layer detects an issue, it will automatically call this function. Here, we simply take the error message and print it to Visual Studio's Output window, making it easy to debug problems as they arise.

The win32_init_vulkan function

Before we dive in, here's an overview of our initialization function. Since Vulkan setup involves many steps, this function will be quite large. However, I’ve chosen to keep everything in a single function rather than splitting it into smaller ones. In my experience, this makes it easier to follow the flow of Vulkan initialization rather than jumping between functions.

That said, this is just my personal preference—you’re free to structure it however you like. If breaking it into multiple functions helps you understand it better, go for it!
VulkanContext
win32_init_vulkan(HINSTANCE instance, s32 windowX, s32 windowY, u32 windowWidth,
                  u32 windowHeight, char *windowTitle)
{
    VulkanContext vk = {0};

    // Create window
    // Set up enabled layers and extensions
    // Create Vulkan Instance
    // Set up debug callback
    // Create surface

    // Pick a physical device and the graphicsAndPresent queue family
    // Create logical device
    // Get graphicsAndPresentQueue from device
    // Create swapchain
    // Get swapchain images and create their views

    return vk;
}
In this post, we'll cover the first five steps of this function. The remaining five steps will be covered in the next post.

Create window

The first step in initializing Vulkan is creating a window. Since Vulkan doesn't provide its own windowing system, we use the Win32 API to set one up. The following code goes inside win32_init_vulkan:
// Register window class
WNDCLASSEX winClass =
{
    sizeof(WNDCLASSEX),
    0, // style
    vulkan_window_proc, // window procedure
    0, // cbClsExtra
    0, // cbWndExtra
    instance, // hInstance
    NULL, // hIcon
    NULL, // hCursor
    NULL, // hbrBackground
    NULL, // lpszMenuName
    "MyUniqueVulkanWindowClassName",
    NULL, // hIconSm
};

if (!RegisterClassEx(&winClass))
{
    assert(!"Failed to register window class");
}

// Make sure the window is not resizable for simplicity
DWORD windowStyle = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;

RECT windowRect =
{
    windowX, // left
    windowY, // top
    windowX + windowWidth, // right
    windowY + windowHeight, // bottom
};

AdjustWindowRect(&windowRect, windowStyle, 0);

windowWidth = windowRect.right - windowRect.left;
windowHeight = windowRect.bottom - windowRect.top;
windowX = windowRect.left;
windowY = windowRect.top;

// Create window
vk.window = CreateWindowEx(0, // Extended style
                           winClass.lpszClassName,
                           windowTitle,
                           windowStyle,
                           windowX, windowY, windowWidth, windowHeight,
                           NULL, NULL, instance, NULL);

if (!vk.window)
{
    assert(!"Failed to create window");
}

ShowWindow(vk.window, SW_SHOW);
Before creating a window, we need to register a window class, which describes how our window behaves. The WNDCLASSEX struct holds information such as the window procedure (vulkan_window_proc), class name, and application instance. Once set up, we register it using RegisterClassEx.

We define a RECT to represent the window's desired dimensions, then use AdjustWindowRect to ensure the size accounts for window decorations (title bar, borders, etc.). Without this adjustment, the client area of the window (the part we can draw on) might be smaller than expected, which would cause problems later.

With the class registered, we call CreateWindowEx, passing in the adjusted dimensions, the class name, and the window title.

Finally, ShowWindow makes the window visible. Without this, the window exists but remains hidden.

Set up enabled layers and extensions

// Query available instance layers
u32 propertyCount = 0;
vkEnumerateInstanceLayerProperties(&propertyCount, NULL);
assert(propertyCount <= 32); // Ensure we don't exceed our fixed-size array

VkLayerProperties layerProperties[32];
vkEnumerateInstanceLayerProperties(&propertyCount, layerProperties);

char *validationLayerName = "VK_LAYER_KHRONOS_validation";

// Check if the requested validation layer is available
bool validationLayerFound = false;
for (u32 i = 0; i < propertyCount; i++)
{
    if (strcmp(validationLayerName, layerProperties[i].layerName) == 0)
    {
        validationLayerFound = true;
        break;
    }
}

assert(validationLayerFound && "Validation layer not found!");
char *enabledLayers[] = { validationLayerName };

char *extensions[] =
{
    // These defines are used instead of raw strings for future compatibility
    VK_KHR_SURFACE_EXTENSION_NAME, // "VK_KHR_surface"
    VK_KHR_WIN32_SURFACE_EXTENSION_NAME, // "VK_KHR_win32_surface"
    VK_EXT_DEBUG_UTILS_EXTENSION_NAME // "VK_EXT_debug_utils"
};

Vulkan Approach

Almost all (if not all) Vulkan functions take a struct that configures how the function behaves. These structs serve as a way to pass parameters to functions. Instead of calling a function with multiple individual parameters, we pass a single struct that contains all the necessary fields. Some of these fields may themselves be structs with even more fields. This design is likely a result of Vulkan’s focus on giving developers as much control as possible, meaning we need to explicitly provide every detail about what we want Vulkan to do.

It’s also worth noting that these structs often follow a common format. They typically start with two specific fields: sType and pNext. For example, a struct might look like this:
VkSomeVulkanStruct
{
    VkStructureType sType;
    const void *pNext;
    // other fields
};
The sType field is literally just specifying what type of struct this is. I’m not sure why they decided to add these two fields to every struct—because we’ll basically be ignoring them—but we still need to specify the type.

I’m mentioning this now because I’ve decided not to overload the code with redundant comments, such as a // pNext comment in every struct where we pass NULL as the second field. Also, keep in mind that the sType field often has verbose names like VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, and so on.

Additionally, very often there is a flags field after sType and pNext, which we will also be ignoring most of the time. So, I won’t be commenting on that either. If you see a NULL and a 0 after the type field in the structs (and you will see it), you can be 99% sure that it's the flags field, which we are ignoring.

Create Vulkan Instance

/* This struct is technically optional, but it's worth adding for a nicer
display when inspecting the app with tools like RenderDoc. */
VkApplicationInfo appInfo =
{
    VK_STRUCTURE_TYPE_APPLICATION_INFO,
    NULL,
    "My Clever App Name",
    1, // application Version
    "My Even Cleverer Engine Name",
    1, // engine Version
    VK_API_VERSION_1_3
};

/* This struct is necessary. The main purpose of this is to inform the
Vulkan driver about which layers and extensions to load when calling
vkCreateInstance. */

VkInstanceCreateInfo createInfo =
{
    VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
    NULL,
    0, // flags (this is the only time I'm commenting on this)
    &appInfo,
    array_count(enabledLayers), // layer count
    enabledLayers, // layers to enable
    array_count(extensions), // extension count
    extensions // extension names
};

if (vkCreateInstance(&createInfo, NULL,
                     &vk.instance) != VK_SUCCESS)
{
    assert(!"Failed to create vulkan instance");
}

Set up debug callback

VkDebugUtilsMessageSeverityFlagsEXT messageSeverity =
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;

VkDebugUtilsMessageTypeFlagsEXT messageType =
    VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;

VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo =
{
    VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
    NULL,
    0,
    messageSeverity,
    messageType,
    vulkan_debug_callback,
    NULL // user data
};

// Load the debug utils extension function
PFN_vkCreateDebugUtilsMessengerEXT vkCreateDebugUtilsMessengerEXT =
(PFN_vkCreateDebugUtilsMessengerEXT)
    vkGetInstanceProcAddr(vk.instance, "vkCreateDebugUtilsMessengerEXT");

VkDebugUtilsMessengerEXT debugMessenger;
if (vkCreateDebugUtilsMessengerEXT(vk.instance, &debugCreateInfo, NULL,
                                   &debugMessenger) != VK_SUCCESS)
{
    assert(!"Failed to create debug messenger!");
}

Create surface

In Vulkan, a surface is an abstraction that represents "something capable of displaying an image we've rendered to." In practical terms, this could be the window in windowed mode or the monitor in fullscreen mode.

The key idea is that the Vulkan team introduced this abstraction to avoid hardcoding OS-specific code for interacting with the window or display we are rendering to. Instead of dealing with platform-specific details directly, Vulkan provides a unified interface.

Before we can even access the GPU, we must create this surface, and the process will vary slightly depending on the platform (Windows, Linux, Android, etc.). On platforms like Android or Linux, you'll typically need to fill in a different VkSurfaceCreateInfo struct and pass in platform-specific objects.

For Windows, as shown here, we're passing the HINSTANCE and HWND—which we created earlier—into the surface creation function:
VkWin32SurfaceCreateInfoKHR surfaceCreateInfo =
{
    VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
    NULL,
    0,
    instance, // HINSTANCE
    vk.window // HWND
};

if (vkCreateWin32SurfaceKHR(vk.instance, &surfaceCreateInfo, NULL,
                            &vk.surface) != VK_SUCCESS)
{
    assert(!"Failed to create surface");
}
It's important not to confuse this Windows HINSTANCE with vk.instance, which is the Vulkan instance.
That concludes part 1 of the initialization. We've completed the first five steps of the win32_init_vulkan function. In the next post, we'll finish the remaining five steps.

Next

Comments

Popular Posts