Vulkan™ 为开发者提供了前所未有的能力,可以对生成图形和计算工作负载进行精细控制,涵盖从小型嵌入式处理器到各种不同架构的高端工作站 GPU。一如既往,强大的能力伴随着重大的责任,确保您的应用程序能在所有可能的目标平台上正确运行至关重要,哪怕只是为了遵循 API 规范的规则,即使某些规则的有意或无意违反似乎不会对特定的硬件和驱动程序实现造成任何问题。
传统的图形 API 通过定义一组非法的 API 使用条件来尝试解决这个问题,这些条件需要驱动程序实现进行捕获,并通过某种错误报告机制报告给应用程序。这种方法的问题在于,尽管这些因不正确的 API 使用而产生的错误在应用程序开发过程中非常有价值,但检查所有这些错误条件会消耗驱动程序中大量不必要的 CPU 时间,当运行已确认正确使用 API 的已发布应用程序时,这些时间将毫无价值。更不用说,实践表明,一些驱动程序实现对 API 规范设定的某些规则并不像其他驱动程序那样严格,因此仅依靠在特定实现上进行测试并观察到没有问题,仍然可能在将同一应用程序运行在其他驱动程序实现上时导致可移植性问题。
错误与运行时的错误
与传统的图形 API 不同,Vulkan 将可能的错误场景分为两个不同的类别:
- 有效性错误 (Validity errors) 是由不正确的 API 使用引起的错误条件,即应用程序未遵守 API 使用规则,而这些规则是获得已发出命令的明确定义的行为所必需的。这些规则在规范中针对所有 API 命令和结构,以“有效用法”标题的文本块进行描述。
- 运行时错误 (Run-time errors) 是即使在正确使用 API 的应用程序执行期间也可能发生的错误条件,例如内存不足,或在某个已被关闭的窗口上进行呈现失败。运行时错误通过结果代码报告。规范以“返回代码”标题的文本块单独描述了每个命令可能返回的可能结果代码,并附有描述每种特定结果代码预期何时由驱动程序实现返回的语言。
尽管许多 Vulkan API 命令确实返回了 VkResult 枚举常量之一作为结果代码,但这些结果代码仅用于指示运行时错误和关于某些操作或对象的状态信息,而不报告有关遵守有效用法条件的信息。这使得应用程序的发布版本能够以最高性能运行,因为驱动程序实现不必花费宝贵的 CPU 周期检查潜在的规范规则违规,因为对于已知正确使用 API 的应用程序来说,这 anyway 是不必要的。
由于驱动程序实现不检查有效用法条件,并期望来自应用程序的所有输入都根据规范有效,因此运行不正确使用 API 的应用程序可能导致意外行为,包括渲染损坏甚至应用程序崩溃。通常,将无效参数传递给 API 命令的后果可能只会在执行后续命令时显现。
救星——验证层
我们已经认识到,对于已知在 Vulkan API 规范方面行为正确的应用程序的发布版本,无需检查有效 API 用法具有巨大的好处,但仍然非常重要,能够在应用程序开发过程中识别不正确的 API 使用,因为在没有关于从何处查找错误的提示的情况下,找到我们所犯的导致奇怪损坏或无法解释的神秘崩溃的错误并非易事。
为了解决这个问题,Vulkan 在 Vulkan SDK 中提供了一组验证和调试层。在撰写本文时,SDK 包括了近十几个专门用于验证 API 使用的某些方面并为开发者提供调试工具(如 API 调用转储器)的层。当启用这些层的任何子集时,它们会自动插入到应用程序发出的每个 Vulkan API 调用的调用链中以执行其工作。本文档范围之外的个别层的详细描述,但有好奇心的读者可以在 这里 找到更多信息。
与传统 API 所采取的方法相比,验证层的优势在于,应用程序只需在明确要求时才花费时间进行广泛的错误检查,通常在开发过程中,并且通常在应用程序的调试版本中使用。这自然符合 Vulkan API 的通用按需付费原则。此外,由于 SDK 附带的官方验证层是集中维护的,并且在各种驱动程序实现中等效工作,因此这种方法不受传统 API 错误检查行为中常见的碎片化问题的困扰,因此开发者可以确信,无论应用程序在哪种驱动程序实现上运行,都将报告相同的验证错误。
更妙的是,验证层不仅查找允许的 API 用法的违规行为,还可以报告潜在的 API 不正确或危险使用的警告,甚至能够报告性能警告,使开发者能够识别 API 使用正确但效率不高的地方。这类潜在性能警告的例子包括绑定未实际使用的资源或使用非最优的图像布局。
愿意在开发过程中验证其 API 用法的应用程序开发者将主要对 VK_LAYER_LUNARG_standard_validation 感兴趣,它将所有标准的验证层打包成一个大型元层。启用此层可确保所有官方验证层都将积极尝试捕获应用程序在使用 Vulkan 时犯下的任何错误。为了将捕获到的有效 API 用法违规报告给应用程序,验证层公开了 VK_EXT_debug_report 实例扩展,该扩展允许将检测到的验证错误和警告馈送到应用程序提供的回调函数。我们将在本文中展示此扩展的基本用法,但更多信息可在 Vulkan Registry 中找到。
为验证准备我们的实例
我们建议所有应用程序在其调试版本中启用和使用验证层,以确保其应用程序始终遵守有效的 API 用法,从而能够跨各种 Vulkan 驱动程序实现进行移植。
下面的代码片段展示了一个典型的 C++ 示例,说明应用程序应如何在调试版本中于实例创建时启用 VK_LAYER_LUNARG_standard_validation 层和 VK_EXT_debug_report 扩展。
std::vector enabledInstanceLayers; std::vector enabledInstanceExtensions;#ifdef MY_DEBUG_BUILD_MACRO /* Enable validation layers in debug builds to detect validation errors */ enabledInstanceLayers.push_back("VK_LAYER_LUNARG_standard_validation");#endif
/* Enable instance extensions used in all build types */ enabledInstanceExtensions.push_back("VK_KHR_surface"); ...#ifdef MY_DEBUG_BUILD_MACRO /* Enable debug report extension in debug builds to be able to consume validation errors */ enabledInstanceExtensions.push_back("VK_EXT_debug_report");#endif
/* Setup instance creation information */ VkInstanceCreateInfo instanceCreateInfo = {}; ... instanceCreateInfo.enabledLayerCount = static_cast(enabledInstanceLayers.size()); instanceCreateInfo.ppEnabledLayerNames = &enabledInstanceLayers[0]; instanceCreateInfo.enabledExtensionCount = static_cast(enabledInstanceExtensions.size()); instanceCreateInfo.ppEnabledExtensionNames = &enabledInstanceExtensions[0];
/* Create the instance */ VkInstance instance = VK_NULL_HANDLE; VkResult result = vkCreateInstance(&instanceCreateInfo, nullptr, &instance);编辑注释:根据您的输入,我已将 NDEBUG 宏的使用替换为表示仅在应用程序调试版本中使用的代码,现在代码示例引用了一个名为 MY_DEBUG_BUILD_MACRO 的自定义宏,您应该将其替换为您项目或编译器工具链中使用的调试构建宏。
当然,一个健壮的应用程序应该首先检查使用的实例层和扩展是否存在,然后再将它们传递给 vkCreateInstance,分别使用 vkEnumerateInstanceLayerProperties 和 vkEnumerateInstanceExtensionProperties 命令。成功创建实例后,验证层将对该实例激活,并且可以开始使用调试报告扩展。
由于 VK_EXT_debug_report 实例扩展不是核心功能,因此其入口点的地址必须通过使用 vkGetInstanceProcAddr 命令获取,如下面的代码片段所示。
#ifdef MY_DEBUG_BUILD_MACRO /* Load VK_EXT_debug_report entry points in debug builds */ PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT = reinterpret_cast (vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT")); PFN_vkDebugReportMessageEXT vkDebugReportMessageEXT = reinterpret_cast (vkGetInstanceProcAddr(instance, "vkDebugReportMessageEXT")); PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT = reinterpret_cast (vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT"));#endif我们的第一个调试报告回调
我们将单独讨论该扩展的每个入口点,但首先让我们看一下应用程序提供的调试报告回调应该是什么样子以及它应该遵循什么行为。应用程序可以注册任意数量的调试报告回调,它们只需要匹配 PFN_vkDebugReportCallbackEXT 定义的签名。下面提供了一个简单的调试报告回调示例,它只是将所有传入的调试消息重定向到 stderr。
VKAPI_ATTR VkBool32 VKAPI_CALL MyDebugReportCallback( VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objectType, uint64_t object, size_t location, int32_t messageCode, const char* pLayerPrefix, const char* pMessage, void* pUserData){ std::cerr << pMessage << std::endl; return VK_FALSE;}传递给回调函数的参数提供了有关触发调用的验证事件的位置和类型的有关信息,例如事件的类型(错误、警告、性能警告等)、触发调用的命令正在创建或操作的对象类型和句柄、描述事件的代码和文本消息,甚至还有一个参数可用于在注册回调时向回调提供应用程序特定的用户数据。通过在回调中设置断点,开发者还可以访问完整的调用堆栈,以更准确地确定违规 API 调用的位置。
回调的返回值是一个布尔值,指示验证层是否应中止触发调试报告回调的 API 调用。但是,开发者必须意识到,如果验证层之一报告了错误,则表明应用程序正在尝试执行无效的操作,因此任何后续操作都可能导致未定义的行为甚至崩溃。因此,建议开发者在第一个错误处停止并尝试解决它,然后再对后续操作的行为做出任何假设。将验证错误视为与编译器报告的错误类似:后续的错误通常是第一个错误的后果。
在注册我们的调试报告回调时,我们可以指定我们想要收到通知的事件类型。通常我们对错误、警告和性能警告感兴趣;下面的代码片段以这种配置注册了我们的回调。
#ifdef MY_DEBUG_BUILD_MACRO /* Setup callback creation information */ VkDebugReportCallbackCreateInfoEXT callbackCreateInfo; callbackCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT; callbackCreateInfo.pNext = nullptr; callbackCreateInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT; callbackCreateInfo.pfnCallback = &MyDebugReportCallback; callbackCreateInfo.pUserData = nullptr;
/* Register the callback */ VkDebugReportCallbackEXT callback; VkResult result = vkCreateDebugReportCallbackEXT(instance, &callbackCreateInfo, nullptr, &callback);#endif已注册的回调可以通过销毁回调对象来注销,就像销毁其他任何 API 对象一样,使用相应的销毁命令 vkDestroyDebugReportCallbackEXT。开发者应确保在销毁实例之前注销其调试报告回调,否则他们将通过任何已注册以接收错误的调试报告回调来收到有关其不当行为的通知。
我们尚未讨论的最后一个调试报告扩展入口点 vkDebugReportMessageEXT 可用于从应用程序代码生成调试报告消息。这对于标记应用程序执行的某些点或将应用程序特定的信息报告到与验证消息相同的流中可能很有用。
更新:自 Vulkan API 规范和 Vulkan SDK 的 1.0.13 版本以来,设备层已被弃用,因此与在设备级别启用验证层相关的说明已相应删除。
外部强制验证
验证应用程序的推荐方法是前面介绍的方法,因为它允许开发者根据构建类型、应用程序设置或其他任何机制来启用验证。此外,调试报告回调还允许对捕获哪些验证事件以及如何捕获进行细粒度控制。
然而,在某些情况下,通过编程方式修改或重新构建应用程序以启用验证可能不可行或不方便。这包括验证未能在调试版本中重现问题的发布版本,或者验证我们无法重新构建的第三方应用程序或库(因为无法访问源代码)。
即使在这些情况下也有解决方案,因为可以通过环境变量 VK_INSTANCE_LAYERS 来启用层。此变量接受要启用的层名称列表,用分号(Windows)或冒号(Linux)分隔。以下命令在 Windows 上启用了所有标准验证层。
> set VK_INSTANCE_LAYERS=VK_LAYER_LUNARG_standard_validation通过这种方法启用验证时,除了设置环境变量来激活层之外,还必须通过配置文件为每个层配置报告机制,否则激活的层将不会产生任何输出。此配置文件必须命名为 vk_layer_settings.txt,并且必须位于应用程序的工作目录中,或位于使用 VK_LAYER_SETTINGS_PATH 环境变量指定的目录中。Vulkan SDK 的 config 文件夹下提供了示例层配置文件,它将简单地将所有错误、警告和性能警告消息输出到 stdout,如果使用,但可以轻松更改为输出不同的验证消息子集,并且可以重定向到文件而不是控制台输出(这对于捕获没有控制台的应用程序的输出可能很有用)。示例配置文件包含有关如何更改各种配置选项的说明。
总结
尽管熟悉 Vulkan API 可能一开始会有点复杂,因为其性质使得其学习曲线比传统 API 更陡峭,但验证层大大简化了捕获任何错误,并且它们还提供了许多额外的有用信息,而不仅仅是报告基本错误。尽管使用验证层并不能完全消除在多个平台上测试应用程序的需要,但它最大限度地减少了因不正确的 API 使用而导致的任何可移植性问题的可能性。
此外,官方加载器和验证层全部开源,可在 Github 上找到。因此,如果您发现任何目前未被任何验证层捕获的错误,请不要犹豫:贡献吧!
不要忘记:在用户为您验证您的应用程序之前,先验证它!