目录
[什么是验证层?(What are validation layers?)](#什么是验证层?(What are validation layers?))
[使用验证层(Using validation layers)](#使用验证层(Using validation layers))
[消息回调(Message callback)](#消息回调(Message callback))
什么是验证层?(What are validation layers?)
Vulkan API 的设计围绕着 "最小化驱动程序开销" 这一核心思想,该目标的体现之一是 API 默认情况下几乎不进行错误检查。即使是像将枚举值设置为错误值、向必需参数传递空指针这类简单错误,除了 C++ 类型检查外,通常也不会被显式处理,最终可能导致程序崩溃或出现未定义行为。由于 Vulkan 要求你对所有操作都明确指定,因此很容易出现一些小错误,例如使用了新的 GPU 特性,却在创建逻辑设备时忘记申请该特性。
不过,这并不意味着无法为 API 添加这些检查功能。Vulkan 引入了一套优雅的机制来解决这个问题,即验证层(validation layers)。验证层是可选组件,它们可以挂钩(hook into)Vulkan 函数调用,执行额外的操作。验证层中的常见操作包括:
- 对照规范检查参数值,以检测误用情况
- 跟踪对象的创建和销毁,查找资源泄漏
- 通过跟踪调用所属的线程,检查线程安全性
- 将所有调用及其参数记录到标准输出
- 跟踪 Vulkan 调用,用于性能分析和回放
以下是诊断型验证层中某个函数的实现示例:
cpp
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);
}
这些验证层可以自由组合堆叠,以包含你所需的所有调试功能。你可以在调试构建(debug builds)中启用验证层,在发布构建(release builds)中完全禁用它们,从而兼顾调试便利性和运行效率!
Vulkan 本身不内置任何验证层,但 LunarG Vulkan SDK 提供了一组实用的验证层,可检查常见错误。这些验证层是完全开源的,因此你可以查看它们检查的错误类型,甚至参与贡献。使用验证层是避免应用程序因意外依赖未定义行为而在不同驱动程序上运行失败的最佳方式。
验证层仅在系统中安装后才能使用。例如,LunarG 验证层仅在安装了 Vulkan SDK 的电脑上可用。
除了上述验证层外,还存在其他类型的验证层,我们建议你尝试使用。探索和测试其他验证层的任务留给读者自行完成。不过,我们建议参考《BestPractices(最佳实践)》,以获取有关如何以最新方式使用 Vulkan 的更多帮助和建议。
Vulkan 以前存在两种不同类型的验证层:实例级验证层(instance)和设备特定验证层(device specific)。其设计思路是:实例级验证层仅检查与实例等全局 Vulkan 对象相关的调用,而设备特定验证层仅检查与特定 GPU 相关的调用。目前,设备特定验证层已被弃用(deprecated),这意味着实例级验证层适用于所有 Vulkan 调用。为了兼容性,规范文档仍建议你在设备级别也启用验证层,部分实现也要求这样做。后续我们将看到,我们只需在逻辑设备级别指定与实例级别相同的验证层即可。
使用验证层(Using validation layers)
本节将介绍如何启用 Vulkan SDK 提供的标准诊断层。与扩展类似,需要通过指定验证层的名称来启用它们。所有实用的标准验证功能都捆绑在 SDK 中的一个名为 VK_LAYER_KHRONOS_validation 的层中。
首先,向程序中添加两个配置变量:一个用于指定要启用的验证层,另一个用于控制是否启用验证层。我们根据程序的编译模式(调试模式 / 发布模式)来设置启用状态。NDEBUG 宏是 C++ 标准的一部分,表示 "非调试模式"。
cpp
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 函数中执行:
cpp
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();
if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) {
return std::ranges::none_of(layerProperties,
[requiredLayer](auto const& layerProperty)
{ return strcmp(layerProperty.layerName, requiredLayer) == 0; });
}))
{
throw std::runtime_error("One or more required layers are not supported!");
}
...
}
现在,在调试模式下运行程序,确保没有出现错误。如果出现错误,请查看 FAQ(常见问题解答)。
最后,修改 VkInstanceCreateInfo 结构体的实例化代码,在启用验证层时包含验证层名称:
cpp
vk::InstanceCreateInfo createInfo{
.pApplicationInfo = &appInfo,
.enabledLayerCount = static_cast<uint32_t>(requiredLayers.size()),
.ppEnabledLayerNames = requiredLayers.data(),
.enabledExtensionCount = 0,
.ppEnabledExtensionNames = nullptr };
如果检查成功,vkCreateInstance 应该不会返回 VK_ERROR_LAYER_NOT_PRESENT 错误,但你仍需运行程序进行确认。
消息回调(Message callback)
默认情况下,验证层会将调试消息打印到标准输出,但我们也可以通过在程序中提供显式回调来自行处理这些消息。这还允许你决定想要查看的消息类型,因为并非所有消息都是(致命的)错误。如果你现在不想配置消息回调,可以直接跳至本章的最后一节。
要在程序中设置处理消息及相关细节的回调,我们需要通过 VK_EXT_debug_utils 扩展设置一个调试信使(debug messenger)和对应的回调函数。
首先,创建一个 getRequiredExtensions 函数,该函数根据是否启用验证层返回必需的扩展列表:
cpp
std::vector<const char*> getRequiredExtensions() {
uint32_t glfwExtensionCount = 0;
auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
if (enableValidationLayers) {
extensions.push_back(vk::EXTDebugUtilsExtensionName );
}
return extensions;
}
GLFW 指定的扩展是必需的(因为我们依赖 GLFW 进行窗口管理),而调试信使扩展则是按需添加的。注意,这里我们使用了 VK_EXT_DEBUG_UTILS_EXTENSION_NAME 宏,该宏等价于字符串字面量 "VK_EXT_debug_utils"。使用宏可以避免拼写错误。
现在,我们可以在 createInstance 函数中使用该函数:
cpp
auto extensions = getRequiredExtensions();
vk::InstanceCreateInfo createInfo({}, &appInfo, requiredLayers, extensions);
运行程序,确保没有收到 VK_ERROR_EXTENSION_NOT_PRESENT 错误。我们实际上不需要单独检查该扩展是否存在,因为验证层的可用性已经隐含了该扩展的存在。
接下来,我们看看调试回调函数的形式。添加一个新的静态成员函数 debugCallback,其原型为 PFN_vkDebugUtilsMessengerCallbackEXT。VKAPI_ATTR 和 VKAPI_CALL 用于确保函数具有 Vulkan 可调用的正确签名。
cpp
static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity, vk::DebugUtilsMessageTypeFlagsEXT type, const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, void*) {
std::cerr << "validation layer: type " << to_string(type) << " msg: " << pCallbackData->pMessage << std::endl;
return vk::False;
}
第一个参数指定消息的严重程度(severity),取值为以下标志之一:
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:关于无效行为且可能导致崩溃的消息
该枚举的值设计为支持比较操作,你可以通过比较来判断消息的严重程度是否达到或超过某个级别。例如:
cpp
if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) {
// 消息足够重要,需要显示
}
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 有关该回调函数的信息即可。此类回调是调试信使的一部分,你可以创建任意多个调试信使。在 instance 下方添加一个类成员来存储调试信使的句柄:
cpp
vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr;
然后,添加一个 setupDebugMessenger 函数,并在 initVulkan 函数中 createInstance 调用之后执行该函数:
cpp
void initVulkan() {
createInstance();
setupDebugMessenger();
}
void setupDebugMessenger() {
if (!enableValidationLayers) return;
}
我们需要填充一个结构体,指定调试信使及其回调的相关细节:
cpp
vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | 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 字段用于指定你希望回调处理的所有严重程度类型。此处我们指定了除 VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 之外的所有类型,以便接收有关潜在问题的通知,同时排除冗长的通用调试信息。
类似地,messageType 字段允许你过滤回调需要处理的消息类型。此处我们启用了所有类型,你可以根据需要禁用某些类型。
最后,pfnUserCallback 字段指定回调函数的指针。你可以选择通过 pUserData 字段传递一个指针,该指针将通过回调函数的 pUserData 参数传递给回调。例如,你可以使用它传递 HelloTriangleApplication 类的指针。
注意,验证层消息和调试回调还有许多其他配置方式,但对于本教程而言,上述设置足以满足入门需求。有关更多配置可能性,请参阅扩展规范。
现在,我们可以在 createInstance 函数中复用这些逻辑:
cpp
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();
if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) {
return std::ranges::none_of(layerProperties,
[requiredLayer](auto const& layerProperty)
{ return strcmp(layerProperty.layerName, requiredLayer) == 0; });
}))
{
throw std::runtime_error("One or more required layers are not supported!");
}
// 获取必需的扩展
auto requiredExtensions = getRequiredExtensions();
// 检查 Vulkan 实现是否支持所有必需的扩展
auto extensionProperties = context.enumerateInstanceExtensionProperties();
for (auto const & requiredExtension : requiredExtensions)
{
if (std::ranges::none_of(extensionProperties,
[requiredExtension](auto const& extensionProperty)
{ return strcmp(extensionProperty.extensionName, requiredExtension) == 0; }))
{
throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension));
}
}
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);
}
配置(Configuration)
除了 VkDebugUtilsMessengerCreateInfoEXT 结构体中指定的标志外,验证层的行为还有许多其他配置选项。浏览至 Vulkan SDK 目录,进入 Config 文件夹,你将找到一个 vk_layer_settings.txt 文件,该文件详细说明了如何配置验证层。
要为你自己的应用程序配置验证层设置,请将该文件复制到项目的 Debug 和 Release 目录中,并按照文件中的说明设置所需的行为。不过,在本教程的后续部分中,我们将假设你使用默认配置。
在本教程中,我们会故意制造几个错误,以向你展示验证层在捕获错误方面的实用性,并让你了解在使用 Vulkan 时明确每一步操作的重要性。接下来,让我们了解系统中的 Vulkan 设备。