狸村 Mystic Web · 狸村
目录

什么是验证层?

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

然而,这并不意味着不能将这些检查添加到 API 中。Vulkan 为此引入了一个优雅的系统,称为 验证层(validation layers)。验证层是可选的组件,它们挂钩到 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 验证层仅适用于安装了 Vulkan SDK 的 PC。

还存在其他层,我们建议使用它们。我们将发现和尝试其他层的工作留给读者作为练习。不过,我们建议查看 BestPractices 层,以获得更多关于如何以更新的方式使用 Vulkan 的帮助和建议。

以前 Vulkan 中有两种不同类型的验证层:实例层和设备特定层。其想法是实例层仅检查与全局 Vulkan 对象(如实例)相关的调用,而设备特定层仅检查与特定 GPU 相关的调用。设备特定层现已弃用,这意味着实例验证层适用于所有 Vulkan 调用。

使用验证层

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

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

constexpr uint32_t WIDTH = 800;
constexpr uint32_t HEIGHT = 600;

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

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

我们将检查所有请求的层是否可用。我们需要遍历请求的层,并验证 Vulkan 实现是否支持所有必需的层。此检查直接在 createInstance 函数中执行:

void createInstance()
{
    ...

    // 获取所需的层
    std::vector<char const*> requiredLayers;
    if (enableValidationLayers)
    {
        requiredLayers.assign(validationLayers.begin(), validationLayers.end());
    }

    // 检查 Vulkan 实现是否支持所需的层。
    auto layerProperties = context.enumerateInstanceLayerProperties();
    auto unsupportedLayerIt = std::ranges::find_if(requiredLayers,
		                                               [&layerProperties](auto const &requiredLayer) {
			                                               return std::ranges::none_of(layerProperties,
                                                                [requiredLayer](auto const &layerProperty) { return strcmp(layerProperty.layerName, requiredLayer) == 0; });
		                                               });
    if (unsupportedLayerIt != requiredLayers.end())
    {
        throw std::runtime_error("Required layer not supported: " + std::string(*unsupportedLayerIt));
    }

    ...
}

使用扩展

如上所述,扩展也需要通过其名称来启用。但在这里,你必须区分实例扩展和设备扩展。

我们首先创建一个 getRequiredInstanceExtensions 函数,它将返回所需的实例扩展列表。

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

    std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);

    return extensions;
}

由 GLFW 指定的扩展始终是必需的,因为我们依赖于 GLFW 进行窗口管理。

我们将检查所有必需的扩展是否可用。我们首先使用 vk::raii::Context::enumerateInstanceLayerProperties 函数获取所有受支持的实例扩展列表,并检查所有必需的层是否都在该列表中。此检查也直接在 createInstance 函数中执行:

void createInstance()
{
    ...

    // 获取所需的扩展。
    auto requiredExtensions = getRequiredInstanceExtensions();

    // 检查 Vulkan 实现是否支持所需的扩展。
    auto extensionProperties = context.enumerateInstanceExtensionProperties();
    auto unsupportedLayerIt = std::ranges::find_if(requiredLayers,
                                                   [&layerProperties](auto const &requiredLayer) {
                                                       return std::ranges::none_of(layerProperties,
                                                                                   [requiredLayer](auto const &layerProperty) { return strcmp(layerProperty.layerName, requiredLayer) == 0; });
                             });
    if (unsupportedPropertyIt != requiredExtensions.end())
    {
        throw std::runtime_error("Required extension not supported: " + std::string(*unsupportedPropertyIt));
    }

    ...
}

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

最后,修改 vk::InstanceCreateInfo 结构体实例化,以包含验证层名称和扩展名称:

void createInstance()
{
    ...

    vk::InstanceCreateInfo createInfo{.pApplicationInfo        = &appInfo,
                                      .enabledLayerCount       = static_cast<uint32_t>(requiredLayers.size()),
                                      .ppEnabledLayerNames     = requiredLayers.data(),
                                      .enabledExtensionCount   = static_cast<uint32_t>(requiredExtensions.size()),
                                      .ppEnabledExtensionNames = requiredExtensions.data()};
    instance = vk::raii::Instance(context, createInfo);
}

如果检查成功,那么 vk::raii::Instance 构造函数应该永远不会抛出 vk::Result::eErrorLayerNotPresent 错误,但你应该运行程序以确保万无一失。

消息回调

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

为了在程序中设置回调以处理消息及其相关细节,我们必须使用 VK_EXT_debug_utils 扩展设置一个带有回调的调试信使。

我们首先根据是否启用验证层来扩展 getRequiredInstanceExtensions 函数:

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

    std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
    if (enableValidationLayers)
    {
        extensions.push_back(vk::EXTDebugUtilsExtensionName);
    }

    return extensions;
}

调试信使扩展被有条件地添加。请注意,我们在这里使用了 vk::EXTDebugUtilsExtensionName 宏,它等同于字面量字符串 "VK_EXT_debug_utils"。使用此宏可以避免拼写错误。

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

static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT       severity,
                                                      vk::DebugUtilsMessageTypeFlagsEXT              type,
                                                      const vk::DebugUtilsMessengerCallbackDataEXT * pCallbackData,
                                                      void *                                         pUserData)
{
  std::cerr << "validation layer: type " << to_string(type) << " msg: " << pCallbackData->pMessage << std::endl;

  return vk::False;
}

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

  • vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose:来自 Vulkan 组件(如加载器、层和驱动程序)的诊断消息

  • vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo :信息性消息,如创建资源

  • vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning:关于不一定错误但很可能在你的应用程序中是 bug 的行为的消息

  • vk::DebugUtilsMessageSeverityFlagBitsEXT::eError :关于无效并可能导致崩溃的行为的消息

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

if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) {
    // 消息足够重要,需要显示
}

messageType 参数可以具有以下值:

  • vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral :发生了一些与规范或性能无关的事件

  • vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation :发生了违反规范或表明可能存在错误的事情

  • vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance:可能未最佳使用 Vulkan

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

  • pMessage :以 null 结尾的调试消息字符串

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

  • objectCount:数组中的对象数量

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

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

现在剩下的就是告诉 Vulkan 有关回调函数的信息。这样的回调是 调试信使(debug messenger) 的一部分,你可以拥有任意多个。在 instance 正下方为此句柄添加一个类成员:

vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr;

现在添加一个函数 setupDebugMessenger,在 createInstance 之后立即从 initVulkan 中调用:

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

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

}

我们需要填充一个包含信使及其回调详细信息的结构体:

void setupDebugMessenger()
{
    ...
    vk::DebugUtilsMessageSeverityFlagsEXT severityFlags(vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning |
                                                        vk::DebugUtilsMessageSeverityFlagBitsEXT::eError);
    vk::DebugUtilsMessageTypeFlagsEXT     messageTypeFlags(
            vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation);
    vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{.messageSeverity = severityFlags,
                                                                          .messageType     = messageTypeFlags,
                                                                          .pfnUserCallback = &debugCallback};
    debugMessenger = instance.createDebugUtilsMessengerEXT( debugUtilsMessengerCreateInfoEXT );
}

messageSeverity 字段允许你指定你希望回调被调用的所有严重性类型。我们只对提示应用程序问题并可能需要修复的消息感兴趣。因此我们请求 eWarningeError 标志。

类似地,messageType 字段允许你过滤你的回调会收到通知的消息类型。我们在这里简单地启用了所有类型。如果某些类型对你没有用,你随时可以禁用它们。

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

请注意,配置验证层消息和调试回调还有很多其他方法,但对于本教程来说,这是一个很好的入门设置。有关更多信息,请参见 扩展规范

完整的 createInstance 函数现在看起来像:

void createInstance()
{
    constexpr vk::ApplicationInfo appInfo{ .pApplicationName   = "Hello Triangle",
                .applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ),
                .pEngineName        = "No Engine",
                .engineVersion      = VK_MAKE_VERSION( 1, 0, 0 ),
                .apiVersion         = vk::ApiVersion14 };

    // 获取所需的层
    std::vector<char const*> requiredLayers;
    if (enableValidationLayers)
        {
            requiredLayers.assign(validationLayers.begin(), validationLayers.end());
        }

    // 检查 Vulkan 实现是否支持所需的层。
    auto layerProperties = context.enumerateInstanceLayerProperties();
    auto unsupportedLayerIt = std::ranges::find_if(requiredLayers,
                                                   [&layerProperties](auto const &requiredLayer) {
                                                       return std::ranges::none_of(layerProperties,
                                                                                   [requiredLayer](auto const &layerProperty) { return strcmp(layerProperty.layerName, requiredLayer) == 0; });
                                                   });
    if (unsupportedLayerIt != requiredLayers.end())
    {
        throw std::runtime_error("Required layer not supported: " + std::string(*unsupportedLayerIt));
    }

    // 获取所需的扩展。
    auto requiredExtensions = getRequiredInstanceExtensions();

    // 检查 Vulkan 实现是否支持所需的扩展。
    auto extensionProperties = context.enumerateInstanceExtensionProperties();
		auto unsupportedPropertyIt =
		    std::ranges::find_if(requiredExtensions,
		                         [&extensionProperties](auto const &requiredExtension) {
			                         return std::ranges::none_of(extensionProperties,
			                                                     [requiredExtension](auto const &extensionProperty) { return strcmp(extensionProperty.extensionName, requiredExtension) == 0; });
		                         });
		if (unsupportedPropertyIt != requiredExtensions.end())
		{
			throw std::runtime_error("Required extension not supported: " + std::string(*unsupportedPropertyIt));
		}

    vk::InstanceCreateInfo createInfo{
        .pApplicationInfo        = &appInfo,
        .enabledLayerCount       = static_cast<uint32_t>(requiredLayers.size()),
        .ppEnabledLayerNames     = requiredLayers.data(),
        .enabledExtensionCount   = static_cast<uint32_t>(requiredExtensions.size()),
        .ppEnabledExtensionNames = requiredExtensions.data() };
    instance = vk::raii::Instance(context, createInfo);
}

配置

除了 vk::DebugUtilsMessengerCreateInfoEXT 结构体中指定的标志之外,验证层的行为还有很多设置。浏览到 Vulkan SDK 并转到 Config 目录。在那里你会找到一个 vk_layer_settings.txt 文件,它解释了如何配置这些层。

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

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

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。