[Vulkan教程]绘制一个三角形/呈现/窗口表面(Window surface)
因为Vulkan是一个平台无关的API,它不能直接和窗口系统交互。为了建立Vulkan和窗口系统之间的连接以将渲染结果呈现到屏幕上,我们需要使用窗口系统接口(WSI)扩展。本章我们讨论第一个扩展VK_KHR_surface。它生成一个VkSurfaceKHR对象代表一个抽象的表面来呈现渲染好的图像。我们程序中的表面由我们用GLFW打开的窗口来支持。
VK_KHR_surface扩展是一个实例级别的扩展,实际上我们已经启用它了,因为它已经被包含在glfwGetRequiredInstanceExtensions函数返回的列表中了。这个列表中同时包含一些其它的窗口系统接口扩展,我们会在之后的章节中用到它们。
窗口表面需要在实例创建之后就被创建,因为它会影响物理设备的选择。原因我们之后在介绍,因为窗口表面实际上算是渲染和呈现相关的内容,在这里解释会使我们基本设置的内容不好理解。还应该注意的是,窗口表面在Vulkan是一个可选的组件,离屏渲染就用不到它。Vulkan允许你这样做,不需要像OpenGL一样必须创建一个不可见的窗口。
创建窗口表面
首先我们在调试回调下面添加一个surface成员变量。
VkSurfaceKHR surface;1
虽然VkSurfaceKHR对象和它的使用是平台无关的,但它的创建并不是,因为它取决于窗口系统(细节)。例如,在Windows上它需要HWND和HMODULE句柄,在X11(Linux上一个常见的窗口系统)上它需要xcb_window_t和xcb_connection_t*。因此,这里需要一个平台特定的附加扩展,在Windows上是VK_KHR_win32_surface,在X11上是VK_KHR_xcb_surface,它也会被自动包含在glfwGetRequiredInstanceExtensions函数返回的列表中。
我会演示一下在Windows上如何使用这个平台特定的扩展来创建一个表面,但在本教程中我们不会使用这种方式。既然用了GLFW,就没必要用平台特定的代码了。GLFW提供了glfwCreateWindowSurface函数来帮我们处理平台的差异性。我们用它之前了解一下它在幕后做了什么还是很好的。
为了访问平台方法,你需要在包含头文件时定义一些宏:
#define VK_USE_PLATFORM_WIN32_KHR#define GLFW_INCLUDE_VULKAN#include <GLFW/glfw3.h>#define GLFW_EXPOSE_NATIVE_WIN32#include <GLFW/glfw3native.h>12345
窗口表面是一个Vulkan对象,所以他也需要通过VkWind32SurfaceCreateInfoKHR来创建。它有两个重要的参数hwnd和hinstance。分别是窗口句柄和进程。
VkWin32SurfaceCreateInfoKHR createInfo{};createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;createInfo.hwnd = glfwGetWin32Window(window);createInfo.hinstance = GetModuleHandle(nullptr);1234
glfwGetWin32Window函数用来通过GLFW窗口对象获取HWND。GetModuleHandle函数用来获取当前进程的句柄。
这之后,可以通过vkCreateWin32SurfaceKHR函数创建表面。参数分别是实例、表面创建信息、自定义分配器和用来存储表面句柄的变量。从技术上来讲,创建表面算一个WSI扩展功能,但它太常用了,以至于标准Vulkan加载器都会包含它,不需要像其它扩展一样需要显式加载它。
if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {throw std::runtime_error("failed to create window surface!");}123
其它平台上创建表面的过程是相似的,像Linux的X11,vkCreateXcbSurfaceKHR需要的参数是一个XCB连接和窗口句柄。
glfwCreateWindowSurface函数在不同平台上有不同的实现来完成正确的操作。我们把它集成到我们的程序中。我们在setupDebugMessenger函数下面创建一个函数createSurface,然后在initVulkan中调用。
void initVulkan() {createInstance();setupDebugMessenger();createSurface();pickPhysicalDevice();createLogicalDevice();}void createSurface() {}1234567891011
GLFW调用不适用结构体形式的参数,这使得createSurface函数的实现非常简单:
void createSurface() {if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {throw std::runtime_error("failed to create window surface!");}}12345
参数分别是VkInstance、GLFW窗口指针、自定义分配器和指向VkSurfaceKHR变量的指针。它返回来自平台相关调用的结果。GLFW并没有提供专用的函数来销毁表面,但可以简单地通过原本的API来完成:
void cleanup() {...vkDestroySurfaceKHR(instance, surface, nullptr);vkDestroyInstance(instance, nullptr);...}123456
注意要在实例销毁前销毁表面。
查询呈现支持
虽然Vulkan的实现可能支持对窗口系统的集成,但这并不意味着系统中的所有设备都支持它。所以我们需要扩展isDeviceSuitable函数,来确保设备可以将图像呈现到我们创建的表面。因为呈现是一个队列相关的特性,主要问题是找到一个支持我们呈现图像到我们创建的表面的队列簇。
支持绘图命令的队列和支持呈现的队列不一定是同一个。因此,我们需要修改一下QueueFamilyIndices结构体,加一个呈现队列的下标。
struct QueueFamilyIndices {std::optional<uint32_t> graphicsFamily;std::optional<uint32_t> presentFamily;bool isComplete() {return graphicsFamily.has_value() && presentFamily.has_value();}};12345678
然后,我们修改一下findQurueFamilies函数,来查找一个具有呈现能力的队列簇。函数vkGetPhysicalDeviceSurfaceSupportKHR可以用来检查是否具有,它的参数分别是物理设备、队列簇下标和表面。在VK_QURUE_GRAPHICS_BIT循环中同时调用它:
VkBool32 presentSupport = false;vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);12
然后检查bool值,如果支持就保存队列簇索引:
if (presentSupport) {indices.presentFamily = i;}123
其实,绘制队列和呈现队列可能是同一个,但在程序中,我们将它们视为单独的队列,以实现统一的方法。当然,你可以添加一些逻辑,首选拥有同时支持绘制和呈现的队列簇的物理设备以提高性能。
创建呈现队列
最后一件事就是修改逻辑设备的创建过程,创建呈现队列,并获取VkQueue句柄。为此句柄添加一个成员变量:
VkQueue presentQueue;1
然后,我们可能需要两个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);}123456789101112131415161718
然后修改VkDeviceCreateInfo指向数组:
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());createInfo.pQueueCreateInfos = queueCreateInfos.data();12
如果队列簇相同,我们只需要传递一次它的索引。最后,我们添加一个调用来取回队列句柄:
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);1
如果队列簇相同,那两个句柄现在很可能有相同的值。下一章,我们会研究交换链,看它如何让我们可以将图像呈现到表面。
