目录
Querying for presentation support
Creating the presentation queue
由于Vulkan是一个与平台无关的API,它不能单独与窗口系统直接交互。为了在Vulkan和窗口系统之间建立连接以向屏幕显示结果,我们需要使用WSI(窗口系统集成)扩展。在本章中,我们将讨论第一个,即VK_KHR_surface。它公开了一个VkSurfaceKHR对象,该对象表示要呈现渲染图像的抽象类型的曲面。程序中的曲面将由我们已经用GLFW打开的窗口支持。
VK_KHR_surface扩展是一个实例级扩展,我们实际上已经启用了它,因为它包含在glfwGetRequiredInstanceExtensions返回的列表中。该列表还包括我们将在接下来的几章中使用的一些其他WSI扩展。
窗口曲面需要在实例创建之后立即创建,因为它实际上会影响物理设备选择。我们推迟这一步的原因是,窗口曲面是渲染目标和演示的更大主题的一部分,对此的解释会打乱基本设置。还应该注意的是,如果您只需要屏幕外渲染,则窗口曲面是Vulkan中完全可选的组件。Vulkan允许您无需像创建一个不可见的窗口(OpenGL所必需的)就可以做到这一点。
Window surface creation
首先在调试回调的正下方添加一个surface类成员。
VkSurfaceKHR surface;
尽管VkSurfaceKHR对象及其用法与平台无关,但它的创建并不是因为它依赖于窗口系统的细节。例如,它需要Windows上的HWND和HMODULE句柄。因此,该扩展中添加了一个特定于平台的扩展,在Windows上称为VK_KHR_win32_surface,也自动包含在glfwGetRequiredInstanceExtensions的列表中。
我将演示如何使用这个特定于平台的扩展在Windows上创建曲面,但在本教程中我们不会实际使用它。使用GLFW这样的库,然后继续使用特定于平台的代码是没有任何意义的。GLFW实际上有glfwCreateWindowSurface为我们处理平台差异。不过,在我们开始依赖它之前,看看它在幕后做了什么还是很不错的。
要访问本机平台功能,需要更新顶部的includes:
#define VK_USE_PLATFORM_WIN32_KHR
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>
因为窗口表面是Vulkan对象,所以它带有需要填充的VkWin32SurfaceCreateInfoKHR结构。它有两个重要参数: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创建曲面,其中包含实例的参数、曲面创建详细信息、自定义分配器和要存储的曲面句柄的变量。从技术上讲,这是一个WSI扩展函数,但它非常常用,标准Vulkan加载器包含它,因此与其他扩展不同,您不需要显式加载它。
if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
throw std::runtime_error("failed to create window surface!");
}
该过程与Linux等其他平台类似,其中vkCreateXcbSurfaceKHR采用XCB连接和窗口作为X11的创建细节。
glfwCreateWindowSurface函数使用每个平台的不同实现来执行此操作。现在我们将把它集成到我们的程序中。在实例创建和setupDebugMessenger之后,添加一个函数createSurface,以便从initVulkan调用。
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
}
void createSurface() {
}
GLFW调用采用简单的参数而不是结构,这使得函数的实现非常简单:
void createSurface() {
if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
throw std::runtime_error("failed to create window surface!");
}
}
参数包括VkInstance、GLFW窗口指针、自定义分配器和指向VkSurfaceKHR变量的指针。它只需通过相关平台调用的VkResult。GLFW不提供破坏表面的特殊功能,但可以通过原始API轻松实现:
void cleanup() {
...
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
...
}
确保在实例之前销毁曲面。
Querying for presentation support
尽管Vulkan实现可能支持窗口系统集成,但这并不意味着系统中的每个设备都支持它。因此,我们需要扩展isDeviceFit,以确保设备可以向我们创建的表面显示图像。由于表示是一个特定于队列的特性,所以问题实际上是如何找到一个支持向我们创建的曲面表示的队列系列。
实际上,支持图形命令的队列族和支持表示的队列族可能不重叠。因此,我们必须考虑通过修改QueueFamilyIndices结构可能会有一个不同的表示队列:
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() {
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
接下来,我们将修改findQueueFamilies函数,以查找能够呈现给窗口表面的队列系列。要检查的函数是vkGetPhysicalDeviceSurfaceSupportKHR,它将物理设备、队列系列索引和表面作为参数。在与VK_QUEUE_GRAPHICS_BIT相同的循环中添加对它的调用:
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
然后只需检查布尔值并存储表示系列队列索引:
if (presentSupport) {
indices.presentFamily = i;
}
请注意,这些队列最终很可能是同一个队列系列,但在整个程序中,我们会将它们视为统一方法的单独队列。尽管如此,您可以添加逻辑,明确选择支持在同一队列中绘制和显示的物理设备,以提高性能。
Creating the presentation queue
剩下的一件事是修改逻辑设备创建过程,以创建表示队列并检索VkQueue句柄。为句柄添加成员变量:
VkQueue presentQueue;
接下来,我们需要有多个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);
}
并修改VkDeviceCreateInfo以指向vector:
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
如果队列系列相同,那么我们只需要传递一次它的索引。最后,添加一个调用以检索队列句柄:
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
如果队列系列相同,那么两个句柄现在很可能具有相同的值。在下一章中,我们将研究交换链,以及它们如何使我们能够将图像呈现在表面。