跳到主要内容

校验层

什么是校验层?

Vulkan API是按最小化驱动程序开销的理念进行设计的,所以默认情况下,Vulkan API提供的错误检查非常有限。很多像参数设置为不正确的枚举值或空指针这样简单的错误都没有处理,遇到这些错误程序会直接崩溃或者发生未定义行为。Vukan需要我们明确定义每一个操作,所以就很容易犯一些小错误,比如使用了一个新GPU特性,却忘记在创建逻辑设备时请求这一特性。

但并不意味着我们不能将错误检查加入API调用中,Vulkan为此引入了校验层来解决这个问题。校验层是可选组件,可以在Vulkan API函数调用上附加校验操作,校验层通常会做下面的工作:

  • 检测参数值是否合法
  • 追踪对象的创建和销毁操作,发现资源泄漏问题
  • 追踪调用来自的线程,检测是否线程安全。
  • 将API调用和调用的参数写入日志
  • 追踪API调用,根据需要进行分析和回放

下面的代码演示了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上使用。

Vulkan以前有两种不同类型的校验层:实例校验层和设备校验层。实例校验层只检查和全局Vulkan对象(如实例)相关的调用,设备校验层只检查和特定GPU相关的调用。设备校验层现在已经弃用,也就是说,现在实例校验层可检测所有Vulkan API调用。

使用校验层

在本节,我们将介绍如何使用Vulkan SDK提供的标准校验层。和使用扩展一样,使用校验层需要指定校验层的名称。Vulkan SDK允许我们通过VK_LAYER_KHRONOS_validation来隐式地开启所有可用的校验层。

我们通过条件编译宏来设定是否启用校验层,代码中的NDEBUG宏是C++标准的一部分,表示是否处于非调试模式下:

const int WIDTH = 800;
const int 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获取所有可用的校验层列表,这个函数的用法和前面我们在创建Vulkan实例章节中使用的vkEnumerateInstanceExtensionProperties函数相同。

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

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

return false;
}

接着,检查是否validationLayers列表中的校验层都可以在availableLayers列表中找到:

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!");
}

...
}

现在,在调试模式下编译运行程序,确保没有出现错误,如果出现错误,请确保正确安装了Vulkan SDK。如果程序报告缺少可用的校验层,可以查阅Vulkan SDK的官方文档寻找解决方法。

最后,修改我们之前的填写的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错误。校验层可用隐含地说明对应的扩展存在,所以不需要额外做扩展是否存在的检查。

现在,让我们看看接受调试信息的回调函数是什么样子。在程序中以vkDebugUtilsMessengerCallbackEXT为原型添加一个debugCallback静态函数,函数使用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:一个以null结尾的包含调试信息的字符串
  • pObjects:存储和消息相关的Vulkan对象句柄的数组
  • objectCount:数组中的对象个数

最后一个参数pUserData是一个指向我们设置回调函数时,传递的数据的指针。

回调函数返回了一个布尔值,表示引发校验层处理的Vulkan API调用是否被中断,如果返回值为true,对应Vulkan API调用就会返回VK_ERROR_VALIDATION_FAILED_EXT错误代码。通常只在测试校验层本身时会返回true,其余情况下,回调函数应该返回VK_FALSE

定义完回调函数,接下来要做的就是让Vulkan使用这一回调函数。我们需要一个VkDebugUtilsMessengerEXT对象来存储回调函数信息,然后将它提交给Vulkan完成回调函数的设置:

VkDebugUtilsMessengerEXT callback;

现在,在initVulkan函数中,createInstance函数之后添加一个setupDebugCallback函数:

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

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

}

我们需要填写VkDebugUtilsMessengerCreateInfoEXT结构体所需的信息:

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是一个指向用户自定义数据的指针,它是可选的,这个指针所指的地址会被作为回调函数的参数,用来向回调函数传递用户数据。

有许多方式配置校验层消息和回调,更多信息可以参考规范文档的扩展

填写完结构体信息后,我们将它作为函数vkCreateDebugUtilsMessengerEXT的参数来创建VkDebugUtilsMessengerEXT对象。由于vkCreateDebugUtilsMessengerEXT函数是一个扩展函数,不会被Vulkan库自动加载,所以需要使用vkGetInstanceProcAddr函数来加载它。这里我们创建了一个代理函数,来载入vkCreateDebugUtilsMessengerEXT函数:

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

vkGetInstanceProcAddr函数如果不能被加载,那么我们的代理函数就会返回nullptr。现在我们可以使用这个代理函数来创建扩展对象了:

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

倒数第二个参数是可选的分配器回调函数,我们没有自定义的分配器,所以设置为nullptr。由于我们的调试回调是针对Vulkan实例和它的校验层,所以把第一个参数设置为Vulkan实例。

VkDebugUtilsMessengerEXT对象需要在程序结束前通过调用vkDestroyDebugUtilsMessengerEXT来清除。和vkCreateDebugUtilsMessengerEXT函数相同,Vulkan库没有自动加载这个函数,需要我们自己加载它。

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

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

这个代理函数需要定义为类的静态成员函数或者定义在类外面,我们在cleanup函数中调用它:

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

vkDestroyInstance(instance, nullptr);

glfwDestroyWindow(window);

glfwTerminate();
}

现在编译运行程序,如果一切顺利,将不会出现错误信息。如果读者想要了解到底是哪个函数调用引发了错误消息,可以在处理消息的回调函数设置断点,然后运行程序,观察程序在断点位置时的调用栈,就可以确定引发错误消息的函数调用。

更多配置

除了VkDebugUtilsMessengerCreateInfoEXT结构体指定的标志外,还有大量可以决定校验层行为的设置。读者可以浏览Vulkan SDK的Config目录,里面有一个vk_layer_settings.txt解释了如何配置校验层。

读者可以将vk_layer_settings.txt 复制到自己项目的Debug和Release目录来使用它,并根据需要修改设置。本教程只使用vk_layer_settings.txt的默认设置。

在之后的章节,我们会故意造成一些错误,来演示如何使用校验层来发现这些错误,帮助读者理解校验层的重要性。现在,是时候来看一看系统中的Vulkan设备了。

本章节代码:C++代码