第 4 章. Vulkan 中的调试
在上一章中,我们初始化了 Vulkan API 并知道了层和扩展的概念。 我们连接物理硬件设备并理解了它所暴露的不同类型的队列。 由于我们正在为实际具体的实现做前期的准备工作,因此了解 Vulkan 中的调试功能,从而避免不愉快的错误,就显得非常重要了。
Vulkan 允许您通过验证层执行调试。 这些验证层检查是可选的,可以在运行时注入到系统中。 传统的图形 API 会预先使用某种错误检查机制来执行验证,这是管线的所必需部分。 这在开发阶段确实很有用,但实际上,在发布阶段这却是一种开销,因为验证错误在开发阶段本身可能已经获得了修复。 这种强制性检查会导致 CPU 花费大量的时间进行错误检查。
另一方面,Vulkan 旨在提供最佳性能,其中可选的验证过程和调试模型起着至关重要的作用。 Vulkan 假定应用程序已经完成了它的功课 ------ 使用开发阶段提供的验证和调试功能,并且在发布阶段它是完全可以信赖的。
在本章中,我们将学习 Vulkan 应用程序的验证和调试过程。 我们将涵盖以下主题
- 窥视 Vulkan 调试
- 了解 LunarG 验证层及其功能
- 在 Vulkan 中实现调试功能
窥视 VUlkan 调试
Vulkan 调试功能用于验证应用程序的实现。 它不仅表示错误,还表示一些其他的验证,例如正确的 API 使用。 它通过验证传递给它的每个参数来执行这项操作,警告使用中可能存在不正确和危险的 API 实践,并在 API 未得到最佳使用时报告任何与性能相关的警告。 默认情况下,是禁用调试功能的,并且启用调试功能是应用程序的责任。 调试仅适用于实例级、在实例(VkInstance)创建时明确启用的那些层。
启用调试功能时,会将其自身插入到层感兴趣的 Vulkan 命令的调用链中。对于每个命令,调试功能 debugging 会访问所有启用的层并验证它们是否存在任何潜在的错误、警告以及调试信息等。
在 Vulkan 中进行调试很简单。 以下概述描述了在应用程序中启用调试所需的步骤:
-
通过在实例级添加 VK_EXT_DEBUG_REPORT_EXTENSION_NAME 扩展来启用调试功能。
-
定义用于调试的一组验证层。 例如,我们对实例和设备级的以下层感兴趣。 有关这些层功能的更多信息,请参阅下一节:
- VK_LAYER_GOOGLE_unique_objects
- VK_LAYER_LUNARG_api_dump
- VK_LAYER_LUNARG_core_validation
- VK_LAYER_LUNARG_image
- VK_LAYER_LUNARG_object_tracker
- VK_LAYER_LUNARG_parameter_validation
- VK_LAYER_LUNARG_swapchain
- VK_LAYER_GOOGLE_threading
-
Vulkan 调试 API 不是核心命令的一部分,核心命令可以通过加载程序静态加载。 这些调试 API 是以扩展 API 的形式存在的,可以在运行时检索并动态链接到预定义的函数指针。 因此,下一步,就是动态查询和链接调试扩展 API vkCreateDebugReportCallbackEXT 和 vkDestroyDebugReportCallbackEXT。 这两个 API 调用用于创建和销毁调试报告。
-
一旦成功检索到用于调试报告的函数指针,前一个 API(vkCreateDebugReportCallbackEXT)就会创建调试报告对象, Vulkan 会在用户自定义的回调中返回调试报告,这个回调必须链接到此 API。
-
不再需要调试时,销毁调试报告对象。
理解 LunarG 验证层以及它们的功能
LunarG Vulkan SDK 支持以下层用于调试和验证目的。 在以下几点中,我们描述了一些层,可以帮助您理解 Vulkan 提供的功能:
- VK_LAYER_GOOGLE_unique_objects:不可分发的 Vulkan 对象句柄不必是唯一的;驱动程序可以为它认为等效的多个对象返回相同的句柄。 此行为使得跟踪对象变得困难,因为在删除时不清楚要引用哪个对象。 该层在创建时将 Vulkan 对象打包为一个唯一的标识符,并在应用程序使用时将其解包。 这确保了在验证时有适当的对象生命周期跟踪(object lifetime tracking)。 根据 LunarG 的建议,该层必须位于验证层链中的最后一个,使其更靠近显示驱动程序。
- VK_LAYER_LUNARG_api_dump:该层有助于了解传递给 Vulkan API 的参数值。 它会打印所有的数据结构参数及其值。
- VK_LAYER_LUNARG_core_validation:用于验证和打印来自描述符集、管线状态、动态状态等的重要信息。 该层跟踪并验证 GPU 内存、对象绑定和命令缓冲区。 此外,它还验证图形管线和计算管线。
- VK_LAYER_LUNARG_image:该层可用于验证纹理格式、渲染目标格式等。 例如,它会验证设备上是否支持请求的格式。 它验证图像视图的创建参数对于创建视图的图像是否合理。
- VK_LAYER_LUNARG_object_tracker:跟踪对象的创建及其使用和销毁,这有助于避免内存泄漏。 它还验证所引用的对象是否已正确创建并且当前有效。
- VK_LAYER_LUNARG_parameter_validation:这个验证层确保传递给 API 的所有参数按照规范约定都是正确的,并且达到所需的期望。 它检查参数的值是否一致,并且符合 Vulkan 规范中定义的有效使用条件。 此外,它还会检查 Vulkan 控制结构的 type 字段是否包含与该类型结构所期望的、相同的值。
- VK_LAYER_LUNARG_swapchain:该层验证 WSI 交换链扩展的使用。 例如,它会在使用其函数之前检查 WSI 扩展是否可用。 此外,它还验证图像索引是否在交换链中的图像数量之内,即检查是否超出了数量范围。
- VK_LAYER_GOOGLE_threading:这对于线程安全来说是很有帮助的。 它检查多线程 API 使用的有效性。 该层确保在多线程环境下使用多个调用的若干对象的同时使用。 它报告线程规则的违规并为这些调用强制执行互斥锁。 此外,它允许应用程序继续运行而不会真正崩溃,尽管报告了线程存在的问题。
- VK_LAYER_LUNARG_standard_validation:以正确的顺序启用所有标准层。
注意
有关验证层的更多信息,请访问 LunarG 的官方网站。 查看 https://vulkan.lunarg.com/doc/sdk,并特别参考“验证层详细信息”部分以获取更多详细信息。
Vulkan 中实现调试
由于调试是通过验证层暴露的,因此调试的大多数核心实现会在 VulkanLayerAndExtension 类(VulkanLEDer / .cpp)下完成。 在本节中,我们将学习有助于我们在 Vulkan 中启用调试过程的实现细节:
Vulkan 调试工具不是核心功能默认的一部分。 因此,为了启用调试和访问报告回调功能,我们需要添加一些必要的扩展和层:
- 扩展:将 VK_EXT_DEBUG_REPORT_EXTENSION_NAME 扩展添加到实例级别。 这有助于将 Vulkan 调试 API 暴露给应用程序:
vector<const char *> instanceExtensionNames = {
. . . . // other extensios VK_EXT_DEBUG_REPORT_EXTENSION_NAME,
};
- 层:在实例级别定义以下层,以允许在这些层上进行调试:
vector<const char *> layerNames = { "VK_LAYER_GOOGLE_threading", "VK_LAYER_LUNARG_parameter_validation", "VK_LAYER_LUNARG_device_limits", "VK_LAYER_LUNARG_object_tracker", "VK_LAYER_LUNARG_image", "VK_LAYER_LUNARG_core_validation", "VK_LAYER_LUNARG_swapchain", "VK_LAYER_GOOGLE_unique_objects"
};
注意
除启用的验证层外,LunarG SDK 还提供了一个称为 VK_LAYER_LUNARG_standard_validation 的特殊层。 这样就可以按照此处提到的正确顺序启用基本的验证。 此外,此内置元数据层会以最佳顺序加载一组标准的验证层。 如果您对层没有特别指定的话,那么这是一个不错的选择。
a)VK_LAYER_GOOGLE_threading
b)VK_LAYER_LUNARG_parameter_validation
c)VK_LAYER_LUNARG_object_tracker
d)VK_LAYER_LUNARG_image
e)VK_LAYER_LUNARG_core_validation
f)VK_LAYER_LUNARG_swapchain
g)VK_LAYER_GOOGLE_unique_objects
然后将这些层提供给 vkCreateInstance()API,从而启用它们:
VulkanApplication* appObj = VulkanApplication::GetInstance(); appObj->createVulkanInstance(layerNames,
instanceExtensionNames, title);
// VulkanInstance::createInstance()
VkResult VulkanInstance::createInstance(vector<const char *>& layers, std::vector<const char *>& extensionNames,
char const*const appName)
{
. . .
VkInstanceCreateInfo instInfo = {};
// Specify the list of layer name to be enabled. instInfo.enabledLayerCount = layers.size(); instInfo.ppEnabledLayerNames = layers.data();
// Specify the list of extensions to
// be used in the application. instInfo.enabledExtensionCount = extensionNames.size(); instInfo.ppEnabledExtensionNames = extensionNames.data();
. . .
vkCreateInstance(&instInfo, NULL, &instance);
}
验证层是特定于供应商和 SDK 版本的。 因此,建议在将它们传递给 vkCreateInstance()API 之前,首先检查底层实现是否支持这些层。 这样,当运行在其他驱动程序的实现上时,应用程序始终保持可移植性。
areLayersSupported()函数是用户定义的实用程序函数,用于检查传入的层名称是否和系统支持的层冲突。 在将不支持的层提供给系统之前,不支持的层会被通知给应用程序并从层名称中移除:
// VulkanLED.cpp
VkBool32 VulkanLayerAndExtension::areLayersSupported (vector<const char *> &layerNames)
{
uint32_t checkCount = layerNames.size(); uint32_t layerCount = layerPropertyList.size(); std::vector<const char*> unsupportLayerNames; for (uint32_t i = 0; i < checkCount; i++) {
VkBool32 isSupported = 0;
for (uint32_t j = 0; j < layerCount; j++) {
if (!strcmp(layerNames[i], layerPropertyList[j]. properties.layerName)) {
isSupported = 1;
}
}
if (!isSupported) {
std::cout << "No Layer support found, removed" " from layer: "<< layerNames[i] << endl;
unsupportLayerNames.push_back(layerNames[i]);
}
else {
cout << "Layer supported: " << layerNames[i] << endl;
}
}
for (auto i : unsupportLayerNames) {
auto it = std::find(layerNames.begin(),
layerNames.end(), i);
if (it != layerNames.end()) layerNames.erase(it);
}
return true;
}
调试报告是使用 vkCreateDebugReportCallbackEXT API 创建的。 这个 API 不是 Vulkan 核心命令的一部分;因此,加载程序无法对其进行静态链接。 如果您尝试按以下方式对其进行访问,您会收到“未定义的符号引用”错误:
vkCreateDebugReportCallbackEXT(instance, NULL, NULL, NULL);
所有调试相关的 API 都需要使用 vkGetInstanceProcAddr()API 进行查询并动态链接。 检索到的 API 引用被存储在相应的函数指针中,名为 PFN_vkCreateDebugReportCallbackEXT。 VulkanLayerAndExtension :: createDebugReportCallback()函数检索创建调试和销毁调试 API,具体的实现如下所示:
/********* VulkanLED.h *********/
// Declaration of the create and destroy function pointers PFN_vkCreateDebugReportCallbackEXT dbgCreateDebugReportCallback; PFN_vkDestroyDebugReportCallbackEXT dbgDestroyDebugReportCallback;
/********* VulkanLED.cpp *********/
VulkanLayerAndExtension::createDebugReportCallback(){
. . .
// Get vkCreateDebugReportCallbackEXT API dbgCreateDebugReportCallback=(PFN_vkCreateDebugReportCallbackEXT) vkGetInstanceProcAddr(*instance,"vkCreateDebugReportCallbackEXT");
if (!dbgCreateDebugReportCallback) {
std::cout << "Error: GetInstanceProcAddr unable to locate vkCreateDebugReportCallbackEXT function.\n";
return VK_ERROR_INITIALIZATION_FAILED;
}
// Get vkDestroyDebugReportCallbackEXT API
dbgDestroyDebugReportCallback= (PFN_vkDestroyDebugReportCallbackEXT)vkGetInstanceProcAddr (*instance, "vkDestroyDebugReportCallbackEXT ");
if (!dbgDestroyDebugReportCallback) {
std::cout << "Error: GetInstanceProcAddr unable to locate vkDestroyDebugReportCallbackEXT function.\n";
return VK_ERROR_INITIALIZATION_FAILED;
}
. . .
}
vkGetInstanceProcAddr()API 动态获取实例级扩展;这些扩展不会在平台上静态暴露,并且需要通过该 API 动态链接。 以下是此 API 的签名:
PFN_vkVoidFunction vkGetInstanceProcAddr(
VkInstance instance,
const char* name);
下表介绍了 API 的各参数:
参数 | 描述 |
---|---|
instance | 这是一个 VkInstance 变量。 如果此变量为 NULL,则该名称必须是以下之一:vkEnumerateInstanceExtensionProperties,vkEnumerateInstanceLayerProperties 或 vkCreateInstance。 |
name | 这是需要查询的、用来动态链接的 API 名称。 |
使用 dbgCreateDebugReportCallback()函数指针,创建调试报告对象并将该句柄存储在 debugReportCallback 中。 API 的第二个参数接受一个 VkDebugReportCallbackCreateInfoEXT 控制结构。 这个数据结构定义了调试的行为,例如调试信息应该包含的内容:错误、一般警告、信息、与性能相关的警告、调试信息等等。
另外,它还需要用户自定义函数(debugFunction)的引用;这有助于过滤和打印从系统中检索到的调试信息。 以下是创建调试报告的语法:
struct VkDebugReportCallbackCreateInfoEXT { VkStructureType type;
const void* pNext;
VkDebugReportFlagsEXT flags; PFN_vkDebugReportCallbackEXT fnCallback; void* pUserData;
};
下表描述了上面提到的结构体字段的作用:
字段 | 描述 |
---|---|
type | 这是这个控制结构的类型信息。 必须将其指定为 VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT。 |
flags | 该字段是为了定义打开调试时要获得的调试信息的种类;下表格定义了这些标志。 |
fnCallback | 该参字段指的是过滤和显示调试消息的函数。 |
VkDebugReportFlagBitsEXT 控制结构可以显示以下标志值的按位组合:
标志位 | 描述 |
---|---|
VK_DEBUG_REPORT_INFORMATION_BIT_EXT | 这是用来显示用户友好的信息,描述当前正在运行的应用程序中的后台活动的内容,例如,调试应用程序时,资源的详细信息可能是有用的,可以用来做进一步优化的判断依据。 |
VK_DEBUG_REPORT_WARNING_BIT_EXT | 这是用来对 API 可能的错误或危险用法,提供警告消息。 |
VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT | 该参数表明 Vulkan 可能不是最佳的使用方式,可能会导致性能损失。 |
VK_DEBUG_REPORT_ERROR_BIT_EXT | 这是指错误消息,指定了错误的 API 使用,这可能导致未定义的结果 - 例如,应用程序崩溃 |
VK_DEBUG_REPORT_DEBUG_BIT_EXT | 这表示来自加载程序和层的诊断信息。 |
createDebugReportCallback 函数实现调试报告的创建。 首先,它会创建 VulkanLayerAndExtension 控制结构对象并填充相关信息。 这其中主要包括两件事情:首先,分配一个用户自定义的函数(pfnCallback),它会打印从系统接收到的调试信息(参见下一点); 其次,分配开发人员感兴趣的调试标志(flags):
/********* VulkanLED.h *********/
// Handle of the debug report callback
VkDebugReportCallbackEXT debugReportCallback;
// Debug report callback create information control structure
VkDebugReportCallbackCreateInfoEXT dbgReportCreateInfo = {};
/********* VulkanLED.cpp *********/
VulkanLayerAndExtension::createDebugReportCallback(){
. . .
// Define the debug report control structure,
// provide the reference of 'debugFunction',
// this function prints the debug information on the console.
dbgReportCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG
_REPORT_CREATE_INFO_EXT;
dbgReportCreateInfo.pfnCallback = debugFunction; dbgReportCreateInfo.pUserData = NULL; dbgReportCreateInfo.pNext = NULL;
dbgReportCreateInfo.flags = VK_DEBUG_REPORT_WARNING_BIT_EXT |
VK_DEBUG_REPORT_PERFORMANCE
_WARNING_BIT_EXT | VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_DEBUG_BIT_EXT;
// Create the debug report callback and store the handle
// into 'debugReportCallback'
result = dbgCreateDebugReportCallback
(*instance, &dbgReportCreateInfo, NULL, &debugReportCallback);
if (result == VK_SUCCESS) {
cout << "Debug report callback object created successfully\n";
}
return result;
}
定义 debugFunction()函数,以用户友好的方式打印检索到的调试信息。 它描述了调试信息的类型以及报告的消息:
VKAPI_ATTR VkBool32 VKAPI_CALL
VulkanLayerAndExtension::debugFunction( VkFlags msgFlags, VkDebugReportObjectTypeEXT objType, uint64_t srcObject,
size_t location, int32_t msgCode, const char *pLayerPrefix, const char *pMsg, void *pUserData) {
if (msgFlags & VK_DEBUG_REPORT_ERROR_BIT_EXT) {
std::cout << "[VK_DEBUG_REPORT] ERROR: ["<<layerPrefix<<"]
Code" << msgCode << ":" << msg << std::endl;
}
else if (msgFlags & VK_DEBUG_REPORT_WARNING_BIT_EXT) {
std::cout << "[VK_DEBUG_REPORT] WARNING: ["<<layerPrefix<<"]
Code" << msgCode << ":" << msg << std::endl;
}
else if (msgFlags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT) {
std::cout<<"[VK_DEBUG_REPORT] INFORMATION:[" <<layerPrefix<<"]
Code" << msgCode << ":" << msg << std::endl;
}
else if(msgFlags& VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT){
cout <<"[VK_DEBUG_REPORT] PERFORMANCE: ["<<layerPrefix<<"]
Code" << msgCode << ":" << msg << std::endl;
}
else if (msgFlags & VK_DEBUG_REPORT_DEBUG_BIT_EXT) {
cout << "[VK_DEBUG_REPORT] DEBUG: ["<<layerPrefix<<"]
Code" << msgCode << ":" << msg << std::endl;
}
else {
return VK_FALSE;
}
return VK_SUCCESS;
}
下表描述了 debugFunction()回调的各个参数:
参数 | 描述 |
---|---|
msgFlags | 该参数指定了已经触发该调用的调试事件的类型,例如错误、警告、性能警告等。 |
objType | 该参数是指由触发调用操作的对象类型。 |
srcObject | 该参数是指由触发的调用创建或操纵的对象句柄。 |
location | 该参数是指描述事件的代码位置。 |
msgCode | 该参数是指消息代码。 |
layerPrefix | 该参数是指负责触发调试事件的层。 |
msg | 该参数包含调试消息文本。 |
userData | 任何特定于应用程序的用户数据都使用此参数指定给回调。 |
提示
debugFunction 回调具有布尔返回值。 true 的返回值表示即使在发生错误后,命令链仍然会继续传递到后续的验证层。
但是,false 值指示验证层在发生错误时中止执行。 建议在第一个错误处停止执行。
错误出现的本身就已经表明意外发生了某些事情;让系统在这些情况下运行可能会导致未定义的结果或更进一步的错误,而且有时可能完全没有意义。 在后一种情况下,如果在此处执行被中止,则为开发人员提供了一个更好的机会来收集和修复报告中的错误。 相比之下,前一种方法在系统抛出大量错误的情况下,可能会使开发人员处于混乱状态,这有时会很麻烦。
为了在 vkCreateInstance 中启用调试功能,将 dbgReportCreateInfo 提供给
VkInstanceCreateInfo 结构的 pNext 字段:
VkInstanceCreateInfo instInfo = {};
. . .
instInfo.pNext = & layerExtension.dbgReportCreateInfo;
vkCreateInstance(&instInfo, NULL, &instance);
最后,一旦不再使用调试功能,就需要销毁调试回调对象:
void VulkanLayerAndExtension::destroyDebugReportCallback(){ VulkanApplication* appObj = VulkanApplication::GetInstance(); dbgDestroyDebugReportCallback(instance,debugReportCallback,NULL);
}
以下是实现的、调试报告的输出。 基于 GPU 供应商和 SDK 提供商的差异,您的输出可能与此不同。 此外,所报告的错误或警告的解释对于 SDK 本身而言是非常具体的,不同的 SDK 版本可能会存在一些差异。 但是在更高的层面上,规范仍然有效;这意味着您可以根据您打开的调试标志看到携带警告、信息、调试帮助等的调试报告。
4-001.png总结
本章简短、精确并且拥有大量的实践和实现。 在没有调试功能的情况下使用 Vulkan 就像在黑暗中没有闪光灯拍照一样。 我们非常清楚 Vulkan 需要大量的编程,而且开发人员出于显而易见的原因会经常犯错, 毕竟他们是人类。 我们从错误中学习,调试使我们能够找到并纠正这些错误。 调试功能同时还提供有建设性的信息来构建高质量的软件产品。
让我们快速回顾一下。 我们知道了 Vulkan 的调试过程。 我们查看了各种 LunarG 验证层,并理解了其中每一个所扮演的角色及其责任。 接下来,我们添加了一些选定的验证层,我们对其中的调试功能比较感兴趣。
我们还添加了暴露了调试功能的调试扩展;否则,API 的定义将无法动态链接到应用程序。 然后,我们通过 Vulkan 创建用户自定义调试报告的函数并将其链接到我们的调试报告回调上;这个回调以一种用户友好和可展示的方式组织捕获到的调试报告。
最后,我们实现了 API 来销毁调试报告的回调对象。
在下一章中,我们将了解命令缓冲区,探索它们在 Vulkan 管线中的作用,并学习如何使用它们记录和执行 API 的调用。 我们还将深入研究用于主机内存和设备内存的 Vulkan 内存管理;我们将学习各种 API 来对它们进行控制。