Vulkan Tutorial 教程翻译(一) 概述

293 阅读11分钟

概述

本章开始会总体介绍 Vulkan 以及其所要解决的问题,之后我们会看一下绘制一个基础的三角形所需必须需要的准备, 这将会给你对之后子章节有一个总体的概览,最后我们会给出Vulkan API的总体结构 以及它的使用方式

Vulkan的起源

与之前已经存在的图形API一样,Vulkan被设计为一个跨平台的对现有GPU硬件的抽象,大多数图形API的问题是,他们设计时受时代所限,仅有可固定配置的图形功能.程序员们必须提供标准格式的顶点数据,光照与着色的效果完全被GPU制造商提供的设备所限制.

随着图形架构的成熟,开始提供越来越多的可编程功能.所有的这些功能都必须被集成到已经存在的API中.这就导致很难对应用开发者的实际意图做出完美的抽象.这就是为什么驱动层的更新会给游戏性能带来那么大的提升.由于这些驱动的复杂性,应用开发者必须处理不同厂商之间的差异,比如shader的语法差异.

除了这些新的特性,过去的十年还涌现出大量图形性能强劲的移动设备.由于移动端对空间和续航有特别的要求,这些移动端的GPU采用了不同的架构,比如 tile rendering 由于程序员可以更好的控制此功能,所以可以提供更高的性能.

老API的另外一个限制是对于多线程的支持.这会导致在CPU侧的性能瓶颈.

Vulkan通过更现代的图形架构来解决这些问题.通过让程序员使用更加复杂的API指出自己的意图来减少图形驱动层的开销.也允许多个线程并行地提交命令给GPU,还通过一个标准地二进制格式来减少在shader之间的不一致,最后,它还整合了GPU的通用计算能力。

如何绘制一个三角形

现在我们概述一下用一个完善的Vulkan API绘制一个三角形的所有步骤.这里介绍的所有概念,都会在之后的章节详细说明.这仅仅是帮助你串联起所有不同的组件.

1.实例和物理设备的选择

Vulkan应用程序首先通过VkInstance来设置Vulkan API. 通过描述你的应用以及所需要的API扩展,来创建一个VkInstance,在创建完实例之后,就可以查询当前所支持的硬件了,选择一个或多个物理设备去使用,可以查询到显存的大小,物理设备所支持的能力等.比如说,可以优先选择独立显卡.

2.逻辑设备和队列簇

在选择完你所需要的物理设备后,需要创建一个逻辑设备VkDevice,在这一步,你可以指定更多的显卡特性,如多窗口、64位浮点数. 还需要指定队列簇,大部分的操作 如绘制,内存读写都是通过提交给队列簇的队列来异步完成的. 队列通过队列簇来分配,每个队列簇里的队列都支持一组指定的操作.例如,我们可以将 渲染、计算、传输放入不同的队列中.队列簇也是在选择物理设备时的一个重要参考指标.一个支持Vulkan的设备可能并没用提供任何的图形功能.然而今天所有支持vulkan的图形显卡都会支持我们需要的队列操作.

3.窗口表面和交换链

除非你只关心图形渲染,否则你就需要创建一个窗口去呈现图像,Windows上可以通过原生的窗口API 或者 GLFW/SDL 三方库去创建,这个教程中,我们使用GLFW.

我们需要两个组件来完成实际的渲染:窗口表面(VkSurfaceKHR) 、交换链(Swap Chain),注意KHR后缀,这表示它是一个Vulkan的扩展.

Vulkan API本身是完全平台无关的,这就是为什么我们需要使用窗口集成扩展来管理和交互,表面是一个跨平台的窗口抽象,其目的是为了渲染,Suface通常会用窗口的句柄来创建,如Windows上HWND,幸运的是GLFW库已经提供了内建的函数来帮我们处理操作系间的差异.

交换链是一组渲染目标的集合,它的基本目的是为了确保我们正在渲染的图像与当前正在显示的图像是不同的对象,这对于确保显示完整图像很重要.每当我们想要绘制一帧时,都需要向swapchain请求一张图像,我们将渲染内容输出到这张图像上,当我们渲染结束时,会把图像返还给交换链,交换链会在合适的时候在屏幕上显示出这副图像.渲染目标的数量以及结束选然后的呈现时机,由你的呈现模式决定.通常呈现模式是双缓冲或者三缓冲架构,我们会在交换链创建这一章深入讨论这些。

一些平台可以通过扩展 VK_KHR_display 和 VK_KHR_display_swapchain 来直接将内容渲染到屏幕上.这允许你创建一个表面以铺满整个屏幕,这将让你实现自己的窗口管理.

4.图像视图和帧缓冲

为了向从交换链中请求的图像绘制内容,我们需要将图像Image包装成ImageView 和 FrameBuffer, ImageView引用Image的特殊部分,FrameBuffer引用ImageView的颜色、深度、模板部分.由于交换链中有许多不同的图像,所以我们需要预先创建好ImageView 和 FrameBuffer,在绘制的时候选择一组用于绘制。

5.渲染过程

渲染过程描述了在渲染期间所使用的图像类型.它们是如何被使用的,他们所产出的内容将如何被处理。在这个三角形绘制程序中,我们告诉Vulkan,会使用单个图像的颜色缓冲区为渲染目标,在绘制之前要用一个颜色清除掉颜色缓冲区,然而,渲染过程只会描述图像的种类,VkFrameBuffer才会实际关联上指定的图像。

6.图形管线

图形管线通过VkPipeline对象创建和设置,它描述了显卡的配置状态,例如视口的大小,深度测试的操作,以及可编程状态设置.VkShaderModule对象从二进制格式的代码文件中创建,驱动也需要知道我们在渲染过程中指定了何种渲染目标.

Vulkan与现有的图形API相比一个最大的区别就是,图形管线的所有配置都需要提前设置.这就意味着如果你向切换不同的shader,或者微调一下你的顶点数据布局,需要重新创建整个渲染管线。同时你也需要提前创建许多不同的渲染管线对象.仅有一些基础的配置,如视口大小的修改,清除颜色缓冲区是可以被动态地设置的。所有的状态都需要被显示的描述出来,例如,并没有默认的颜色混合方式。

好消息是,你做的这些操作是等价于AOT而不是JIT的,这就给驱动提供了更多的优化机会。因为大的图形管线的切换都是显示执行的,运行的效率也变得更加的可以预测。

7.指令池和指令缓存

正如之前所提到的,在Vulkan中,想要执行操作,我们需要通过提交队列的方式,如绘制操作,就需要提交一个到队列中。这些操作首先需要被记录到一个VkCommandBuffer对象中,这些命令通过一个关联到指定队列簇的命令池来分配得到,为了绘制一个三角形,我们需要记录如下指令:

  • 开始一个渲染过程
  • 绑定一个渲染管线
  • 绘制三个顶点
  • 结束渲染过程

由于在FrameBuffer里面的图像依赖于交换链,所以我们需要为每一个图像记录命令缓冲并在绘制的时候选择一个正确的。

另外一种方式是在每一帧绘制的时候记录命令缓冲,但是这样效率不高

8.主循环

现在,绘制指令已经被包进了命令缓冲里了,主循环就相当简单直接了,首先通过vkAcquireNextImageKHR 拿到一个交换链上的图像,我们可以选择合适的命令缓冲,然后在其上提交指令,最后将渲染好的图像返还给交换链,以便于后面展示在屏幕上。

那些被提交到队列的操作会异步执行,因此我们必须使用诸如信号量这样的同步对象来保证正确的执行顺序,要运行绘制指令必须等图像的请求结束,否则就会出现把渲染的内容输出到正在屏幕上显示的图像上,因此有关呈现的操作就需要等待渲染的结束,对此我们需要使用第二个信号量,此信号量用于保证渲染已经完全结束了。

9.总结

通过上面的描述,我们对如何绘制一个三角形有了一个基础的理解。真实的程序还包含更多的步骤,例如分配顶点缓冲,创建统一缓冲区,上传图像资源,这些都会在后面的章节讲述。考虑到Vulkan学习曲线陡峭,我们会从简单的开始。我们会把顶点坐标数据直接放在着色器代码里,因为管理顶点缓冲区也需要与指令队列类似的对象。

总结起来,绘制三角形我们需要做的事情:

  • 创建实例
  • 选择物理显卡
  • 创建逻辑设备以及用于绘制和展示的队列
  • 创建窗口 窗口表面 和交换链
  • 将交换链提供的图像包裹为图像视图
  • 创建渲染过程并指定渲染目标和使用场景
  • 为渲染过程创建帧缓冲区
  • 装配图形管线
  • 为每个关联交换链的图像分配命令缓冲并记录操作指令
  • 请求图像进行每一帧的绘制,将绘制指令提交到正确的命令缓冲区,最后将图像返还给交换链

这有许多步骤,但每一步都会比较清晰简单,如果你对单个步骤和整体的关系感到困惑,可以回来复习一下。

API相关概念

这一节简要概述Vulkan API的大致结构和组成

代码约定

所有Vulkan的函数,结构体,枚举类型都定义在vulkan.h头文件中,它包含在Vulkan的SDK中,后面一章会介绍如何安装。

函数由一个小写的vk作为前缀

类型如枚举或结构体由Vk为前缀

枚举值有VK_为前缀

API使用struct结构体用于给函数提供参数.对象的创建一般都是以下模式:

VkXXXCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
createInfo.pNext = nullptr;
createInfo.foo = ...;
createInfo.bar = ...;

VkXXX object;
if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) {
    std::cerr << "failed to create object" << std::endl;
    return false;
}

许多Vulkan中的结构体,需要通过sType参数显示的指定类型,pNext用于指定扩展的数据类型,此教程设置为nullptr,那些创建和销毁的函数都提供了一个VkAllocationCallbacks参数,可以用它自定义驱动内存的管理,教程里也都设置为nullptr.几乎所有的函数都返回一个VkResult对象 ,可能是 VK_SUCCESS 或错误码。

验证层

前面说过,Vulkan被设计为高性能,低驱动开销,因此它默认的错误检查和debug能力都是严重受限的。如果你做了一些错误的操作,驱动通常会崩溃而不是返回错误码,甚至还有更糟糕的情况,在你的显卡上是好的,在其他的设备上是失败的。

Vulkan允许你通过验证层的引入去扩展这些检测,验证层是一些代码插入在驱动层和API之间的代码片段,用于一些额外的函数参数检查,以及追踪内存管理问题.漂亮的一点是你可以在debug的时候开启它,在发布的时候再禁用,以达到驱动零负载的效果。任何人都可以写自己的验证层,SDK提供了一套标准的验证层工具集,我们会在这个教程中使用。你也可以注册一个回调函数,从验证层接收调试的信息。

因为Vulkan每个操作几乎都是显示的,验证层也很方便做扩展,所以他相比于OpenGL或者DirectX更容易发现问题。

在开始开发之前,我们先来配置开发环境.