Vulkan Tutorial in C - 011 - Uniform and Vertex Buffers

This marks the beginning of the third app, where we add uniform and vertex buffers. You can find the full source code here.
Now that our app is working with textures, let's add a uniform buffer to incorporate a projection matrix. Additionally, we'll create a simple static vertex buffer to remove the hardcoded vertex data from our current vertex shader.

Create Buffer function

First, after the vk_find_memory_type function, let's create a helper function to handle buffer creation:
void
vk_create_buffer(VulkanContext *vk, VkDeviceSize size,
                 VkBufferUsageFlags usage,
                 VkMemoryPropertyFlags properties,
                 VkBuffer *buffer, VkDeviceMemory *bufferMemory)
{
    VkBufferCreateInfo bufferInfo =
    {
        VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
        NULL,
        0,
        size,
        usage,
        VK_SHARING_MODE_EXCLUSIVE,
        0, NULL
    };
    
    if (vkCreateBuffer(vk->device, &bufferInfo, NULL,
                       buffer) != VK_SUCCESS)
    {
        assert(!"Failed to create buffer!");
    }
    
    // Get Memory Requirements
    VkMemoryRequirements memRequirements;
    vkGetBufferMemoryRequirements(vk->device, *buffer, &memRequirements);
    
    // Allocate Memory
    VkMemoryAllocateInfo allocInfo =
    {
        VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        NULL,
        memRequirements.size,
        vk_find_memory_type(vk, memRequirements.memoryTypeBits, properties)
    };
    
    if (vkAllocateMemory(vk->device, &allocInfo, NULL,
                         bufferMemory) != VK_SUCCESS)
    {
        assert(!"Failed to allocate buffer memory!");
    }
    
    // Bind Memory
    vkBindBufferMemory(vk->device, *buffer, *bufferMemory, 0);
}
This function will create both the VkBuffer and VkBufferMemory that are passed to it. You could wrap these in a helper struct to simplify the code and avoid passing them as parameters (which is what I do in my own code), but I decided not to introduce that in this tutorial. I find that such abstractions can complicate things for beginners rather than make them easier to understand.

I realized I forgot to prefix the create_shader_module function with vk, so for consistency, I'll rename it to vk_create_shader_module. Be sure to update the code where it’s used as well.

Now, right after the texture-related Vulkan objects, let's add the objects for our uniform and vertex buffers:
/*
* Uniform Buffer Vulkan Objects
*/
    
VkBuffer uniformBuffer;
VkDeviceMemory uniformBufferMemory;

/*
* Vertex Buffer Vulkan Objects
*/

VkBuffer vertStagingBuffer;
VkDeviceMemory vertStagingBufferMemory;

VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;

Update Descriptor Set Layout

Let's update the descriptor set layout. Unfortunately, I missed a spot where I should have used an array. Instead, I used the trick of passing the address of a struct to make it appear as a single-element array. With code this large, it was bound to happen, and it showed up here. Hopefully, you caught that in the last tutorial, and it didn’t cause too much confusion:
VkDescriptorSetLayoutBinding descSetLayoutBinding1 =
{
    0, // binding
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
    1, // descriptorCount
    VK_SHADER_STAGE_FRAGMENT_BIT,
    NULL // pImmutableSamplers
};

VkDescriptorSetLayoutBinding descSetLayoutBinding2 =
{
    1, // binding
    VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
    1, // descriptorCount
    VK_SHADER_STAGE_VERTEX_BIT,
    NULL // pImmutableSamplers
};

VkDescriptorSetLayoutBinding descSetLayoutBindings[] =
{
    descSetLayoutBinding1,
    descSetLayoutBinding2
};

VkDescriptorSetLayoutCreateInfo descSetLayoutInfo =
{
    VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
    NULL,
    0,
    array_count(descSetLayoutBindings),
    descSetLayoutBindings
};

if (vkCreateDescriptorSetLayout(vk.device, &descSetLayoutInfo, NULL,
                                &descSetLayout) != VK_SUCCESS)
{
    assert(!"Failed to create descriptor set layout!");
}
As you can see, we're now passing two VkDescriptorSetLayoutBinding objects—one for the image sampler and one for the uniform buffer. I've also corrected the code to pass the array to VkDescriptorSetLayoutCreateInfo, as I should have done from the start.
I don’t mean to suggest that passing the address of a struct to make it appear as an array with a single element is something you shouldn’t do. I’m just saying that I prefer not to do that in this tutorial, as it might confuse beginners.

Update the Descriptor Pool

Now, let's update the descriptor pool. Similar to what we just did, we'll pass two VkDescriptorPoolSize objects—one for the image sampler and one for the uniform buffer:
VkDescriptorPoolSize descPoolSize1 =
{
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
    1 // descriptorCount
};

VkDescriptorPoolSize descPoolSize2 =
{
    VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
    1 // descriptorCount
};

VkDescriptorPoolSize descPoolSizes[] =
{
    descPoolSize1,
    descPoolSize2
};

VkDescriptorPoolCreateInfo descPoolInfo =
{
    VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
    NULL,
    0,
    1, // maxSets
    array_count(descPoolSizes),
    descPoolSizes
};

if (vkCreateDescriptorPool(vk.device, &descPoolInfo, NULL,
                           &descPool) != VK_SUCCESS)
{
    assert(!"Failed to create descriptor pool!");
}
That’s it for the descriptor system for now. Previously, we were creating the staging buffer for our texture in place, but now that we have a helper function for it, let’s replace that code with a function call:
VkBuffer texStagingBuffer;
VkDeviceMemory texStagingBufferMemory;
    
vk_create_buffer(&vk, texDataSize,
                 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                 &texStagingBuffer, &texStagingBufferMemory);
Make sure to replace the "Create the Staging Buffer", "Allocate Memory for the Staging Buffer", and "Bind the Buffer to the Allocated Memory" sections with our new, shiny function call.

Create Uniform Buffer

Now, right after we create the image sampler, let's create our uniform buffer. We won’t be using a staging buffer for the uniform buffer. Instead, we'll request host-visible memory because, later on, we’ll modify this code to keep the uniform buffer mapped permanently, allowing us to update its data easily every frame.

For now, we’ll update it once and unmap it, but since our goal is to change it later, we’re skipping the extra steps of creating a staging buffer, which we’d just remove down the line anyway:
VkDeviceSize uniBufferSize = sizeof(f32) * 4 * 4;
    
// Create the buffer (standard Vulkan buffer creation)
vk_create_buffer(&vk, uniBufferSize,
                 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                 &uniformBuffer, &uniformBufferMemory);

// Map memory to update the projection matrix
{
    void *data;
    vkMapMemory(vk.device, uniformBufferMemory, 0, uniBufferSize, 0,
                &data);
    
    f32 projectionMatrix[] =
    {
        2.0f / (f32)winWidth, 0, 0, -1,
        0, 2.0f / (f32)winHeight, 0, -1,
        0, 0, 1, 0,
        0, 0, 0, 1
    };
    
    memcpy(data, &projectionMatrix, uniBufferSize);
    
    vkUnmapMemory(vk.device, uniformBufferMemory);
}
Notice that I’ve hardcoded a very simple projection matrix to convert from NDC to pixel coordinates. It uses row-major notation and assumes Vulkan’s NDC origin is at the top-left.

At the very top of the WinMain function, update the call to win32_init_vulkan to use the winWidth and winHeight variables:
u32 winWidth = 800;
u32 winHeight = 600;

VulkanContext vk = win32_init_vulkan(instance,
                                     100, 100, winWidth, winHeight,
                                     "My Shiny Vulkan Window");
In the next post, we’ll continue updating our code to reflect the changes we’ve made so far, including modifying our shaders. See you there.

Next

Comments

Popular Posts