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