狸村 Mystic Web · 狸村
目录

由于 Vulkan 是一个平台无关的 API,它本身并不设计为直接与窗口系统交互。为了在 Vulkan 和窗口系统之间建立连接,以便将结果显示到屏幕上,我们需要使用 WSI(窗口系统集成)扩展。在本章中,我们将讨论第一个扩展,即 VK_KHR_surface。它公开了一个 VkSurfaceKHR 对象,该对象表示一种抽象类型的表面,用于呈现渲染后的图像。我们程序中的表面将由我们已经用 GLFW 打开的窗口来支持。

VK_KHR_surface 扩展是一个实例级别的扩展,实际上我们已经启用了它,因为它包含在 glfwGetRequiredInstanceExtensions 返回的列表中。该列表还包含一些其他 WSI 扩展,我们将在接下来的几章中使用。

窗口表面需要在实例创建之后立即创建,因为它实际上会影响物理设备的选择。我们推迟这一步的原因是窗口表面是渲染目标和呈现这一更大主题的一部分,提前解释会使基础设置变得混乱。还应该注意的是,如果你只需要离屏渲染,窗口表面在 Vulkan 中是一个完全可选的组件。Vulkan 允许你在没有像创建不可见窗口(OpenGL 所必需的)这类技巧的情况下做到这一点。Vulkan 还允许你从非呈现 GPU 进行远程渲染,或通过互联网远程渲染,或者在没有渲染或呈现目标的情况下运行 AI 计算加速。

虽然这里将演示 GLFW,但 Vulkan 渲染到表面的概念是任何其他窗口 API 的通用目标。这个概念适用于移动平台,也适用于直接与窗口管理器交互的工作。话虽如此,现在让我们专注于 GLFW,看看它在本教程中具体是如何工作的。

窗口表面创建

首先在调试回调下方添加一个 surface 类成员。

vk::raii::SurfaceKHR surface = nullptr;

尽管 VkSurfaceKHR 对象及其用法是平台无关的,但它的创建并非如此,因为它取决于窗口系统的细节。例如,在 Windows 上它需要 HWNDHMODULE 句柄。因此,该扩展有一个平台特定的附加部分,在 Windows 上称为 VK_KHR_win32_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>

因为窗口表面是一个 Vulkan 对象,它带有一个需要填充的 vk::Win32SurfaceCreateInfoKHR 结构体。它有两个重要参数:hwndhinstance。它们分别是窗口和进程的句柄。

vk::Win32SurfaceCreateInfoKHR createInfo{.hinstance = GetModuleHandle(nullptr),
                                         .hwnd      = glfwGetWin32Window(window)};

glfwGetWin32Window 函数用于从 GLFW 窗口对象获取原始的 HWNDGetModuleHandle 调用返回当前进程的 HINSTANCE 句柄。

之后,可以使用 vk::raii::Instance::createWin32SurfaceKHR 创建表面,它包含一个用于表面创建细节和自定义分配器的参数。从技术上讲,这是一个 WSI 扩展函数,但由于它非常常用,标准的 Vulkan 加载器包含了它,因此与其他扩展不同,你不需要显式加载它。

surface = instance.createWin32SurfaceKHR(createInfo);

其他平台(如 Linux)的过程类似,其中 vk::raii::Instance::createXcbSurfaceKHR 使用 XCB 连接和窗口作为 X11 的创建细节。

glfwCreateWindowSurface 函数正是执行此操作,但为每个平台提供了不同的实现。我们现在将其集成到我们的程序中。添加一个 createSurface 函数,在实例创建和 setupDebugMessenger 之后立即从 initVulkan 中调用。

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
}

void createSurface() {

}

GLFW 调用使用简单的参数而不是结构体,这使得函数的实现非常直接:

void createSurface() {
    VkSurfaceKHR       _surface;
    if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) {
        throw std::runtime_error("failed to create window surface!");
    }
    surface = vk::raii::SurfaceKHR(instance, _surface);
}

然而,正如你在上面看到的,GLFW 只处理 Vulkan C API。VkSurfaceKHR 对象是一个 C API 对象。幸运的是,它可以原生地提升为 C++ 包装器,这就是我们在这里所做的。

参数是 VkInstance、GLFW 窗口指针、自定义分配器以及指向 VkSurfaceKHR 变量的指针。它只是传递相关平台调用的 VkResult。GLFW 没有提供销毁表面的特殊函数,但将其包装在我们的 RAII SurfaceKHR 对象中将让 Vulkan RAII 为我们处理销毁。

查询呈现支持

尽管 Vulkan 实现可能支持窗口系统集成,但这并不意味着系统中的每个设备都支持它。因此,我们需要扩展 createLogicalDevice 以确保设备可以将图像呈现到我们创建的表面。由于呈现是队列特定的功能,问题实际上在于找到一个支持向我们创建的表面进行呈现的队列族。

实际上,支持图形命令的队列族和支持呈现的队列族有可能但不一定重叠。但为简单起见,我们假设存在这样一个队列族,并在不存在时退出。

也就是说,接下来,我们将寻找一个同时具备支持图形操作和呈现到我们窗口表面能力的队列族。检查呈现支持的函数是 vk::raii::PhysicalDevice::getSurfaceSupportKHR,它以队列族索引和表面作为参数:

uint32_t queueIndex = ~0;
for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++)
{
  if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) &&
      physicalDevice.getSurfaceSupportKHR(qfpIndex, *surface))
  {
    // 找到一个同时支持图形和呈现的队列族
    queueIndex = qfpIndex;
    break;
  }
}
if (queueIndex == ~0)
{
  throw std::runtime_error("Could not find a queue for graphics and present -> terminating");
}

创建呈现队列

剩下的一件事是修改逻辑设备创建过程,以创建队列并检索 vk::raii::Queue 句柄。我们需要修改过滤逻辑,以在我们检测它们时找到要使用的最佳队列族。以下是我们如何在设备创建函数中通过一个函数来完成:

void createLogicalDevice() {
    // 找到第一个支持图形的队列族的索引
    std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice.getQueueFamilyProperties();

    // 获取 queueFamilyProperties 中同时支持图形和呈现的第一个索引
    uint32_t queueIndex = ~0;
    for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++)
    {
      if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) &&
          physicalDevice.getSurfaceSupportKHR(qfpIndex, *surface))
      {
        // 找到一个同时支持图形和呈现的队列族
        queueIndex = qfpIndex;
        break;
      }
    }
    if (queueIndex == ~0)
    {
      throw std::runtime_error("Could not find a queue for graphics and present -> terminating");
    }

    // 查询 Vulkan 1.3 功能
		vk::StructureChain<vk::PhysicalDeviceFeatures2,
		                   vk::PhysicalDeviceVulkan11Features,
		                   vk::PhysicalDeviceVulkan13Features,
		                   vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>
		    featureChain = {
		        {},                                    // vk::PhysicalDeviceFeatures2
		        {.shaderDrawParameters = true},        // vk::PhysicalDeviceVulkan11Features
		        {.dynamicRendering = true},            // vk::PhysicalDeviceVulkan13Features
		        {.extendedDynamicState = true}         // vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT
		    };

    // 创建一个 Device
    float                     queuePriority = 0.5f;
    vk::DeviceQueueCreateInfo deviceQueueCreateInfo{.queueFamilyIndex = queueIndex, .queueCount = 1, .pQueuePriorities = &queuePriority};
    vk::DeviceCreateInfo      deviceCreateInfo{.pNext                   = &featureChain.get<vk::PhysicalDeviceFeatures2>(),
                                               .queueCreateInfoCount    = 1,
                                               .pQueueCreateInfos       = &deviceQueueCreateInfo,
                                               .enabledExtensionCount   = static_cast<uint32_t>(requiredDeviceExtension.size()),
                                               .ppEnabledExtensionNames = requiredDeviceExtension.data()};

    device = vk::raii::Device( physicalDevice, deviceCreateInfo );
    queue  = vk::raii::Queue(device, queueIndex, 0);
}

在下一章中,我们将研究交换链以及它们如何允许我们将图像呈现到表面。

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。