3
votes

While following a Vulkan tutorial using C# I reached the point where I should start using Uniform buffers (you can find the tutorial here) First let me show the related code, and then explain the problem. I have those few structures that are related:

unsafe struct Buffer
{
    public SharpVulkan.Buffer buffer;
    public DeviceMemory memory;
    public readonly ulong size;

    public Buffer(ulong size) : this()
    {
        this.size = size;
    }

    public void Destroy(Device device)
    {
        device.DestroyBuffer(buffer);
        device.FreeMemory(memory);
    }
}

struct MVPMatrices
{
    public static MVPMatrices Identity { get; } = new MVPMatrices { model = Matrix4x4.Identity, view = Matrix4x4.Identity, projection = Matrix4x4.Identity };

    public Matrix4x4 model;
    public Matrix4x4 view;
    public Matrix4x4 projection;
}

struct UniformBuffer
{
    public Buffer buffer;
    public DescriptorSet descriptorSet;

    public void Destroy(Device device)
    {
        buffer.Destroy(device);
    }
}

I think they all are very self-explanatory, but if you need more information I can update the question. Next, in the main program there are a few fields I have created that are related:

static DescriptorSetLayout descriptorSetLayout;
static DescriptorPool descriptorPool;
static UniformBuffer mvpMatricesBuffer;

static readonly MVPMatrices[] mvpMatricesArray = new MVPMatrices[1];
static ref MVPMatrices MVPMatrices => ref mvpMatricesArray[0];

Those are some utility methods I have for buffers:

static Buffer CreateBuffer(ulong size, BufferUsageFlags usage, MemoryPropertyFlags memoryPropertyFlags = MemoryPropertyFlags.HostVisible | MemoryPropertyFlags.HostCoherent)
    {
        Buffer buffer = new Buffer(size);
        BufferCreateInfo createInfo = new BufferCreateInfo
        {
            StructureType = StructureType.BufferCreateInfo,
            SharingMode = SharingMode.Exclusive,
            Size = size,
            Usage = usage,
        };
        buffer.buffer = logicalDevice.CreateBuffer(ref createInfo);

        logicalDevice.GetBufferMemoryRequirements(buffer.buffer, out MemoryRequirements memoryRequirements);
        physicalDevice.GetMemoryProperties(out PhysicalDeviceMemoryProperties memoryProperties);

        uint memoryTypeIndex = 0;
        for (uint i = 0; i < memoryProperties.MemoryTypeCount; i++)
        {
            MemoryType* memoryType = &memoryProperties.MemoryTypes.Value0 + i;
            if ((memoryRequirements.MemoryTypeBits & (1 << (int)i)) != 0 && memoryType->PropertyFlags.HasFlag(memoryPropertyFlags))
            {
                memoryTypeIndex = i;
                break;
            }
        }

        MemoryAllocateInfo allocateInfo = new MemoryAllocateInfo
        {
            StructureType = StructureType.MemoryAllocateInfo,
            AllocationSize = memoryRequirements.Size,
            MemoryTypeIndex = memoryTypeIndex,
        };
        buffer.memory = logicalDevice.AllocateMemory(ref allocateInfo);
        logicalDevice.BindBufferMemory(buffer.buffer, buffer.memory, 0);
        return buffer;
    }

    static void SetBufferData<T>(this Buffer buffer, T[] data)
        where T : struct
    {
        ulong size = (ulong)(Marshal.SizeOf<T>() * data.Length);
        if (size != buffer.size)
            throw new ArgumentException("Size of buffer data must be the same as the size of the buffer!");

        IntPtr memory = logicalDevice.MapMemory(buffer.memory, 0, size, MemoryMapFlags.None);
        System.Buffer.MemoryCopy(Marshal.UnsafeAddrOfPinnedArrayElement(data, 0).ToPointer(), memory.ToPointer(), (uint)size, (uint)size);
        logicalDevice.UnmapMemory(buffer.memory);
    }

    static T[] GetBufferData<T>(this Buffer buffer)
        where T : struct
    {
        T[] result = new T[(int)buffer.size / Marshal.SizeOf<T>()];
        IntPtr memory = logicalDevice.MapMemory(buffer.memory, 0, buffer.size, MemoryMapFlags.None);
        System.Buffer.MemoryCopy(memory.ToPointer(), Marshal.UnsafeAddrOfPinnedArrayElement(result, 0).ToPointer(), (uint)buffer.size, (uint)buffer.size);
        logicalDevice.UnmapMemory(buffer.memory);
        return result;
    }

    static DescriptorSet AllocateDescriptorSet()
    {
        DescriptorSetLayout* setLayout = stackalloc DescriptorSetLayout[1];
        *setLayout = descriptorSetLayout;

        DescriptorSetAllocateInfo allocateInfo = new DescriptorSetAllocateInfo
        {
            StructureType = StructureType.DescriptorSetAllocateInfo,
            DescriptorPool = descriptorPool,
            DescriptorSetCount = 1,
            SetLayouts = (IntPtr)setLayout,
        };
        DescriptorSet set = DescriptorSet.Null;
        logicalDevice.AllocateDescriptorSets(ref allocateInfo, &set);
        return set;
    }

    static UniformBuffer CreateUniformBuffer(ulong size, uint binding)
    {
        UniformBuffer buffer = new UniformBuffer
        {
            buffer = CreateBuffer(size, BufferUsageFlags.UniformBuffer),
            descriptorSet = AllocateDescriptorSet(),
        };

        DescriptorBufferInfo bufferInfo = new DescriptorBufferInfo
        {
            Buffer = buffer.buffer.buffer,
            Offset = 0,
            Range = buffer.buffer.size,
        };

        WriteDescriptorSet descriptorWrite = new WriteDescriptorSet
        {
            StructureType = StructureType.WriteDescriptorSet,
            BufferInfo = new IntPtr(&bufferInfo),
            DescriptorCount = 1,
            DescriptorType = DescriptorType.UniformBuffer,
            DestinationArrayElement = 0,
            DestinationBinding = binding,
            DestinationSet = buffer.descriptorSet,
            ImageInfo = IntPtr.Zero,
            TexelBufferView = IntPtr.Zero,
        };
        logicalDevice.UpdateDescriptorSets(1, &descriptorWrite, 0, null);
        return buffer;
    }

At the start I also initialized it with a value, but instead I made the Get/SetBufferData methods, so I can update the data every frame. Here is the function that's called in the main loop right before rendering:

static void UpdateApplication()
{
    MVPMatrices.model = Matrix4x4.Identity;
    MVPMatrices.view = Matrix4x4.Identity;
    MVPMatrices.projection = Matrix4x4.Identity;

    mvpMatricesBuffer.buffer.SetBufferData(mvpMatricesArray);
    Console.WriteLine(mvpMatricesBuffer.buffer.GetBufferData<MVPMatrices>()[0].model);
    Console.WriteLine(mvpMatricesBuffer.buffer.GetBufferData<MVPMatrices>()[0].view);
    Console.WriteLine(mvpMatricesBuffer.buffer.GetBufferData<MVPMatrices>()[0].projection);
}

There is also the function for that allocates the command buffers. There is really no need to show the whole thing, but basically: I allocate the buffer, bind some buffers and draw. Right before drawing I bind the descriptor set to the command buffer, so this is the code I use for that (all you need to know is that "buffer" a pointer to the command buffer):

DescriptorSet* descriptorSets = stackalloc DescriptorSet[1];
*descriptorSets = mvpMatricesBuffer.descriptorSet;
buffer->BindDescriptorSets(PipelineBindPoint.Graphics, pipelineLayout, 0, 1, descriptorSets, 0, null);

Now there are the two shaders. The only relevant shader is the vertex shader, so here it is:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 0) uniform MVPMatrices
{
    mat4 model;
    mat4 view;
    mat4 projection;
} mvpMatrices;

layout(location = 0) in vec3 position;
layout(location = 1) in vec4 color;

layout(location = 0) out vec4 vertexColor;

void main()
{
    gl_Position = mvpMatrices.projection * mvpMatrices.view * mvpMatrices.model * vec4(position, 1);
    vertexColor = color;
}

So what is the problem?
As always with graphics: It doesn't render. Whenever I delete the part where I multiply by the matrices it works perfectly fine. I also tried to set the color to the second row of the model matrix (which should be green), and it didn't work.

The logs I showed in the update method print the data of the matrices buffer, and they all are the identity matrices (just like they should be).
That means the data of the buffer is fine.

The descriptor sets are a different story, though. When I don't bind the set layout to the pipeline layout, I get an error saying the shader uses a descriptor slot but there it's not declared in the pipeline layout.
That means the set layout is being bound correctly.

If I don't create the descriptor set then I get an error when I bind it to the command buffer.
That means the descriptor set is being created correctly, and thus the descriptor pool is also fine (I would get an error otherwise).

So the buffers and descriptor sets both work.
That leaves me with one conclusion: The buffer and descriptor set are not linked correctly. The weird thing is that the code linking the two is the Device.UpdateDescriptorSets call. The problem is that if I don't pass the buffer info the the DescriptorWrite variable, or if I pass a null handle to the buffer info, I get an error saying the descriptor set has never been updated.
Same thing if I don't pass the descriptor set to the DescriptorWrite variable.
That means it's aware of the fact that I update it, and it's aware of the fact that I am sending a buffer, but the data is still somehow not being sent to the shader.

After trying to debug it for over a week already, I can say that everything looks perfect to me, and I have no idea what the problem is. This made me pretty hopeless, so I came here.

This is my first post on the website, and I have no idea if I gave you all of the information you need (even though I feel like I gave too many), so if there is anything wrong with the post, please tell me and I will try to fix it.

2
Could You share Your code in an easily debugable form?Ekzuzy
Yes, here is a BitBucket link: bitbucket.org/oraviram/c-sandbox/src/… I didn't post it previously because it's way too long for here.Or Aviram
I will look at it and try to find the source of the problem.Ekzuzy
It seems that on my computer Your program works as expected. When I modify the projection matrix, the size of the quad also changes (though I had to disable debug reports, because it caused an exception to be thrown).Ekzuzy
Huh, this is weird... I also tried settings the color to the second row of the model matrix and it was simply 0. I might switch to C++ for my engine (I am just trying it and seeing if there are any features it doesn't have I will not be able to live without), hopefully then it will work. I should probably write an answer to the question for tracking purposes, but I have no idea what I should write there.Or Aviram

2 Answers

0
votes

Apparently all of the code related to the uniforms is ok. For some reason the problem was with the debug report. I really have no idea what it has to do with the uniforms, but when I disable it everything is perfect.

What I decided to do is just disable the debug report for the most part, but be able to bring it back (when I need some feedback from Vulkan about stuff) by simply defining something using #define. It might not be a good solution, but it's the best I could come up with.

0
votes

I faced something similar and am still trying to chase down the reason. If I enable the "VK_LAYER_LUNARG_core_validation" validation layer in my Android app, my uniform buffers are zero'd in the shaders.

Don't have a workaround yet, but I wonder if this is a bug in the validation layer or in my descriptor sets.

I am able to enable the other validation layers without this issue.