消息回调

仅仅启用校验层并没有任何用处,我们不能得到任何有用的调试信息。为了获得调试信息,我们需要使用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_ATTR和VKAPI_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实例。现在,让我们编译运行程序,如果一切顺利,读者可以看到一个空白窗口,关闭空白窗口后,可以在控制台窗口看到下面的信息:

image

这说明,我们的程序还存在问题!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();
}

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