验证层

什么是验证层?

Vulkan API 的设计理念是最小化驱动程序开销,而这一目标的体现之一是默认情况下 API 中的错误检查非常有限。即使是像将枚举设置为不正确的值或将空指针传递给必需参数这样简单的错误,通常也不会显式处理,只会导致崩溃或未定义的行为。因为 Vulkan 要求您对所做的每一件事都非常明确,所以很容易犯许多小错误,例如使用新的 GPU 功能,但忘记在创建逻辑设备时请求它。

但是,这并不意味着这些检查不能添加到 API 中。Vulkan 引入了一个优雅的系统,称为验证层。验证层是可选组件,它们会挂接到 Vulkan 函数调用中以应用其他操作。验证层中的常见操作是

  • 对照规范检查参数的值,以检测滥用

  • 跟踪对象的创建和销毁,以查找资源泄漏

  • 通过跟踪调用来源的线程来检查线程安全性

  • 将每个调用及其参数记录到标准输出

  • 跟踪 Vulkan 调用以进行分析和重放

以下是诊断验证层中函数实现的一个示例

VkResult vkCreateInstance(
    const VkInstanceCreateInfo* pCreateInfo,
    const VkAllocationCallbacks* pAllocator,
    VkInstance* instance) {

    if (pCreateInfo == nullptr || instance == nullptr) {
        log("Null pointer passed to required parameter!");
        return VK_ERROR_INITIALIZATION_FAILED;
    }

    return real_vkCreateInstance(pCreateInfo, pAllocator, instance);
}

这些验证层可以自由堆叠,以包含您感兴趣的所有调试功能。您可以简单地为调试构建启用验证层,并为发布构建完全禁用它们,这使您可以两全其美!

Vulkan 不附带任何内置的验证层,但 LunarG Vulkan SDK 提供了一组很好的层,用于检查常见错误。它们也完全开源,因此您可以检查它们检查哪种类型的错误并做出贡献。使用验证层是通过意外依赖未定义的行为来避免应用程序在不同驱动程序上崩溃的最佳方法。

只有在系统上安装了验证层才能使用它们。获取这些层的一种方法是通过 LunarG SDK。

使用验证层

在本节中,我们将看到如何启用 Vulkan SDK 提供的标准诊断层。与扩展一样,验证层需要通过指定其名称来启用。所有有用的标准验证都捆绑在 SDK 中包含的一个层中,该层称为 VK_LAYER_KHRONOS_validation

让我们首先向程序添加两个配置变量,以指定要启用的层以及是否启用它们。我选择根据程序是否在调试模式下编译来确定该值。NDEBUG 宏是 C++ 标准的一部分,表示“非调试”。

const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;

const std::vector<const char*> validationLayers = {
    "VK_LAYER_KHRONOS_validation"
};

#ifdef NDEBUG
    const bool enableValidationLayers = false;
#else
    const bool enableValidationLayers = true;
#endif

我们将添加一个新函数 checkValidationLayerSupport,该函数检查是否所有请求的层都可用。首先使用 vkEnumerateInstanceLayerProperties 函数列出所有可用的层。它的用法与实例创建章节中讨论的 vkEnumerateInstanceExtensionProperties 相同。

bool checkValidationLayerSupport() {
    uint32_t layerCount;
    vkEnumerateInstanceLayerProperties(&layerCount, nullptr);

    std::vector<VkLayerProperties> availableLayers(layerCount);
    vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());

    return false;
}

接下来,检查 validationLayers 中的所有层是否存在于 availableLayers 列表中。您可能需要包含 <cstring> 以使用 strcmp

for (const char* layerName : validationLayers) {
    bool layerFound = false;

    for (const auto& layerProperties : availableLayers) {
        if (strcmp(layerName, layerProperties.layerName) == 0) {
            layerFound = true;
            break;
        }
    }

    if (!layerFound) {
        return false;
    }
}

return true;

我们现在可以在 createInstance 中使用此函数

void createInstance() {
    if (enableValidationLayers && !checkValidationLayerSupport()) {
        throw std::runtime_error("validation layers requested, but not available!");
    }

    ...
}

现在以调试模式运行程序,并确保不会发生错误。如果确实发生错误,请查看常见问题解答。

最后,修改 VkInstanceCreateInfo 结构体实例化,以在启用验证层时包含验证层名称

if (enableValidationLayers) {
    createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
    createInfo.ppEnabledLayerNames = validationLayers.data();
} else {
    createInfo.enabledLayerCount = 0;
}

如果检查成功,则 vkCreateInstance 永远不应返回 VK_ERROR_LAYER_NOT_PRESENT 错误,但您应该运行程序以确保这一点。

消息回调

验证层默认会将调试消息打印到标准输出,但我们也可以通过在程序中提供显式回调来自己处理它们。这还允许您决定要查看哪种类型的消息,因为并非所有消息都一定是(致命)错误。如果您现在不想这样做,则可以跳到本章的最后一节。

要在程序中设置回调以处理消息和相关详细信息,我们必须使用 VK_EXT_debug_utils 扩展设置一个带有回调的调试消息传递器。

我们首先创建一个 getRequiredExtensions 函数,该函数将根据是否启用验证层来返回所需的扩展列表

std::vector<const char*> getRequiredExtensions() {
    uint32_t glfwExtensionCount = 0;
    const char** glfwExtensions;
    glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

    std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);

    if (enableValidationLayers) {
        extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
    }

    return extensions;
}

始终需要 GLFW 指定的扩展,但会条件性地添加调试消息传递器扩展。请注意,我在此处使用了 VK_EXT_DEBUG_UTILS_EXTENSION_NAME 宏,它等于文字字符串“VK_EXT_debug_utils”。使用此宏可让您避免拼写错误。

我们现在可以在 createInstance 中使用此函数

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

运行程序,确保没有收到 VK_ERROR_EXTENSION_NOT_PRESENT 错误。我们实际上不需要检查此扩展是否存在,因为它应该由验证层的可用性来暗示。

现在我们来看看调试回调函数是什么样的。添加一个新的静态成员函数,名为 debugCallback,使用 PFN_vkDebugUtilsMessengerCallbackEXT 原型。VKAPI_ATTRVKAPI_CALL 确保该函数具有 Vulkan 调用它的正确签名。

static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
    VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
    VkDebugUtilsMessageTypeFlagsEXT messageType,
    const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
    void* pUserData) {

    std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;

    return VK_FALSE;
}

第一个参数指定消息的严重程度,它是以下标志之一

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT:诊断消息

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:信息性消息,例如资源的创建

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:关于不一定是错误的行为的消息,但很可能是应用程序中的错误

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:关于无效且可能导致崩溃的行为的消息

此枚举的值的设置方式使您可以使用比较操作来检查消息是否等于或比某个严重级别更严重,例如

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

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:Vulkan 的潜在非最佳使用

pCallbackData 参数是指一个 VkDebugUtilsMessengerCallbackDataEXT 结构,其中包含消息本身的详细信息,最重要的成员是

  • pMessage:作为空终止字符串的调试消息

  • pObjects:与消息相关的 Vulkan 对象句柄数组

  • objectCount:数组中的对象数

最后,pUserData 参数包含一个在回调设置期间指定的指针,允许您将自己的数据传递给它。

回调返回一个布尔值,指示是否应中止触发验证层消息的 Vulkan 调用。如果回调返回 true,则调用将以 VK_ERROR_VALIDATION_FAILED_EXT 错误中止。这通常仅用于测试验证层本身,因此您应始终返回 VK_FALSE

现在剩下的就是告诉 Vulkan 关于回调函数。或许有点令人惊讶的是,即使在 Vulkan 中,调试回调也是用需要显式创建和销毁的句柄来管理的。这样的回调是调试信使的一部分,您可以根据需要拥有任意多个。在此句柄的 instance 下面添加一个类成员

VkDebugUtilsMessengerEXT debugMessenger;

现在添加一个函数 setupDebugMessenger,以便在 createInstance 之后从 initVulkan 中调用

void initVulkan() {
    createInstance();
    setupDebugMessenger();
}

void setupDebugMessenger() {
    if (!enableValidationLayers) return;

}

我们需要填写一个结构,其中包含有关信使及其回调的详细信息

VkDebugUtilsMessengerCreateInfoEXT createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
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;
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;
createInfo.pfnUserCallback = debugCallback;
createInfo.pUserData = nullptr; // Optional

messageSeverity 字段允许您指定希望调用回调的所有严重性类型。我在此处指定了除 VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 之外的所有类型,以接收有关可能问题的通知,同时排除详细的常规调试信息。

类似地,messageType 字段允许您筛选通知回调的消息类型。我在此处简单地启用了所有类型。如果它们对您没有用处,您可以随时禁用一些类型。

最后,pfnUserCallback 字段指定回调函数的指针。您可以选择性地将指针传递给 pUserData 字段,该指针将通过 pUserData 参数传递给回调函数。例如,您可以使用它将指针传递给 HelloTriangleApplication 类。

请注意,还有许多其他方法可以配置验证层消息和调试回调,但是这是开始本教程的一个很好的设置。有关可能性的更多信息,请参阅 扩展规范

此结构应传递给 vkCreateDebugUtilsMessengerEXT 函数以创建 VkDebugUtilsMessengerEXT 对象。不幸的是,由于此函数是扩展函数,因此不会自动加载。我们必须使用 vkGetInstanceProcAddr 自己查找其地址。我们将创建自己的代理函数,在后台处理此操作。我将其添加到 HelloTriangleApplication 类定义的正上方。

VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {
    auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
    if (func != nullptr) {
        return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
    } else {
        return VK_ERROR_EXTENSION_NOT_PRESENT;
    }
}

如果无法加载该函数,则 vkGetInstanceProcAddr 函数将返回 nullptr。如果可用,我们现在可以调用此函数来创建扩展对象

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

倒数第二个参数再次是我们将设置为 nullptr 的可选分配器回调,除此之外,这些参数非常简单。由于调试信使特定于我们的 Vulkan 实例及其层,因此需要将其显式指定为第一个参数。您稍后还会在其他对象中看到这种模式。

还需要使用对 vkDestroyDebugUtilsMessengerEXT 的调用来清理 VkDebugUtilsMessengerEXT 对象。与 vkCreateDebugUtilsMessengerEXT 类似,该函数也需要显式加载。

CreateDebugUtilsMessengerEXT 下面创建另一个代理函数

void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
    auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
    if (func != nullptr) {
        func(instance, debugMessenger, pAllocator);
    }
}

确保此函数是静态类函数或类外的函数。然后我们可以在 cleanup 函数中调用它

void cleanup() {
    if (enableValidationLayers) {
        DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
    }

    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);

    glfwTerminate();
}

调试实例的创建和销毁

尽管我们现在已通过验证层向程序添加了调试功能,但我们尚未涵盖所有内容。vkCreateDebugUtilsMessengerEXT 调用需要已创建的有效实例,并且 vkDestroyDebugUtilsMessengerEXT 必须在销毁实例之前调用。这目前使我们无法调试 vkCreateInstancevkDestroyInstance 调用中的任何问题。

但是,如果您仔细阅读扩展文档,您会看到有一种方法可以专门为这两个函数调用创建一个单独的调试工具信使。它要求您仅将指向 VkDebugUtilsMessengerCreateInfoEXT 结构的指针传递到 VkInstanceCreateInfopNext 扩展字段中。首先将信使创建信息的填充提取到一个单独的函数中

void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
    createInfo = {};
    createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
    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;
    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;
    createInfo.pfnUserCallback = debugCallback;
}

...

void setupDebugMessenger() {
    if (!enableValidationLayers) return;

    VkDebugUtilsMessengerCreateInfoEXT createInfo;
    populateDebugMessengerCreateInfo(createInfo);

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

现在我们可以在 createInstance 函数中重用它

void createInstance() {
    ...

    VkInstanceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    createInfo.pApplicationInfo = &appInfo;

    ...

    VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
    if (enableValidationLayers) {
        createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
        createInfo.ppEnabledLayerNames = validationLayers.data();

        populateDebugMessengerCreateInfo(debugCreateInfo);
        createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo;
    } else {
        createInfo.enabledLayerCount = 0;

        createInfo.pNext = nullptr;
    }

    if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
        throw std::runtime_error("failed to create instance!");
    }
}

debugCreateInfo 变量放置在 if 语句之外,以确保它在 vkCreateInstance 调用之前不会被销毁。通过以这种方式创建额外的调试信使,它将在 vkCreateInstancevkDestroyInstance 期间自动使用,并在之后进行清理。

测试

现在让我们故意犯一个错误,看看验证层是如何工作的。暂时删除cleanup函数中对DestroyDebugUtilsMessengerEXT的调用,并运行你的程序。程序退出后,你应该看到类似这样的信息:

validation layer test

如果你没有看到任何消息,请检查你的安装

如果你想查看哪个调用触发了消息,你可以在消息回调中添加断点,并查看堆栈跟踪。

配置

验证层的行为设置远不止VkDebugUtilsMessengerCreateInfoEXT结构中指定的标志。浏览到 Vulkan SDK 并进入 Config 目录。在那里你会找到一个 vk_layer_settings.txt 文件,该文件解释了如何配置层。

要为你自己的应用程序配置层设置,请将该文件复制到你项目的 DebugRelease 目录,并按照说明设置所需的行为。但是,在本教程的其余部分,我将假设你正在使用默认设置。

在整个教程中,我将故意犯一些错误,以向你展示验证层在捕获错误方面的帮助有多大,并教你了解确切地知道你在使用 Vulkan 做什么有多么重要。现在是时候查看系统中的 Vulkan 设备了。