窗口表面

由于 Vulkan 是一个与平台无关的 API,它本身无法直接与窗口系统接口。为了建立 Vulkan 和窗口系统之间的连接,以便将结果呈现到屏幕上,我们需要使用 WSI(窗口系统集成)扩展。在本章中,我们将讨论第一个扩展,即 VK_KHR_surface。它公开了一个 VkSurfaceKHR 对象,该对象表示一种抽象的表面类型,用于呈现渲染的图像。我们程序中的表面将由我们已经使用 GLFW 打开的窗口支持。

VK_KHR_surface 扩展是实例级别的扩展,我们实际上已经启用了它,因为它包含在 glfwGetRequiredInstanceExtensions 返回的列表中。该列表还包括一些我们将在接下来的几章中使用的其他 WSI 扩展。

窗口表面需要在实例创建后立即创建,因为它实际上会影响物理设备的选择。我们推迟这一点的原因是,窗口表面是渲染目标和呈现这一更大主题的一部分,对此的解释会使基本设置变得混乱。还应该注意的是,窗口表面在 Vulkan 中是一个完全可选的组件,如果您只需要屏幕外渲染。Vulkan 允许您在没有像创建不可见窗口这样的技巧(OpenGL 需要)的情况下执行此操作。

窗口表面创建

首先在调试回调的下方添加一个 surface 类成员。

VkSurfaceKHR surface;

尽管 VkSurfaceKHR 对象及其使用是与平台无关的,但它的创建不是,因为它取决于窗口系统的详细信息。例如,它需要 Windows 上的 HWNDHMODULE 句柄。因此,该扩展有一个特定于平台的添加,在 Windows 上称为 VK_KHR_win32_surface,并且也自动包含在 glfwGetRequiredInstanceExtensions 的列表中。

我将演示如何使用这个特定于平台的扩展在 Windows 上创建表面,但我们实际上不会在本教程中使用它。使用像 GLFW 这样的库,然后继续使用特定于平台的代码没有任何意义。GLFW 实际上具有 glfwCreateWindowSurface,可以为我们处理平台差异。尽管如此,在我们开始依赖它之前,了解它在幕后做了什么还是有好处的。

要访问本机平台功能,您需要更新顶部的 include

#define VK_USE_PLATFORM_WIN32_KHR
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>

因为窗口表面是一个 Vulkan 对象,所以它带有一个需要填充的 VkWin32SurfaceCreateInfoKHR 结构。它有两个重要的参数:hwndhinstance。这些是窗口和进程的句柄。

VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);

glfwGetWin32Window 函数用于从 GLFW 窗口对象获取原始的 HWNDGetModuleHandle 调用返回当前进程的 HINSTANCE 句柄。

之后,可以使用 vkCreateWin32SurfaceKHR 创建表面,其中包括实例、表面创建详细信息、自定义分配器以及要存储表面句柄的变量的参数。从技术上讲,这是一个 WSI 扩展函数,但它非常常用,以至于标准 Vulkan 加载器包含了它,因此与其它扩展不同,您不需要显式加载它。

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

对于其他平台(如 Linux)类似,其中 vkCreateXcbSurfaceKHR 使用 X11 将 XCB 连接和窗口作为创建详细信息。

glfwCreateWindowSurface 函数使用每个平台的不同实现来执行此操作。我们现在将其集成到我们的程序中。添加一个函数 createSurface,以便在实例创建和 setupDebugMessenger 之后从 initVulkan 调用。

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
}

void createSurface() {

}

GLFW 调用采用简单的参数而不是结构体,这使得函数的实现非常简单

void createSurface() {
    if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
        throw std::runtime_error("failed to create window surface!");
    }
}

这些参数是 VkInstance、GLFW 窗口指针、自定义分配器以及指向 VkSurfaceKHR 变量的指针。它只是简单地传递来自相关平台调用的 VkResult。GLFW 不提供用于销毁表面的特殊函数,但这可以通过原始 API 轻松完成

void cleanup() {
        ...
        vkDestroySurfaceKHR(instance, surface, nullptr);
        vkDestroyInstance(instance, nullptr);
        ...
    }

确保在实例之前销毁表面。

查询演示支持

尽管 Vulkan 实现可能支持窗口系统集成,但这并不意味着系统中的每个设备都支持它。因此,我们需要扩展 isDeviceSuitable 以确保设备可以将图像呈现到我们创建的表面。由于演示是特定于队列的功能,因此问题实际上是关于查找支持演示到我们创建的表面的队列族。

实际上,支持绘图命令的队列族和支持演示的队列族可能不重叠。因此,我们必须考虑到可能存在不同的演示队列,方法是修改 QueueFamilyIndices 结构

struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;
    std::optional<uint32_t> presentFamily;

    bool isComplete() {
        return graphicsFamily.has_value() && presentFamily.has_value();
    }
};

接下来,我们将修改 findQueueFamilies 函数,以查找具有向窗口表面呈现能力的队列族。用于检查的函数是 vkGetPhysicalDeviceSurfaceSupportKHR,它接受物理设备、队列族索引和表面作为参数。在与 VK_QUEUE_GRAPHICS_BIT 相同的循环中添加对它的调用。

VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);

然后只需检查布尔值并存储呈现队列族的索引。

if (presentSupport) {
    indices.presentFamily = i;
}

请注意,最终它们很可能属于同一个队列族,但在整个程序中,我们将它们视为独立的队列,以保持统一的方法。尽管如此,您可以添加逻辑,以明确偏好在同一队列中支持绘图和呈现的物理设备,以提高性能。

创建呈现队列

剩下的就是修改逻辑设备创建过程,以创建呈现队列并检索 VkQueue 句柄。为该句柄添加一个成员变量。

VkQueue presentQueue;

接下来,我们需要多个 VkDeviceQueueCreateInfo 结构体,以从两个队列族创建队列。一种优雅的方法是创建一个包含所有必要队列所需的所有唯一队列族的集合。

#include <set>

...

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};

float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = queueFamily;
    queueCreateInfo.queueCount = 1;
    queueCreateInfo.pQueuePriorities = &queuePriority;
    queueCreateInfos.push_back(queueCreateInfo);
}

并修改 VkDeviceCreateInfo 以指向该向量。

createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();

如果队列族相同,那么我们只需要传递它的索引一次。最后,添加一个调用来检索队列句柄。

vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);

如果队列族相同,那么现在这两个句柄很可能具有相同的值。在下一章中,我们将研究交换链以及它们如何使我们能够将图像呈现到表面。