跳到主要内容

窗口表面

Vulkan是一个平台无关的API,它不能直接和窗口系统交互。为了将Vulkan渲染的图像显示在窗口上,我们需要使用WSI(Window System Integration)扩展。在本章节,我们首先介绍VK_KHR_surface扩展,它通过VkSurfaceKHR对象抽象出可供Vulkan渲染的表面。本教程中,我们使用GLFW来获取VkSurfaceKHR对象。

VK_KHR_surface是一个实例级别的扩展,它包含在glfwGetRequiredInstanceExtensions返回的扩展列表中,所以不需要再请求这一扩展,该扩展列表还包含了接下来几章使用的其他WSI扩展。

由于窗口表面对物理设备的选择有一定影响,它只能在Vulkan实例创建之后才能进行创建。

创建窗口表面

VkSurfaceKHR surface;

虽然VkSurfaceKHR对象是平台无关的,但创建它依赖窗口系统。比如,在Windows系统上,它的创建需要HWND和HMODULE句柄。有一个名为VK_KHR_win32_surface的Windows平台扩展,用于处理与Windows系统窗口交互的操作,这一扩展也包含在glfwGetRequiredInstanceExtensions返回的扩展列表中。

接下来,我们将演示如何使用这一Windows系统的扩展来创建表面,但之后的章节,我们不会使用这一特定平台的扩展,而是直接使用GLFW库来完成相关操作。我们可以使用GLFW库的glfwCreateWindowSurface函数来完成表面的创建。出于学习目的,这里演示GLFW库在背后究竟做了什么。

我们需要填写VkWin32SurfaceCreateInfoKHR结构体来完成VkSurfaceKHR对象的创建。这一结构体包含了两个非常重要的成员: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函数需要我们自己加载,加载后使用Vulkan实例、要创建的表面信息、自定义内存分配器和存储表面对象句柄的指针作为调用参数:

auto CreateWin32SurfaceKHR = (PFN_vkCreateWin32SurfaceKHR) vkGetInstanceProcAddr(instance, "vkCreateWin32SurfaceKHR");

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

其它平台的处理方式与之类似,比如Linux平台,可以通过vkCreateXcbSurfaceKHR函数创建窗口表面。

GLFW库的glfwCreateWindowSurface函数在不同平台的实现是不同的,可以跨平台使用。现在将它集成到我们的应用程序中,添加一个createSurface函数,然后在initVulkan函数中调用它:

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

void createSurface() {

}

glfwCreateWindowSurface函数的参数非常直白:

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

参数依次是VkInstance对象、GLFW窗口指针、自定义内存分配器、存储VkSurfaceKHR对象句柄的指针。创建的表面在应用程序退出前需要销毁,GLFW并没有提供销毁表面的函数,我们可以调用vkDestroySurfaceKHR完成这一工作:

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

需要注意,需要在Vulkan实例被销毁前完成表面对象的销毁。

查询呈现支持

尽管Vulkan实现可能对特定的窗口系统进行了支持,但并不意味着系统中的所有设备都支持它。所以,我们需要扩展isDeviceSuitable函数来确保设备可以在创建的表面上显示图像。

实际上,支持绘制指令的队列族和支持呈现的队列族并不一定是同一个,所以我们需要修改QueueFamilyIndices结构体,添加成员变量存储呈现队列族的索引:

struct QueueFamilyIndices {
int graphicsFamily = -1;
int presentFamily = -1;

bool isComplete() {
return graphicsFamily >= 0 && presentFamily >= 0;
}
};

接着,我们还需要修改findQueueFamilies函数,查找具有呈现能力的队列族。我们可以在检查队列族是否具有VK_QUEUE_GRAPHICS_BIT的后面,调用vkGetPhysicalDeviceSurfaceSupportKHR检查设备是否具有呈现能力:

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

然后,根据队列族中的队列数量和是否支持呈现,确定呈现队列族的索引:

if (queueFamily.queueCount > 0 && presentSupport) {
indices.presentFamily = i;
}

读者可能已经注意到,最后选择的绘制队列族和呈现队列族很有可能是同一个。但为了统一操作,即使两者是同一个,我们也将它们分开对待。实际上,读者可以选择绘制和呈现队列族为同一个的设备来提高性能。

创建呈现队列

现在可以修改逻辑设备的创建过程,创建呈现队列,并将队列句柄保存在类成员变量中:

VkQueue presentQueue;

我们需要多个VkDeviceQueueCreateInfo结构体来创建队列族,一个优雅的处理方式是使用STL的集合创建每一个不同的队列族,这样对于同一个队列族,我们只会传递它的索引一次:

#include <set>

...

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<int> uniqueQueueFamilies = {indices.graphicsFamily, indices.presentFamily};

float queuePriority = 1.0f;
for (int 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结构体的pQueueCreateInfos

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

最后,调用vkGetDeviceQueue函数获取队列句柄:

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

队列族相同时,我们获取的队列句柄也是相同的。在下一章,我们将介绍交换链,以及如何将图像显示到窗口表面。

本章节代码:C++代码