[Vulkan教程]绘制一个三角形/设置/验证层(Validation layers)

什么是验证层

Vulkan API设计理念之一就是最小化驱动程序开销,表现之一就是默认情况下,API中的错误检查非常有限。即使是像不正确的枚举或空指针传递这样的简单错误,通常也不会被明确处理,只会导致崩溃或未定义的行为。Vulkan需要你自己清楚自己做的所有事情,我们可能很容易出一些小错误,比如在使用GPU新特性时,忘记在创建逻辑设备时请求它。

Vulkan引入了一个优雅的系统——验证层,来为API增加检查功能。验证层是一个可选的组件,它可以被绑定到Vulkan函数调用中以应用其它操作。验证层中常见的操作有:

  • 检测参数值是否满足规范要求
  • 跟踪对象的创建和销毁以发现资源泄露
  • 跟踪调用线程来检查线程安全
  • 将函数调用及其参数记录到标准输出
  • 跟踪Vulkan调用以进行分析和重放

诊断验证层函数实现实例:

  1. VkResult vkCreateInstance(
  2. const VkInstanceCreateInfo* pCreateInfo,
  3. const VkAllocationCallbacks* pAllocator,
  4. VkInstance* instance) {
  5. if (pCreateInfo == nullptr || instance == nullptr) {
  6. log("Null pointer passed to required parameter!");
  7. return VK_ERROR_INITIALIZATION_FAILED;
  8. }
  9. return real_vkCreateInstance(pCreateInfo, pAllocator, instance);
  10. }

这些验证层可以自由堆叠以包含你感兴趣的所有调试功能。您可以简单地在调试版本启用验证层,在发布版本完全禁用它们,完美!

Vulkan没有内置任何验证层,但LunarG Vulkan SDK提供了一组不错的验证层来检查常见的错误。它们完全开源,你可以看它们都检查了什么错误,也可以为它们贡献代码。使用验证层可以防止你的程序在不同的显卡上被一些奇怪的未定义行为中断。

验证层需要先安装才能使用。要使用LunarG提供的验证层,你得先安装Vulkan SDK。

以前,Vulkan中你可以选择在实例或特定设备上启用验证层。在实例上启用验证层是为了检查全局对象相关的调用,在特定设备上启用是为了检查和特定设备相关的调用。设备层验证层已经被启用了,实例层验证层会检查所有调用。考虑到兼容性规范文档仍然建议你在特定设备启用验证层,这可能是某些Vulkan实现所必需的。我们会简单地在实例和设备层指定相同的验证层

开启验证层

在本节中,我们将看到如何启用 Vulkan SDK 提供的标准验证层。和扩展一样,验证层需要通过名称来启用。所有有用的标准验证层都被捆绑到了一个层中——VK_LAYER_KHRONOS_validation

我们首先在程序中添加两个配置变量,一个用来指定要启用的层,一个用来标记我们是否启用它们。通过宏NDEBUG(“not debug”),仅在调试模式下启用验证层。

  1. const uint32_t WIDTH = 800;
  2. const uint32_t HEIGHT = 600;
  3. const std::vector<const char*> validationLayers = {
  4. "VK_LAYER_KHRONOS_validation"
  5. };
  6. #ifdef NDEBUG
  7. const bool enableValidationLayers = false;
  8. #else
  9. const bool enableValidationLayers = true;
  10. #endif

我们添加一个新的函数checkValidationLayerSupport来检查需要的验证层是否可用。首先使用[vkEnumerateInstanceLayerProperties](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkEnumerateInstanceLayerProperties.html)函数列出所有可用的层。它的用法和创建实例一章中的[vkEnumrateInstanceExtensionProperties](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkEnumerateInstanceExtensionProperties.html)相同。

  1. bool checkValidationLayerSupport() {
  2. uint32_t layerCount;
  3. vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
  4. std::vector<VkLayerProperties> availableLayers(layerCount);
  5. vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
  6. return false;
  7. }

然后,检查我们需要的验证层validationLayers是否都存在availableLayers中。引入<cstring>头文件中的strcmp

  1. for (const char* layerName : validationLayers) {
  2. bool layerFound = false;
  3. for (const auto& layerProperties : availableLayers) {
  4. if (strcmp(layerName, layerProperties.layerName) == 0) {
  5. layerFound = true;
  6. break;
  7. }
  8. }
  9. if (!layerFound) {
  10. return false;
  11. }
  12. }
  13. return true;

现在,我们在createInstance中调用:

  1. void createInstance() {
  2. if (enableValidationLayers && !checkValidationLayerSupport()) {
  3. throw std::runtime_error("validation layers requested, but not available!");
  4. }
  5. //...
  6. }

现在可以在调试模式下运行一下程序来确保没有发生错误。如果发生错误了,可以先看一下FAQ。

最后,修改一下[VkInstanceCreateInfo](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkInstanceCreateInfo.html)结构体,来包含要启用的验证层名称:

  1. if (enableValidationLayers) {
  2. createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
  3. createInfo.ppEnabledLayerNames = validationLayers.data();
  4. } else {
  5. createInfo.enabledLayerCount = 0;
  6. }

如果成了,vkCreateInstance不会返回VK_ERROR_LAYER_NOT_PRESENT错误。你可以运行程序确认一下。

消息回调

验证层默认会把调试信息打印到标准输出,我们可以通过指定一个回调来自己处理调试信息。你可以只打印你想要的信息。如果你现在还不想这样做,你可以直接跳到最后一节。

为了在程序中设置一个回调来处理消息和详细信息,我们需要使用VK_EXT_debug_utils扩展。

我们首先定义一个函数getRequiredExtensions来返回我们需要的扩展,在开启验证层时,我们添加一个额外的扩展:

  1. std::vector<const char*> getRequiredExtensions() {
  2. uint32_t glfwExtensionCount = 0;
  3. const char** glfwExtensions;
  4. glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
  5. std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
  6. if (enableValidationLayers) {
  7. extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
  8. }
  9. return extensions;
  10. }

GLFW指定的扩展是必须的,调试信息输出回调扩展按需添加。请注意,我这里使用了VK_EXT_DEBUG_UTILS_EXTENSION_NAME宏,它等于字符串"VK_EXT_debug_utils",使用宏可以防止错别字。

现在,我们可以在createInstance函数中使用该函数:

  1. auto extensions = getRequiredExtensions();
  2. createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
  3. createInfo.ppEnabledExtensionNames = extensions.data();

运行程序,确保你没有收到VK_ERROR_EXTENSION_NOT_PRESENT错误。我们不需要检查这个扩展是否可用,有验证层就有它。

现在,我们看看调试回调函数长啥样。新建一个新的静态成员函数debugCallback,类型为PFN_vkDebugUtilsMessengerCallbackEXTVKAPI_ATTRVKAPI+CALL确保函数拥有正确的签名来让Vulkan调用。

  1. static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
  2. VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
  3. VkDebugUtilsMessageTypeFlagsEXT messageType,
  4. const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
  5. void* pUserData) {
  6. std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
  7. return VK_FALSE;
  8. }

第一个参数指明了消息的严重性,为以下值之一:

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT:诊断消息
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:信息性消息,如创建资源
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:不一定错误的行为消息,但你的程序很可能有BUG
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:无效的行为消息,并可能造成崩溃

你可以把这个值同某个严重性等级比较:

  1. if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
  2. // Message is important enough to show
  3. }

mesageType参数可有以下值:

  • VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT:与规范或性能无关
  • VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT:违反了规范或预示一个可能的错误
  • VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT:对Vulkan有潜在的非最优使用

pCallbackDataVkDebugUtilsMessengerCallbackDataEXT类型,它包含消息本身的细节,最重要的成员是:

  • pMessage:C风格字符串调试信息
  • pObject:和消息有关的Vulkan对象句柄数组
  • objectCount:数组中对象的个数

最后一个参数pUserData是你在初始化消息回调时指定的回传参数。

返回值用来指示Vulkan是否中止函数调用。如果此处返回true,会导致Vulkan调用因VK_ERROR_VALIDATION_FAILED_EXT错误而中止。这通常用来测试验证层自身,所以你应该一直返回VK_FALSE

剩下的工作就是要告诉Vulkan该回调函数。或许会让人惊讶,在Vulkan中调试回调也需要由一个句柄来管理,显式创建和销毁。回调是调试信使的一部分,你可以想咋弄咋弄。在instance下,添加一个新的成员变量:

  1. VkDebugUtilsMessengerEXT debugMessenger;

然后添加一个函数setupDebugMessenger,在initVulkan中调用createInstance之后调用:

  1. void initVulkan() {
  2. createInstance();
  3. setupDebugMessenger();
  4. }
  5. void setupDebugMessenger() {
  6. if (!enableValidationLayers) return;
  7. }

我们填充一下调试信使创建信息结构体:

  1. VkDebugUtilsMessengerCreateInfoEXT createInfo{};
  2. createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
  3. createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
  4. createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
  5. createInfo.pfnUserCallback = debugCallback;
  6. createInfo.pUserData = nullptr; // Optional

messageSeverity成员允许你过滤特定严重级别的信息。我这里没有启用VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT,因为我只想看可能有问题的信息,而忽略那些一般的信息。

messageType一样,允许你过滤特定类型的信息。这边我都允许了。你可以只允许你想要的。

pfnUserCallback用来指定回调函数指针。你可以通过pUserData将一个值传递给回调函数的pUserData参数。例如,你可以将HelloTriangleApplication的指针传递过去。

其实,还有很多方法可以配置验证层消息和调试毁掉,对刚开始本教程的我们而言,以上方法挺好的。其它方法你可以参考扩展规范

调试信使创建信息结构体应该被传给vkCreateDebugUtilsMessengerEXT函数来创建一个VkDebugUtilsMessengerEXT对象。这个函数是一个扩展函数,它不会自动加载。我们需要通过[vkGetInstanceProcAddr](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkGetInstanceProcAddr.html)来自己查找它的地址。我们创建一个代理函数来包装这个过程,我已经把它添加到HelloTriangleApplication类定义的上面。

  1. VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {
  2. auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
  3. if (func != nullptr) {
  4. return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
  5. } else {
  6. return VK_ERROR_EXTENSION_NOT_PRESENT;
  7. }
  8. }

如果函数找不到,[vkGetInstanceProcAdddr](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkGetInstanceProcAddr.html)函数就会返回nullptr。我们现在可以调用这个函数来创建一个扩展对象:

  1. if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
  2. throw std::runtime_error("failed to set up debug messenger!");
  3. }

倒数第二个参数还是可选的分配器回调,仍设为nullptr,其它参数都很好理解。调试信使特定于Vulkan实例或它的层,这需要通过第一个参数来显式指定。之后,你还会在其它子对象中看到这种模式。

VkDebugUtilsMessengerEXT对象也需要通过vkDestroyDebugUtilsMessengerEXT函数来销毁。同vkCreateDebugUtilsMessengerEXT函数一样,该函数也需要被显式加载。

创建另一个代理函数CreateDebugUtilsMessengerEXT

  1. void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
  2. auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
  3. if (func != nullptr) {
  4. func(instance, debugMessenger, pAllocator);
  5. }
  6. }

确保这个函数是一个类静态函数或全局函数。之后我们可以在cleanup函数中调用它:

  1. void cleanup() {
  2. if (enableValidationLayers) {
  3. DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
  4. }
  5. vkDestroyInstance(instance, nullptr);
  6. glfwDestroyWindow(window);
  7. glfwTerminate();
  8. }

调试实例的创建和销毁

虽然我们已经将验证层的调试添加到程序中了,但它并没有覆盖所有内容。vkCreateDebugUtilsMessengerEXT的调用需要在实例创建之后,vkDestroyDebugUtilsMessengerEXT的调用需要在实例销毁之前。这意味着我们目前无法调试[vkCreateInstance](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCreateInstance.html)[vkDestroyInstance](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkDestroyInstance.html)

如果你仔细阅读了扩展文档的话,你会知道,有一个方法可以专门为这两个函数调用创建调试信使。它只需要你通过[VKInstanceCreateInfo](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkInstanceCreateInfo.html)的扩展成员pNext传递一个VkDebugUtilsMessengerCreateInfoEXT指针就行了。首先将VkDebugUtilsMessengerCreateInfoEXT的创建搞到一个函数中:

  1. void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
  2. createInfo = {};
  3. createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
  4. createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
  5. createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
  6. createInfo.pfnUserCallback = debugCallback;
  7. }
  8. ...
  9. void setupDebugMessenger() {
  10. if (!enableValidationLayers) return;
  11. VkDebugUtilsMessengerCreateInfoEXT createInfo;
  12. populateDebugMessengerCreateInfo(createInfo);
  13. if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
  14. throw std::runtime_error("failed to set up debug messenger!");
  15. }
  16. }

然后在createInstance中使用:

  1. void createInstance() {
  2. ...
  3. VkInstanceCreateInfo createInfo{};
  4. createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
  5. createInfo.pApplicationInfo = &appInfo;
  6. ...
  7. VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
  8. if (enableValidationLayers) {
  9. createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
  10. createInfo.ppEnabledLayerNames = validationLayers.data();
  11. populateDebugMessengerCreateInfo(debugCreateInfo);
  12. createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo;
  13. } else {
  14. createInfo.enabledLayerCount = 0;
  15. createInfo.pNext = nullptr;
  16. }
  17. if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
  18. throw std::runtime_error("failed to create instance!");
  19. }
  20. }

debugCreateInfo变量放在if语句的外面,以确保它不会在[vkCreateInstance](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCreateInstance.html)调用之前被销毁。这种方式创建的调试信使会在[vkCreateInstance](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCreateInstance.html)[vkDestroyInstance](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkDestroyInstance.html)期间被使用并在此之后清理。

测试

现在我们可以故意犯一个错误来看看验证层的表现。暂时删除cleanup中对DestroyDebugUtilsMessengerEXT的调用,然后运行程序。程序退出的时候,你应该会看到这个:

  1. validation layer: Validation Error: [ VUID-vkDestroyInstance-instance-00629 ] Object 0: handle = 0x110fab0, type = VK_OBJECT_TYPE_INSTANCE; Object 1: handle = 0x10000000001, type = VK_OBJECT_TYPE_DEBUG_UTILS_MESSENGER_EXT; | MessageID = 0x8b3d8e18 | OBJ ERROR : For VkInstance 0x110fab0[], VkDebugUtilsMessengerEXT 0x10000000001[] has not been destroyed. The Vulkan spec states: All child objects created using instance must have been destroyed prior to destroying instance (https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#VUID-vkDestroyInstance-instance-00629)
  2. validation layer: Validation Error: [ VUID-vkDestroyInstance-instance-00629 ] Object 0: handle = 0x110fab0, type = VK_OBJECT_TYPE_INSTANCE; Object 1: handle = 0x10000000001, type = VK_OBJECT_TYPE_DEBUG_UTILS_MESSENGER_EXT; | MessageID = 0x8b3d8e18 | OBJ ERROR : For VkInstance 0x110fab0[], VkDebugUtilsMessengerEXT 0x10000000001[] has not been destroyed. The Vulkan spec states: All child objects created using instance must have been destroyed prior to destroying instance (https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#VUID-vkDestroyInstance-instance-00629)

如果你啥也没看到,你应该检查一下是否安装成功

如果你想看是哪个调用触发的消息,你可以在消息回调添加一个断点,然后检查一下调用堆栈。

配置

除了在VkDebugUtilsMessengerCreateInfoEXT结构体中设置的标志位外,对验证层的行为还有很多设置。浏览一下Vulkan SDK的Config目录,你会找到一个叫vk_layer_settings.txt的文件,它介绍了如何配置层。

在这个教程中,我会假设你使用默认设置。如果你想有自己的层设置,你可以看手册。

在本教程中,我会通过故意犯一些错误的方式,来向你展示我们在捕获它们时验证层所提供的帮助。让你认识到,在使用Vulkan时清楚你的行为非常重要。

好的,下一章物理设备和队列簇。