Vulkan 新手开发者常见陷阱

这是 Vulkan API 中关于假设、陷阱和反模式的简短列表。它不是“最佳实践”列表,而是涵盖了 Vulkan 新手开发者容易犯的常见错误。

验证层

在开发过程中,请确保已启用验证层。它们是使用 Vulkan API 时捕获错误的重要工具。参数检查、对象生命周期和线程违规都包含在提供的错误检查中。确保它们已启用的一种方法是验证输出流中是否存在文本“Debug Messenger Added”。有关更多信息,请参阅 Vulkan SDK 层文档。

Vulkan 是一个工具箱

在 Vulkan 中,大多数问题都可以用多种方法解决,每种方法都有自己的优点和缺点。很少有“完美”的解决方案,执着于寻找一个解决方案通常是徒劳的。当遇到问题时,尝试创建一个满足当前需求并且不过于复杂的适当解决方案。虽然 Vulkan 的规范可能很有用,但它不是在实践中如何使用 Vulkan 的最佳来源。相反,请参考外部资源,例如本指南、硬件最佳实践指南、教程和其他文章,以获取更深入的信息。最后,对各种解决方案进行性能分析是发现使用哪种解决方案的重要部分。

记录命令缓冲区

许多早期的 Vulkan 教程和文档建议编写一次命令缓冲区并在尽可能的情况下重复使用它。然而,在实践中,重复使用很少能获得宣传的性能优势,同时由于实施的复杂性而导致不小的开发负担。虽然重复使用计算数据是一种常见的优化方法,但它可能显得违反直觉,但由于添加和删除对象以及视锥体剔除等技术会根据每帧的需要改变发出的绘制调用,因此管理场景使得重复使用命令缓冲区成为一个严峻的设计挑战。它需要一个缓存方案来管理命令缓冲区并维护状态,以确定是否以及何时需要重新录制。相反,最好每帧都重新录制新的命令缓冲区。如果性能是一个问题,也可以进行多线程录制,以及使用辅助命令缓冲区进行非可变绘制调用,例如后处理。

多个管线

图形 VkPipeline 包含执行绘制调用所需的状态组合。使用不同的着色器、混合模式、顶点布局等渲染场景,每种可能性都需要一个管线。因为管线的创建和在绘制调用之间交换它们都有相关的成本,所以最好仅在需要时创建和交换管线。但是,通过使用各种技术和功能来进一步减少创建和交换,超出简单情况可能会适得其反,因为它增加了复杂性,并且不能保证带来好处。对于大型引擎,这可能是必要的,但否则不太可能成为瓶颈。使用管线缓存可以进一步降低成本,而无需诉诸更复杂的方案。

每个交换链图像的资源重复

流水线处理帧是提高性能的常用方法。通过同时渲染多个帧,每个帧都使用自己所需的资源副本,它可以减少资源争用,从而降低延迟。此方法的简单实现将为交换链中的每个图像复制所需的资源。问题在于,这会导致假设渲染资源必须为每个交换链图像复制一次。虽然对于某些资源来说是可行的,例如每帧使用的命令缓冲区和信号量,但与交换链图像的一对一重复通常不是必需的。Vulkan 提供了很大的灵活性,允许开发者选择适合他们情况的重复级别。许多资源可能只需要两个副本,例如,统一缓冲区或每帧更新一次的数据,而其他资源可能根本不需要任何重复。

每个队列族的多个队列

多个硬件平台在每个队列族中都有多个 VkQueue。通过能够从单独的队列将工作提交到同一队列族,这可能很有用。虽然可能存在优势,但创建或使用额外的队列不一定更好。有关具体的性能建议,请参阅硬件供应商的最佳实践指南。

描述符集

描述符集旨在方便地根据用途和更新频率对着色器中使用的数据进行分组。Vulkan 规范要求硬件至少同时支持使用 4 个描述符集,而大多数硬件至少支持 8 个。因此,在合理的情况下,几乎没有理由不使用多个描述符集。

正确的 API 使用实践

虽然验证层可以捕获许多类型的错误,但它们并非完美无缺。以下是在遇到奇怪行为时,一些良好的习惯和可能出现的错误来源的简短列表。

  • 初始化所有变量和结构体。

  • 为每个结构体使用正确的 sType

  • 验证正确的 pNext 链的使用,不需要时将其置为空。

  • Vulkan 中没有默认值。

  • 使用正确的枚举 (enum)、VkFlag 和位掩码值。

  • 考虑使用类型安全的 Vulkan 包装器,例如 C++ 的 Vulkan.hpp

  • 检查函数返回值,例如 VkResult

  • 在适当的地方调用清理函数。