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.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);
}
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;
* 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.
{
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!");
}
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:
{
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!");
}
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);
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.// 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);
}
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.u32 winHeight = 600;
VulkanContext vk = win32_init_vulkan(instance,
100, 100, winWidth, winHeight,
"My Shiny Vulkan Window");
Next
Comments
Post a Comment