Vulkan 教程 Khronos Vulkan® Tutorial · 中文翻译 GitHub

Vulkan 是平台无关的 API,因此它本身并不设计与窗口系统直接对接。要在 Vulkan 与窗口系统之间建立连接,把结果显示到屏幕上,就需要使用 WSI (Window System Integration,窗口系统集成)扩展。本章讨论第一个, 即 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 上需要 HWND 和 HMODULE 句柄。因此,这个扩展有一个平台相关的补充部分,在 Windows 上叫做 VK_KHR_win32_surface,它同样会被自动包含在 glfwGetRequiredInstanceExtensions 返回的列表里。

下面演示如何在 Windows 上用这个平台相关扩展创建表面,但本教程不会 用到它。毕竟先用 GLFW 这样的库、再回头写平台相关代码毫无意义。GLFW 实际上提供了 glfwCreateWindowSurface,替我们处理平台差异。不过,在 开始依赖它之前,先看看它背后做了什么还是有好处的。

要访问原生平台函数,需要更新文件顶部的 include:

#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 结构体。它有两个重要参数:hwnd 和 hinstance,分别是窗口和进程的句柄。

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

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

之后就可以用 vk::raii::Instance::createWin32SurfaceKHR 创建表面,它的 参数包含表面创建细节和自定义分配器。严格来说,这是个 WSI 扩展函数, 但它太常用了,标准 Vulkan 加载器已经包含它,所以与其他扩展不同,你 不需要显式加载。

surface = instance.createWin32SurfaceKHR(createInfo);

其他平台的过程类似。比如在 Linux 上,vk::raii::Instance::createXcbSurfaceKHR 接收 XCB 连接和窗口作为创建细节;如果你运行在 Wayland 下,就用 vk::raii::Instance::createWaylandSurfaceKHR,它改为接收 wl_display 和 wl_surface。

glfwCreateWindowSurface 函数执行的正是这一操作,只是每个平台的实现 不同。现在把它集成到我们的程序里。添加 createSurface 函数,在 initVulkan 中于实例创建和 setupDebugMessenger 之后调用。

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
		    };

    // 创建设备
    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);
}

在 下一章里,我们会研究交换链,以及它如何让我们 把图像呈现到表面。