Vulkan Tutorial in C - 002 - Preparation

The VulkanContext Struct

Let’s take a look at the VulkanContext struct. It will store the Vulkan state we'll need that I think is pretty much generic between multiple apps we might have. There are other stuff that I decided not to include here which we'll see later on:
typedef struct
{
    HWND window;
    VkInstance instance;
    VkSurfaceKHR surface;
    VkPhysicalDevice physicalDevice;
    VkDevice device;
    u32 graphicsAndPresentQueueFamily;
    VkQueue graphicsAndPresentQueue;
    VkSwapchainKHR swapchain;
    VkFormat swapchainImageFormat;
    VkImage swapchainImages[2];
    VkImageView swapchainImageViews[2];
    VkExtent2D swapchainExtents;
} VulkanContext;
I included the HWND window handle for now, but later on I'll modify this tutorial to work on Linux as well. At that point I might change this struct to be platform independent.

From Vulkan docs tutorial, I highlighted a few interesting bits.

About the Vulkan instance:
The very first thing you need to do is initialize the Vulkan library by creating an instance. The instance is the connection between your application and the Vulkan library, and creating it involves specifying some details about your application to the driver.
About the surface:
[...] It exposes a VkSurfaceKHR object that represents an abstract type of surface to present rendered images to. [...]
The window surface needs to be created right after the instance creation, because it can actually influence the physical device selection.
About the physical device:
It’s actually possible that the queue families supporting drawing commands and the queue families supporting presentation do not overlap. Therefore, we have to take into account that there could be a distinct presentation queue.
I picked that bit because I'm not sure that's really worth it. In my system they do overlap, and because I think that'll also be the case for most people I'm going to assume the graphics and present queues are the same.

About Queues:
[...] almost every operation in Vulkan, anything from drawing to uploading textures, requires commands to be submitted to a queue. [...]
There are different types of queues that originate from different queue families, and each family of queues allows only a subset of commands. For example, there could be a queue family that only allows processing of compute commands or one that only allows memory transfer related commands.
This is a pretty important thing to understand about Vulkan.
Since I come from working with OpenGL and Direct3D 11, that's not at all how I'm used to think: in the older model, if we need to create a texture or upload data to it, we just call a function. If we want to draw, we just call a function.

In Vulkan, we record commands into a command buffer. Then, we submit that to the GPU.
That by itself is alright, but there's also synchronization:
[...] Queue submission and synchronization is configured through parameters in the VkSubmitInfo structure. [...] The first three parameters specify which semaphores to wait on before execution begins and in which stage(s) of the pipeline to wait. We want to wait for writing colors to the image until it’s available, so we’re specifying the stage of the graphics pipeline that writes to the color attachment. That means that theoretically, the implementation can already start executing our vertex shader and such while the image is not yet available.
From that bit of explanation from Vulkan docs tutorial, we can see that one of the mechanisms we have for sync are semaphores.

I think it's saying that when we submit a command buffer with draw commands, we want to tell Vulkan to wait until the next swapchain image is ready to start writing color values to it.
The part of the GPU pipeline that writes colors to the output image is the pixel/fragment shader, so everything before that doesn't need to wait on that semaphore.

Error Checking in This Tutorial

I'm just using assertions. Whenever something could fail, I assert that it worked as expected.

This is not production-ready, just a beginner’s tutorial--none of the code here is intended for a shippable application.

That said, keep the assertions. They will quickly identify when something goes wrong and help you save a lot of time.

About Resource Management

I'll not clean up resources at the end of the app. If you want to, be my guest.

I will free resources if they can be freed immediately, though.

Loading Files

Later in the tutorial, we'll need to load files into memory, specifically shaders. Since shaders are one of the last things we’ll tackle (and you’ll probably be pretty tired by then), let’s take care of this now while we're still fresh and get the file loading utility out of the way.
typedef struct
{
    void *data;
    size_t size;
} LoadedFile;

LoadedFile
load_entire_file(char *fileName)
{
    LoadedFile result = {0};

    FILE *handle;
    fopen_s(&handle, fileName, "rb");
    assert(handle);

    fseek(handle, 0, SEEK_END);
    result.size = ftell(handle);
    fseek(handle, 0, SEEK_SET);
    assert(result.size > 0);

    result.data = malloc(result.size);
    assert(result.data);

    size_t bytesRead = fread(result.data, 1, result.size, handle);
    assert(bytesRead == result.size);

    fclose(handle);

    return result;
}

Windows - Global Running Variable and Window Procedure

As the final step in this post, I'll define a globalRunning variable to keep track of whether our app is still running, and create the WindowProc:
static bool globalRunning;

LRESULT CALLBACK
vulkan_window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
    switch (message)
    {
        case WM_CREATE:
        {
            OutputDebugString("Window created\n");
        } break;

        case WM_SIZE:
        {
            OutputDebugString("Window resized\n");
        } break;

        case WM_CLOSE:
        case WM_DESTROY:
        {
            globalRunning = false;
        } break;

        default:
        {
            return DefWindowProc(window, message, wparam, lparam);
        } break;
    }

    return 0;
}

A Word of Caution

The WindowProc function can be a bit finicky. For instance, in the default case, we return the result of calling DefWindowProc. If we were to omit this return statement, the window creation would silently fail without any error messages, which can be extremely frustrating to debug. I made this mistake when writing the code, and it took me a while to figure out what went wrong.

Another potential pitfall is handling certain messages, like WM_PAINT. If we were to handle that incorrectly, it would cause issues with the app, again without providing any error messages to guide us. (Way to go Windows!)

Finally, notice that in the WM_CLOSE and WM_DESTROY cases, I set globalRunning = false. That's crucial—without it, our app’s process would continue running indefinitely (at full speed) after the window is closed. The app would only terminate when you manually close it in Task Manager or shut down the computer.

Conclusion

In the next post, let's start initializing Vulkan itself.

Next

Comments

Popular Posts