Vulkan - 1 个统一缓冲区,N 个网格 - 动态 VkDeviceMemory

Vulkan - 1 uniform buffer, N meshes - dynamic VkDeviceMemory

假设我在随机位置渲染简单的立方体。

将其中的3个作为立方体的起始数量,应用程序获取一个VkBuffer句柄并将其绑定到一个VkDeviceMemory以便在其中连续存储所有立方体的模型矩阵,稍后由着色器通过描述符集访问。 VkDeviceMemory 的内存刚好够这 3 个立方体。

我想要做的是,每次用户按下一个键,一个新的立方体应该在某处弹出。我的问题是,我应该如何调整内存大小?您能否概述一下我应该完成的步骤?

我知道我可以为每个多维数据集使用单独的 VkBuffer/VkDeviceMemory,但我不想那样做。我读到的所有地方都说这是一种反模式。

我是否应该丢弃 VkDeviceMemory,分配一个大小合适的新的,然后收工?描述符集呢,它们需要任何特殊处理吗?

在我读到的某些地方,您可以分配非常大的数据块,因此在处理越来越多的多维数据集时,您处于安全的一边,我想,您会停止允许更多的多维数据集其中的一个弹出是因为达到了限制。有没有办法绕过这个自我施加的限制?

编辑:我也意识到一次分配一小块是个坏主意。我感兴趣的是重新分配本身,以及它需要做什么。

The VkDeviceMemory has just enough memory for those 3 cubes.

为什么?如果你想支持任意数量的多维数据集,那么你应该管理你的内存,这样你就可以处理数量的变化,比如用最少数量的重新分配进行转换。

Should I just discard the VkDeviceMemory, allocate a new one with the right size, and call it a day?

对于数量可变的结构,您应该根据当前需求和未来可能的需求进行分配。同时,您不想过度分配。对于像变换这样的东西,相对于现代 GPU 通常可用的内存量来说非常小,开始时分配 1024 个独特的变换并不是不合理的。如果它是一个简单的 mat4 转换,它只消耗 512 个字节,所以其中 1k 只需要半兆字节的内存。与纹理甚至复杂网格的典型内存负载相比,这是微不足道的。

如果你真的把它们都吃光了,你可以重新分配更多。根据您可能的使用模式,您可以使用固定块大小(如 1024)重新分配,或者以指数方式增加分配,例如始终分配当前端的两倍。您可以 google vector reallocation 获取有关处理可能超出其当前范围的连续内存的策略的更多信息。 Here's 一篇关于该主题的文章

同意 Jherico 所说的,但还有一个额外的选择,那就是不要将自己局限于一个 VkBuffer

通常你想考虑 VkDeviceMemory 内存页的倍数 (4 KiB),有些设备甚至喜欢 64 KiB 的倍数。即使你分配的东西比那个小,你也很可能会用完那么多内存,因为 OS 内核不能给你更小的块。

因此,如果每个转换需要 64 B,那么您可能只打算分配 1k 个转换块。分配一个 64 KiB VkBuffer / VkDeviceMemory 对,当它填满时分配第二对,当它填满时分配第三对,依此类推

当您开始绘制时,您需要为每个块单独进行绘制调用,并在其间重新绑定缓冲区。如果您发现在实践中您最终会绘制大量的立方体并且绘制调用和状态更改的数量限制了性能,请使用更大的块大小——无论如何您都会使用内存,因此以较小的增量分配它没有任何帮助。

如果这样做,那么每次分配一个新块时,都需要为其设置一个新的描述符。同时创建它,然后在绘制之间仅绑定到为您将要使用的缓冲区设置的描述符。

如果您重新分配缓冲区,那么您要么需要等待之前的渲染完成并在使用重新分配的缓冲区进行绘制之前更新您拥有的描述符集,要么您可以创建一个新的描述符集并立即绘制,然后稍后当您知道使用它的绘图已完成时回收旧的描述符集。

回答问题"how do I reallocate and start using the new memory",忽略有关分配策略的问题:重新分配与分配一个新事物没有什么不同,用你想要的数据填充它,然后开始使用它。因此,您基本上需要执行与初始分配相同的所有步骤。

需要注意的是,命令缓冲区中引用的大多数对象在命令缓冲区完成执行之前无法安全地修改。通常,您将记录帧 N+1 的命令,同时帧 N 的命令仍在执行。所以你想避免更新可变对象(如描述符集)以开始使用新分配;相反,您需要一个新的描述符集。

这是您需要的物品清单:

  1. 缓冲区本身:一个VkBuffer和一个VkDeviceMemory。如果您在当前 VkDeviceMemory 中分配了额外的 space,那么它对于旧的和新的 VkBuffer 都足够大,那么您不需要新的 VkDeviceMemory 对象。无论哪种方式,创建一个所需大小的新 VkBuffer,并将其绑定到 VkDeviceMemory 对象的未使用部分。

  2. 一种将缓冲区绑定到管道的方法:一种VkDescriptorSet。您将使用与以前相同的描述符集布局,这不会改变。因此,从您的描述符池中分配一个新的描述符集,并使用 vkUpdateDescriptorSet 将缓冲区描述符设置为指向您的新缓冲区(如果不需要更改,您也可以从以前的描述符集中复制其他描述符) .

  3. 最后,在为要使用新缓冲区的帧构建命令缓冲区时,将新的描述符集传递给 vkCmdBindDescriptorSets 而不是旧的。

  4. 最终,在使用旧缓冲区和描述符集的所有命令缓冲区完成后,您可以释放缓冲区和描述符集。对于描述符集,您可能只是 return 将它放入池中,或者保留它并在下次需要重新分配缓冲区时重新使用它。然后可以释放旧缓冲区使用的设备内存,或者您可以保留它以供以后重用。