[Vulkan教程]绘制一个三角形/呈现/窗口表面(Window surface)

因为Vulkan是一个平台无关的API,它不能直接和窗口系统交互。为了建立Vulkan和窗口系统之间的连接以将渲染结果呈现到屏幕上,我们需要使用窗口系统接口(WSI)扩展。本章我们讨论第一个扩展VK_KHR_surface。它生成一个VkSurfaceKHR对象代表一个抽象的表面来呈现渲染好的图像。我们程序中的表面由我们用GLFW打开的窗口来支持。

VK_KHR_surface扩展是一个实例级别的扩展,实际上我们已经启用它了,因为它已经被包含在glfwGetRequiredInstanceExtensions函数返回的列表中了。这个列表中同时包含一些其它的窗口系统接口扩展,我们会在之后的章节中用到它们。

窗口表面需要在实例创建之后就被创建,因为它会影响物理设备的选择。原因我们之后在介绍,因为窗口表面实际上算是渲染和呈现相关的内容,在这里解释会使我们基本设置的内容不好理解。还应该注意的是,窗口表面在Vulkan是一个可选的组件,离屏渲染就用不到它。Vulkan允许你这样做,不需要像OpenGL一样必须创建一个不可见的窗口。

创建窗口表面

首先我们在调试回调下面添加一个surface成员变量。

  1. VkSurfaceKHR surface;
  2. 1

虽然VkSurfaceKHR对象和它的使用是平台无关的,但它的创建并不是,因为它取决于窗口系统(细节)。例如,在Windows上它需要HWNDHMODULE句柄,在X11(Linux上一个常见的窗口系统)上它需要xcb_window_txcb_connection_t*。因此,这里需要一个平台特定的附加扩展,在Windows上是VK_KHR_win32_surface,在X11上是VK_KHR_xcb_surface,它也会被自动包含在glfwGetRequiredInstanceExtensions函数返回的列表中。

我会演示一下在Windows上如何使用这个平台特定的扩展来创建一个表面,但在本教程中我们不会使用这种方式。既然用了GLFW,就没必要用平台特定的代码了。GLFW提供了glfwCreateWindowSurface函数来帮我们处理平台的差异性。我们用它之前了解一下它在幕后做了什么还是很好的。

为了访问平台方法,你需要在包含头文件时定义一些宏:

  1. #define VK_USE_PLATFORM_WIN32_KHR
  2. #define GLFW_INCLUDE_VULKAN
  3. #include <GLFW/glfw3.h>
  4. #define GLFW_EXPOSE_NATIVE_WIN32
  5. #include <GLFW/glfw3native.h>
  6. 12345

窗口表面是一个Vulkan对象,所以他也需要通过VkWind32SurfaceCreateInfoKHR来创建。它有两个重要的参数hwndhinstance。分别是窗口句柄和进程。

  1. VkWin32SurfaceCreateInfoKHR createInfo{};
  2. createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
  3. createInfo.hwnd = glfwGetWin32Window(window);
  4. createInfo.hinstance = GetModuleHandle(nullptr);
  5. 1234

glfwGetWin32Window函数用来通过GLFW窗口对象获取HWNDGetModuleHandle函数用来获取当前进程的句柄。

这之后,可以通过vkCreateWin32SurfaceKHR函数创建表面。参数分别是实例、表面创建信息、自定义分配器和用来存储表面句柄的变量。从技术上来讲,创建表面算一个WSI扩展功能,但它太常用了,以至于标准Vulkan加载器都会包含它,不需要像其它扩展一样需要显式加载它。

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

其它平台上创建表面的过程是相似的,像Linux的X11,vkCreateXcbSurfaceKHR需要的参数是一个XCB连接和窗口句柄。

glfwCreateWindowSurface函数在不同平台上有不同的实现来完成正确的操作。我们把它集成到我们的程序中。我们在setupDebugMessenger函数下面创建一个函数createSurface,然后在initVulkan中调用。

  1. void initVulkan() {
  2. createInstance();
  3. setupDebugMessenger();
  4. createSurface();
  5. pickPhysicalDevice();
  6. createLogicalDevice();
  7. }
  8. void createSurface() {
  9. }
  10. 1234567891011

GLFW调用不适用结构体形式的参数,这使得createSurface函数的实现非常简单:

  1. void createSurface() {
  2. if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
  3. throw std::runtime_error("failed to create window surface!");
  4. }
  5. }
  6. 12345

参数分别是VkInstance、GLFW窗口指针、自定义分配器和指向VkSurfaceKHR变量的指针。它返回来自平台相关调用的结果。GLFW并没有提供专用的函数来销毁表面,但可以简单地通过原本的API来完成:

  1. void cleanup() {
  2. ...
  3. vkDestroySurfaceKHR(instance, surface, nullptr);
  4. vkDestroyInstance(instance, nullptr);
  5. ...
  6. }
  7. 123456

注意要在实例销毁前销毁表面。

查询呈现支持

虽然Vulkan的实现可能支持对窗口系统的集成,但这并不意味着系统中的所有设备都支持它。所以我们需要扩展isDeviceSuitable函数,来确保设备可以将图像呈现到我们创建的表面。因为呈现是一个队列相关的特性,主要问题是找到一个支持我们呈现图像到我们创建的表面的队列簇。

支持绘图命令的队列和支持呈现的队列不一定是同一个。因此,我们需要修改一下QueueFamilyIndices结构体,加一个呈现队列的下标。

  1. struct QueueFamilyIndices {
  2. std::optional<uint32_t> graphicsFamily;
  3. std::optional<uint32_t> presentFamily;
  4. bool isComplete() {
  5. return graphicsFamily.has_value() && presentFamily.has_value();
  6. }
  7. };
  8. 12345678

然后,我们修改一下findQurueFamilies函数,来查找一个具有呈现能力的队列簇。函数vkGetPhysicalDeviceSurfaceSupportKHR可以用来检查是否具有,它的参数分别是物理设备、队列簇下标和表面。在VK_QURUE_GRAPHICS_BIT循环中同时调用它:

  1. VkBool32 presentSupport = false;
  2. vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
  3. 12

然后检查bool值,如果支持就保存队列簇索引:

  1. if (presentSupport) {
  2. indices.presentFamily = i;
  3. }
  4. 123

其实,绘制队列和呈现队列可能是同一个,但在程序中,我们将它们视为单独的队列,以实现统一的方法。当然,你可以添加一些逻辑,首选拥有同时支持绘制和呈现的队列簇的物理设备以提高性能。

创建呈现队列

最后一件事就是修改逻辑设备的创建过程,创建呈现队列,并获取VkQueue句柄。为此句柄添加一个成员变量:

  1. VkQueue presentQueue;
  2. 1

然后,我们可能需要两个VkDeviceQueueCreateInfo来从不同的簇创建队列。一个比较优雅的方式是:对于相同的队列簇我们创建相同的队列:

  1. #include <set>
  2. ...
  3. QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
  4. std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
  5. std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};
  6. float queuePriority = 1.0f;
  7. for (uint32_t queueFamily : uniqueQueueFamilies) {
  8. VkDeviceQueueCreateInfo queueCreateInfo{};
  9. queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
  10. queueCreateInfo.queueFamilyIndex = queueFamily;
  11. queueCreateInfo.queueCount = 1;
  12. queueCreateInfo.pQueuePriorities = &queuePriority;
  13. queueCreateInfos.push_back(queueCreateInfo);
  14. }
  15. 123456789101112131415161718

然后修改VkDeviceCreateInfo指向数组:

  1. createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
  2. createInfo.pQueueCreateInfos = queueCreateInfos.data();
  3. 12

如果队列簇相同,我们只需要传递一次它的索引。最后,我们添加一个调用来取回队列句柄:

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

如果队列簇相同,那两个句柄现在很可能有相同的值。下一章,我们会研究交换链,看它如何让我们可以将图像呈现到表面。