在 Vulkan 中更新顶点缓冲区的最普遍正确方法

Most generally correct way of updating a vertex buffer in Vulkan

假设设备内存中有一个顶点缓冲区和一个主机连贯且可见的暂存缓冲区。还假设桌面系统具有独立的 GPU(因此是独立的内存)。最后,假设正确的帧间同步。

我看到两种更新顶点缓冲区的通用方法:

  1. Map + memcpy + 取消映射到暂存缓冲区,然后是包含 vkCmdCopyBuffer 的瞬态(单个命令)命令缓冲区,将其提交到图形队列并等待队列空闲,然后释放临时命令缓冲区。之后像往常一样将常规帧绘制队列提交到图形队列。这是 https://vulkan-tutorial.com (for example, this .cpp file) 上使用的代码。

  2. 与上面类似,只是在暂存缓冲区复制提交后使用额外的信号量发出信号,并在常规帧绘制提交中等待,从而跳过"wait-for-idle"命令。

#2 对我来说有点意义,我反复阅读不要在 Vulkan 中执行任何 "wait-for-idle" 操作,因为它会使 CPU 与 GPU 同步,但我从来没有看到它在任何教程或在线示例中使用。如果必须相对频繁地更新顶点缓冲区,专业人士通常会做什么?

首先,如果您分配了一致性内存,那么您几乎可以肯定是为了从 CPU 访问它。这需要映射它。 Vulkan 不是 OpenGL;不需要在使用内存之前取消映射内存 (and OpenGL doesn't even have that requirement anymore)。

取消映射内存只应永远在您要删除内存分配本身时完成。

其次,如果您想到的想法涉及让 CPU 在继续之前等待队列或设备空闲,那么您的想法很糟糕,应该使用不同的想法。您唯一应该等待设备空闲的时间是您想要销毁设备的时候。

不应相信教程代码可以提供最佳实践。它通常旨在简单,以便于理解一个概念。简单的 Vulkan 代码通常会影响性能(如果您不关心性能,则不应使用 Vulkan)。

无论如何,在 Vulkan 中做大多数事情都没有“最普遍正确的方法”。有很多绝对 不正确 的方法,但没有“通常这样做”的建议。 Vulkan 是一个低级的、显式的 API,其结果是您需要将 Vulkan 的工具应用到您的具体情况。也许在不同的硬件上配置文件。

例如,如果您每帧都生成全新的顶点数据,最好查看实现是否可以直接从相干内存中读取顶点数据,这样就根本不需要暂存缓冲区。是的,读取速度可能较慢,但整个过程可能比传输后读取更快。

话又说回来,它可能不会。它在某些硬件上可能更快,而在其他硬件上可能更慢。并且某些硬件可能根本不允许您对任何具有顶点输入用途的缓冲区使用相干内存。即使允许,您也可以在传输过程中做其他工作,因此 GPU 在读取传输数据之前等待的时间最少。一些硬件有一个小的设备本地内存池,你可以直接从 CPU;此内存适用于此类流媒体应用程序。

但是,如果您打算进行暂存,那么您的选择主要是关于您在哪个队列上提交传输操作(假设硬件有多个队列)。这主要与您愿意忍受的延迟时间有关。

例如,如果您正在为大型地形系统流式传输数据,那么顶点数据需要一两帧才能在 GPU 上使用可能没问题。在这种情况下,您应该寻找一个替代的、仅传输队列,在该队列上执行从暂存缓冲区到主内存的复制。如果这样做,则需要确保使用最终结果的后续命令与该队列同步,这需要通过信号量来完成。

如果您处于低延迟情况下,传输的数据需要在本帧中使用,那么将两者提交到同一个队列可能会更好。您可以使用事件而不是信号量来同步它们。但是你也应该努力在传输和渲染操作之间放置一些不相关的工作,这样你就可以在操作中利用一定程度的并行性。