Vulkan Tutorial in C - 013 - Dynamic Vertex Buffer

This marks the beginning of the fourth app, where we add a dynamic vertex buffer. You can find the full source code here.

Dynamic Vertex Buffer

In the last tutorial, we introduced a uniform buffer and a static vertex buffer to our app. However, static vertex buffers aren’t meant to be updated frequently. While they’re great for storing data that doesn’t change every frame—and we should use them whenever possible for efficiency—sometimes we need to update data dynamically. To handle that, we’ll introduce the concept of a dynamic vertex buffer.

In Vulkan, this isn’t a distinct feature but rather a different way of managing memory. The vertex buffer itself is no different from the one we created earlier. The key difference is that this time, we’ll request host-visible, coherent memory, map it once at the start of the app, and keep it mapped so we can update its contents every frame.

Unique Vertex Attributes

You may have noticed that when we bound our Static Vertex Buffer in the previous tutorial, we passed an array of vertex buffers—even though it contained only a single buffer. This might lead you to think that we could simply add our dynamic vertex buffer to that array and be done with it. However, that's not the case.

The reason is that in Vulkan, VkVertexInputAttributeDescription requires each attribute to have a unique location, even if they come from different buffer bindings. This restriction means we cannot provide multiple vertex buffers with completely different sets of data within the same pipeline.

The ability to pass multiple vertex buffers is designed to split attributes across different buffers. For example, we could have one vertex buffer storing positions and another storing UV coordinates, both describing the same game objects.

But that’s not what we need here. In our case, each vertex buffer should contain a complete set of data. We want a static vertex buffer holding both positions and UVs for static objects and a dynamic vertex buffer holding both positions and UVs for dynamic objects. Unfortunately, Vulkan doesn’t allow this within a single graphics pipeline.

So, the first step is to move our pipeline creation code into a helper function.

Create Graphics Pipeline function

void
vk_create_graphics_pipeline(VulkanContext *vk,
                            u32 descSetLayoutCount,
                            VkDescriptorSetLayout *descSetLayouts,
                            u32 shaderStageInfoCount,
                            VkPipelineShaderStageCreateInfo *shaderStageInfos,
                            VkPipelineVertexInputStateCreateInfo vertInputStateInfo,
                            VkRenderPass renderPass,
                            VkPipelineLayout *pipelineLayout,
                            VkPipeline *graphicsPipeline)
{
    /*
    * Define Dynamic State Crate Info
    */
    
    VkPipelineDynamicStateCreateInfo dynamicStateInfo =
    {
        VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
        NULL,
        0,
        0, NULL // No dynamic states
    };
    
    /*
    * Define Viewport State Create Info
    */
    
    VkViewport viewport =
    {
        0, 0, // x, y
        (f32)vk->swapchainExtents.width,
        (f32)vk->swapchainExtents.height,
        0, 0 // min, max depth
    };
    
    VkViewport viewports[] = { viewport };
    
    VkRect2D scissor =
    {
        {0, 0}, // offset
        vk->swapchainExtents
    };
    
    VkRect2D scissors[] = { scissor };
    
    VkPipelineViewportStateCreateInfo viewportStateInfo =
    {
        VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
        NULL,
        0,
        array_count(viewports),
        viewports,
        array_count(scissors),
        scissors
    };
    
    /*
    * Define Rasterization State Create Info
    */
    
    VkPipelineRasterizationStateCreateInfo rasterizationStateInfo =
    {
        VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
        NULL,
        0,
        VK_FALSE, // depthClampEnable
        VK_FALSE, // rasterizerDiscardEnable
        VK_POLYGON_MODE_FILL, // polygonMode (solid triangles)
        VK_CULL_MODE_BACK_BIT, // cullMode
        VK_FRONT_FACE_CLOCKWISE, // frontFace
        VK_FALSE, 0, 0, 0, // no depth bias
        1.0f // lineWidth
    };
    
    /*
    * Define Multisample State Create Info
    */
    
    VkPipelineMultisampleStateCreateInfo multisampleStateInfo =
    {
        VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
        NULL,
        0,
        VK_SAMPLE_COUNT_1_BIT, // rasterizationSamples
        VK_FALSE, // sampleShadingEnable
        0, // minSampleShading
        NULL, // pSampleMask
        VK_FALSE, // alphaToCoverageEnable
        VK_FALSE, // alphaToOneEnable
    };
    
    /*
    * Define Color Blend State Create Info
    */
    
    VkFlags colorWriteMask =
        VK_COLOR_COMPONENT_R_BIT |
        VK_COLOR_COMPONENT_G_BIT |
        VK_COLOR_COMPONENT_B_BIT |
        VK_COLOR_COMPONENT_A_BIT;
    
    VkPipelineColorBlendAttachmentState colorBlendAttachment =
    {
        VK_TRUE, // blendEnable
        VK_BLEND_FACTOR_SRC_ALPHA,
        VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
        VK_BLEND_OP_ADD,
        VK_BLEND_FACTOR_SRC_ALPHA,
        VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
        VK_BLEND_OP_ADD,
        colorWriteMask,
    };
    
    VkPipelineColorBlendAttachmentState
        colorBlendAttachments[] = { colorBlendAttachment };
    
    VkPipelineColorBlendStateCreateInfo colorBlendStateInfo =
    {
        VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
        NULL,
        0,
        VK_FALSE,
        VK_LOGIC_OP_CLEAR,
        array_count(colorBlendAttachments),
        colorBlendAttachments,
        {0, 0, 0, 0}
    };
    
    /*
    * Create Pipeline Layout
    */
    
    VkPipelineLayoutCreateInfo pipelineLayoutInfo =
    {
        VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
        NULL,
        0,
        descSetLayoutCount,
        descSetLayouts,
        0, NULL // (no push constant ranges)
    };
    
    if (vkCreatePipelineLayout(vk->device, &pipelineLayoutInfo, NULL,
                               pipelineLayout) != VK_SUCCESS)
    {
        assert(!"Failed to create pipeline layout!");
    }
    
    VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateInfo =
    {
        VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
        NULL,
        0,
        VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
        VK_FALSE // primitiveRestartEnable
    };
    
    /*
    * Create Graphics Pipeline
    */
    
    VkGraphicsPipelineCreateInfo pipelineInfo =
    {
        VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
        NULL,
        0,
        shaderStageInfoCount,
        shaderStageInfos,
        &vertInputStateInfo,
        &inputAssemblyStateInfo,
        NULL, // pTessellationState
        &viewportStateInfo,
        &rasterizationStateInfo,
        &multisampleStateInfo,
        NULL, // pDepthStencilState
        &colorBlendStateInfo,
        &dynamicStateInfo,
        *pipelineLayout,
        renderPass,
        0, // subpass index
        NULL, 0 // (no base pipeline)
    };
    
    // Create the graphics pipeline
    if (vkCreateGraphicsPipelines(vk->device, VK_NULL_HANDLE, 1,
                                  &pipelineInfo, NULL,
                                  graphicsPipeline) != VK_SUCCESS)
    {
        assert(!"Failed to create graphics pipeline!");
    }
    }
Nothing fancy here—just simple C code abstraction. Or, as Casey Muratori calls it, compression-oriented programming. I moved a bunch of code from the pipeline creation into this function because it’s convenient in this case.

You could choose to structure this function differently, depending on what you want to parameterize. But to keep things simple for the tutorial, I’ve packed a lot into it—since we’ll be creating two identical graphics pipelines anyway.

Update App-specific Vulkan objects

In the section of the code where we define the app-specific Vulkan objects, let's rename pipelineLayout to staticPipelineLayout and graphicsPipeline to staticGraphicsPipeline. Additionally, we'll introduce a pair of corresponding objects for the dynamic pipeline: dynamicPipelineLayout and dynamicGraphicsPipeline.
VkRenderPass renderPass;
VkFramebuffer swapchainFramebuffers[2];
VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;
VkFence frameFence;

VkCommandBuffer graphicsCommandBuffer;

VkPipelineLayout staticPipelineLayout;
VkPipeline staticGraphicsPipeline;

VkPipelineLayout dynamicPipelineLayout;
VkPipeline dynamicGraphicsPipeline;
If you're anything like me, you might think that creating two graphics pipelines feels wasteful—but don’t worry. In Vulkan, having multiple graphics pipelines is actually quite common and not wasteful at all.

The reason is that almost any change we want to make—whether it's swapping shaders or having different vertex buffers (as in our case)—requires a separate pipeline. This is just how Vulkan is designed.

Dynamic Vertex Buffer Vulkan Objects

Right after creating the (static) vertex buffer, let's also add the necessary objects for the dynamic one. You could rename vertexBuffer to staticVertexBuffer, but since we also have a staging buffer for it—and there’s no other vertex buffer to cause confusion—I felt that would just be unnecessary busy work.
/*
* Static Vertex Buffer Vulkan Objects
*/

VkBuffer vertStagingBuffer;
VkDeviceMemory vertStagingBufferMemory;

VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;

/*
* Dynamic Vertex Buffer Vulkan Objects
*/

VkBuffer dynamicVertBuffer;
VkDeviceMemory dynamicVertBufferMemory;

Create Dynamic Vertex Buffer

Now, right above where we define the vertex input layout, let's create the dynamic vertex buffer.
Notice that while for our static vertex buffer we allocated exactly the amount of memory needed for its data, in the case of the dynamic vertex buffer, we don’t know the exact amount of data we'll need. Additionally, the amount of data we draw each frame might (and most likely will) change depending on the state of the game.

To handle this, we pick a maximum number of objects (quads, in this case) we might ever want to draw, and allocate enough memory to hold that amount.
// Enough to draw 4096 quads
u32 dynamicVertBufferMaxSize = 4096 * 6 * sizeof(f32);

vk_create_buffer(&vk, dynamicVertBufferMaxSize,
                 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT |
                 VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                 &dynamicVertBuffer, &dynamicVertBufferMemory);

/*
* Map its memory permanently
*/

void *dynamicVertBufferMemoryMap = NULL;
if (vkMapMemory(vk.device,
                dynamicVertBufferMemory,
                0,
                dynamicVertBufferMaxSize,
                0,
                &dynamicVertBufferMemoryMap) != VK_SUCCESS)
{
    assert(!"Failed to map memory");
}
Notice that we're requesting CPU-accessible memory, mapping it immediately, but not copying anything to it yet—we simply leave it mapped.

Replace Graphics Pipeline Creation Code

Now, we'll replace the entire pipeline creation code with a function call. This change affects everything from "Define Dynamic State Create Info" to "Create the graphics pipeline".

Instead of that block of code, we’ll now have two function calls to our helper function—one for creating the static pipeline and another for the dynamic pipeline:
/*
* Create Graphics Pipelines
*/
    
vk_create_graphics_pipeline(&vk,
                            array_count(descSetLayouts),
                            descSetLayouts,
                            array_count(shaderStageInfo),
                            shaderStageInfo,
                            vertInputStateInfo,
                            renderPass,
                            &staticPipelineLayout,
                            &staticGraphicsPipeline);

vk_create_graphics_pipeline(&vk,
                            array_count(descSetLayouts),
                            descSetLayouts,
                            array_count(shaderStageInfo),
                            shaderStageInfo,
                            vertInputStateInfo,
                            renderPass,
                            &dynamicPipelineLayout,
                            &dynamicGraphicsPipeline);
I’ll wrap it up here for now. In the next post, we’ll finish updating the code to work with our new dynamic vertex buffer. See you then!

Next

Comments

Popular Posts