Vulkan - 统一缓冲区未发送到着色器

3

在使用C#进行Vulkan教程时,我到了应该开始使用Uniform缓冲区的地方(你可以在这里找到该教程)。 首先让我展示相关代码,然后解释问题。我有几个相关结构体:

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);
    }
}

我认为它们都很容易理解,但如果你需要更多信息,我可以更新问题。 接下来,在主程序中,我创建了几个相关的字段:

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

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

以下是我为缓冲区编写的一些实用方法:

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;
    }

一开始我也用一个值初始化了它,但是我做了Get/SetBufferData方法,这样我就可以每帧更新数据。这里是在渲染之前在主循环中调用的函数:

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);
}

还有一个函数是用于分配命令缓冲区的。不需要展示整个过程,但基本上:我分配缓冲区,绑定一些缓冲区并进行绘制。在绘制之前,我将描述符集绑定到命令缓冲区,所以这是我使用的代码(你只需要知道"buffer"是指向命令缓冲区的指针):

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

现在有两个着色器。唯一相关的着色器是顶点着色器,因此在这里:

#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;
}

那么问题是什么?与图形一样:它没有渲染。每当我删除通过矩阵相乘的部分时,它就可以正常工作。我还尝试将颜色设置为模型矩阵的第二行(应该是绿色),但它并没有起作用。
在更新方法中显示的日志打印出矩阵缓冲区的数据,它们都是单位矩阵(就像它们应该的那样)。这意味着缓冲区的数据是正确的。
然而,描述符集是另一回事。如果我不将集布局绑定到管线布局,则会出现错误,指出着色器使用了描述符槽,但在管线布局中没有声明。这意味着集布局被正确地绑定。
如果我不创建描述符集,那么当我将其绑定到命令缓冲区时,我会收到一个错误。这意味着描述符集被正确创建,因此描述符池也正常(否则我会收到错误)。
因此,缓冲区和描述符集都有效。这让我得出一个结论:缓冲区和描述符集没有正确链接。奇怪的是,连接两者的代码是Device.UpdateDescriptorSets调用。问题在于,如果我不将缓冲区信息传递给DescriptorWrite变量,或者如果我向缓冲区信息传递空句柄,我会收到一个错误,指出描述符集从未被更新过。如果我不将描述符集传递给DescriptorWrite变量,那么情况也是一样的。这意味着它知道我正在更新它,知道我正在发送缓冲区,但数据仍然无法发送到着色器。
尝试调试了一个星期后,我可以说,对我来说一切看起来都很完美,我不知道问题在哪里。这让我感到绝望,所以我来到这里。
这是我在网站上的第一篇帖子,我不知道是否提供了您所需的所有信息(即使我感觉提供太多了),因此,如果帖子有任何问题,请告诉我,我会尝试解决。

你能分享一下易于调试的代码吗? - Ekzuzy
是的,这里有一个 BitBucket 链接:https://bitbucket.org/oraviram/c-sandbox/src/627e199c9ddde6e11b696fbbf10b80824baf98b0/Source/Program.cs?at=master&fileviewer=file-view-default 我之前没有发布它,因为它对于这里来说太长了。 - Or Aviram
在我的电脑上,您的程序似乎按预期工作。当我修改投影矩阵时,四边形的大小也会改变(尽管我不得不禁用调试报告,因为它会引发异常)。 - Ekzuzy
哎呀,这很奇怪...我还尝试将颜色设置为模型矩阵的第二行,但它仅为0。我可能会为我的引擎切换到C++(我只是尝试并查看是否有任何重要的功能),希望那时它能够正常工作。出于追踪目的,我应该写一个答案,但我不知道我应该在那里写些什么。 - Or Aviram
我下载了你的代码并运行它:它可以正常工作。所以我尝试运行我的代码,但是它却不能正常工作。然后我将你的Program.cs文件复制粘贴到我的文件中,它就可以正常工作了,因此问题肯定出在我的代码上。我会尝试比较这两个代码,并且当我找到问题时,会将其发布为这个问题的答案。感谢你的帮助。 - Or Aviram
显示剩余5条评论
2个回答

0

显然,与制服相关的所有代码都没问题。由于某种原因,问题出在调试报告上。我真的不知道它与制服有什么关系,但当我禁用它时,一切都完美了。

我决定的做法是大部分时间禁用调试报告,但可以通过简单地使用 #define 定义某些内容来重新启用它(当我需要从Vulkan获取反馈时)。这可能不是一个好的解决方案,但这是我能想到的最好的方法。


为什么不通过环境变量全局启用/禁用验证层呢?这很容易,而且不需要修改源代码。它们可以报告许多(如果不是大多数)问题。并且它们不应该干扰您的代码。尽管我没有做太多工作,但我很高兴能帮上忙;-)。 - Ekzuzy
哇,我竟然不知道这些东西的存在。虽然在我的简单沙盒项目中并不需要使用它们,但当我真正开始为我的引擎使用Vulkan时,我会看看我能做什么的。 - Or Aviram

0

我遇到了类似的问题,仍在尝试查找原因。如果在我的Android应用程序中启用“VK_LAYER_LUNARG_core_validation”验证层,则着色器中的统一缓冲区将为零。

目前还没有解决方法,但我在想这是否是验证层或者我的描述符集中的错误。

我能够启用其他验证层而不出现此问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接