Vulkan常见的陷阱
以下是 Vulkan开发过程中经常会遇到的"陷阱",涵盖了 Vulkan 新手很容易犯的常见错误。
验证层
在开发过程中,请确保开启 Validation Layers,它是在使用 Vulkan API 时获取错误信息的宝贵工具。参数检查、对象生命周期和线程冲突都是错误检查的一部分。确保验证层已启用的一种方法是检查输出流是否有文本 “Debug Messenger Added” 。更多信息,请参阅 Vulkan SDK 文档。
Vulkan 是一个工具
在 Vulkan 中,大多数问题的解决方法不止一种,每种方法都有自己的优缺点。很少有“完美”的解决方案,追求寻找一个完美的解决方案往往是徒劳的。遇到问题时,请尝试创建一个满足当前需求且不会过于复杂的解决方案。虽然 Vulkan 规范可能很有用,但它并没有全面描述使用 Vulkan 的最佳实践。请参考一些外部的编程指南、硬件最佳实践指南、教程和文章来获 得更多信息。最后,分析各种解决方案后再确定最终使用哪个解决方案。
录制命令缓冲区
许多早期的 Vulkan 教程和文档建议录制一次命令缓冲区,并尽可能重复使用它。然而,在实际使用中,重用命令缓冲区并没有宣传那样的性能优势,却由于实现的复杂性,增加了开发负担。虽然这似乎有悖常理,因为重用计算数据是一种常见的优化,但管理添加和删除对象的场景以及视锥剔除等技术(会改变每帧的调用的绘制指令)使重用命令缓冲区成为一项严峻的设计挑战。它需要一个缓存方案来管理命令缓冲区并维护它的状态,来确定是否需要以及何时需要重新录制。相反,每帧重新录制新的命令缓冲区可能是一个更好的选择。如果性能存在问题,可以进行多线程录制,并使用辅助命令缓冲区进行可变的绘制调用,例如后处理。
多个管线
图形VkPipeline
包含执行绘制调用所需的状态组合,一个渲染场景会使用不同的着色器、混合模式、顶点布局等,每种可能都需要一个管线。由于创建管线和在绘制调用间切换管线会有性能成本,因此最好仅在需要时创建和切换管道。通过使用各种技术和功能来进一步减少创建和切换,可能会适得其反,因为它增加了复杂性,却不能保证性能收益。大型引擎可能需要,一般场景可能不会成为瓶颈。使用管道缓存可以进一步降低管线创建成本,而无需采用更复杂的方 案。
复用每个 Swapchain 的图像资源
流水线的帧处理是提高性能的常用方法,通过同时渲染多帧,每帧使用自己的所需资源副本,通过消除资源争用来减少延迟。该方案实现需要复制交换链中每个图像所需的资源。问题在于,这会导致必须为每个交换链图像复制一次渲染资源。虽然对于某些资源(如命令缓冲区和信号量)来说可以这么做,但通常不需要与交换链图像进行一对一的复制。Vulkan 有很大的灵活性,让开发者选择适合他们的复制级别。许多资源可能只需要两个副本,例如,统一缓冲区或每帧更新一次的数据,而其他资源可能根本不需要任何复制。
单个队列族多个队列
一些硬件的每个队列族包含多个VkQueue
,将任务提交到同一队列族的多个队列的功能可能会有用途。虽然可能有优势,但创建或使用额外的队列并不一定更好。具体的性能建议,请参阅硬件供应商的最佳实践指南。
描述符集
Descriptor Sets (描述符集) 旨在按使用情况和更新频率对着色器中使用的数据进行分组。Vulkan 规范要求硬件支持一次使用的描述符集最少4 个,大多数 硬件最少支持 8 个。因此在合理的情况下,几乎没有理由不使用多个。
正确的 API 用法
虽然验证层可以捕获各种错误,但它并不是完美的。以下是一些好的编程习惯和可能的错误来源:
- 初始化所有变量和结构。
- 对每个结构使用正确的
sType
。 - 验证正确的
pNext
链使用,在不需要时将其清零。 - Vulkan 中没有默认值。
- 使用正确的
enum
、VkFlag
和bitmask
值。 - 考虑使用类型安全的 Vulkan 包装器,例如适用于 C++ 的 Vulkan.hpp。
- 检查函数返回值,例如
VkResult
- 在适当的情况下调用 cleanup 函数。