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

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

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

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

创建窗口表面

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

VkSurfaceKHR surface;

虽然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函数来帮我们处理平台的差异性。我们用它之前了解一下它在幕后做了什么还是很好的。

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

#define VK_USE_PLATFORM_WIN32_KHR
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>

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

VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);

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

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

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

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

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

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

void createSurface() {

}

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

void createSurface() {
    if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
        throw std::runtime_error("failed to create window surface!");
    }
}

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

void cleanup() {
    ...
    vkDestroySurfaceKHR(instance, surface, nullptr);
    vkDestroyInstance(instance, nullptr);
    ...
}

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

查询呈现支持

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

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

struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;
    std::optional<uint32_t> presentFamily;

    bool isComplete() {
        return graphicsFamily.has_value() && presentFamily.has_value();
    }
};

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

VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);

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

if (presentSupport) {
    indices.presentFamily = i;
}

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

创建呈现队列

最后一件事就是修改逻辑设备的创建过程,创建呈现队列,并获取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指向数组:

createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();

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

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

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

目录
上一节 - 绘制一个三角形/设置/逻辑设备和队列(Logical device and queues)
下一节 - 绘制一个三角形/呈现/交换链(Swip chain)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值