窗口表面
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
对象的创建。这一结构体包含了两个非常重要的成员:hwnd
和hinstance
,分别对应窗口句柄和进程实例句柄:
VkWin32SurfaceCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);
glfwGetWin32Window
从GLFW窗口对象获取窗口句柄hwnd
,GetModuleHandle
获取当前进程的实例句柄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++代码