Search code examples
c++bufferoffsetvulkan

How do I load multiple structs into a single UBO?


I am following the tutorials on: Here.

I have completed up till loading models so my code is similar to that point.

I am now trying to pass another struct to the Uniform Buffer Object, in a similar way to previously shown.

I have created another struct defined outside the application to store the data as follows:

struct Light{
    alignas(16) glm::vec3 position;
    alignas(16) glm::vec3 colour;
};

After doing this, I resized the uniform buffer size in the following way:

    void createUniformBuffers() {
        VkDeviceSize bufferSize = sizeof(CameraUBO) + sizeof(Light);
    ...

Next, when creating the descriptor sets, I added the lightBufferInfo below the already defined bufferInfo as shown below:

        ...
        for (size_t i = 0; i < swapChainImages.size(); i++) {
            VkDescriptorBufferInfo bufferInfo = {};
            bufferInfo.buffer = uniformBuffers[i];
            bufferInfo.offset = 0;
            bufferInfo.range = sizeof(UniformBufferObject);

            VkDescriptorBufferInfo lightBufferInfo = {};
            lightBufferInfo.buffer = uniformBuffers[i];
            lightBufferInfo.offset = 0;
            lightBufferInfo.range = sizeof(Light);

        ...

I then added this to the descriptorWrites array:

        ...
        descriptorWrites[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
        descriptorWrites[2].dstSet = descriptorSets[i];
        descriptorWrites[2].dstBinding = 2;
        descriptorWrites[2].dstArrayElement = 0;
        descriptorWrites[2].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
        descriptorWrites[2].descriptorCount = 1;
        descriptorWrites[2].pBufferInfo = &lightBufferInfo;
        ...

Now similarly to the UniformBufferObject I plan to use the updateUniformBuffer(uint32_t currentImage) function to change the lights position and colour, but first I just tried to set the position to a desired value:

    void updateUniformBuffer(uint32_t currentImage) {
        ...
        ubo.proj[1][1] *= -1;

        Light light = {};
        light.position = glm::vec3(0, 10, 10);
        light.colour = glm::vec3(1, 1, 0);

        void* data;
        vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0, &data);
        memcpy(data, &ubo, sizeof(ubo));
        vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
    }

I do not understand how the offset works when trying to pass two objects to a uniform buffer, so I do not know how to copy the light object to uniformBuffersMemory.

How would the offsets be defined in order to get this to work?


Solution

  • A note before reading further: Splitting data for a single UBO into two different structs and descriptors makes passing data a bit more complicated, as all your sizes and writes need to be aligned to the minUniformBufferAlignment property of your device, making your code a bit more complicated. If you're starting with Vulkan you may want to split the data either into two UBOs (creating two buffers), or just pass all values as a single struct.

    But if you want to continue with the way you described in your post:

    First you need to properly size your array, because your copies need to be aligned to minUniformBufferAlignment you probably can't just copy your light data to the area right after your other data. If your device has an minUniformBufferAlignment of 256 bytes and you want to copy over two host structs you'r uniform buffers size needs to be at least 2 * 256 bytes and not just sizeof(matrices) + sizeof(lights). So you need to adjust your bufferSize in the VkDeviceSize structure accordingly.

    Next you need to offset your lightBufferInfo VkDescriptorBufferInfo:

    lightBufferInfo.offset = std::max(sizeof(Light), minUniformBufferOffsetAlignment);
    

    This will let your vertex shader know where to start fetching data for that binding.

    On most NVidia GPUs e.g., minUniformBufferOffsetAlignment is 256 bytes, where as the size of your Light struct is 32 bytes. So to make this work on such a GPU you have to align at 256 bytes instead of 32.

    Inspecting your setup in RenderDoc should then look similar to this:

    enter image description here

    Note that for more complex allocations and scenarios you'd need to properly get the right alignment size depending on the size of your data structure instead of using a simple max like above.

    And now when updating your uniform buffers you need to map and copy to the proper offset too:

    void* mapped = nullptr;
    
    // Copy matrix data to offset for binding 0
    vkMapMemory(device, uniformBuffersMemory[currentImage].memory, 0, sizeof(ubo), 0, &mapped);
    memcpy(mapped, &ubo, sizeof(ubo));
    vkUnmapMemory(device, uniformBuffersMemory[currentImage].memory);
    
    // Copy light data to offset for binding 1 
    vkMapMemory(device, uniformBuffersMemory[currentImage].memory, std::max(sizeof(ubo), minUniformBufferOffsetAlignment), sizeof(Light), 0, &mapped);
    
    memcpy(mapped, &uboLight, sizeof(Light));
    vkUnmapMemory(device, uniformBuffersMemory[currentImage].memory);
    

    Note that you may want to only map once after creating the buffers for performance reasons rather than mapping on every update. Just store the offset pointer somewhere in your code.