Vulkan 演化历史、对比OpenGL、编程模型
1.介绍
1.1 Vulkan及其演化史
著名的OpenGL API问世已经差不多四分之一个世纪,而且它还在 不断发展。本质上来说,OpenGL是一个纯粹的状态机,其中包含了若 干个开关量,可以设置为开/关的状态(on/off)。这些状态数据被用来构建设备中的依赖映射关系,对资源进行管理,并通过最优的方法进行控制以达到性能的最大化。
这种状态机可以隐式地自动化资源管理,但是它对应用程序逻辑 的解读不够智能化,而应用程序正是资源管理背后的驱动力所在。其产生的结果可能是用户无法预测的,比如实现中断,导致着色器代码 被重新编译,但是应用程序并不需要系统这么做。此外,OpenGL API 也会受到其他因素的限制,例如不可预测的程序行为、多线程的扩展 性、渲染的故障等。后续会将OpenGL与Vulkan API进行比较来理 解两者之间的各种差异。 Khronos于2016发布了革命性的新架构Vulkan API,它充分利用了 现代图形处理器单元的优势,来实现高性能图形和计算应用程序的开 发。
Khronos是一个会员制的社区和专注于发布开放标准和免费API的 组织。更多信息请参阅网站:https://www.khronos.org。
Vulkan的原始概念是由AMD基于它们的私有Mantle API设计和实现 的。这个API已经在几款不同的游戏中体现了自己的先进特性,它有着 革命性的实现方案,完全满足了工业界的竞争性需求。AMD开源了自己 的Mantle API并且贡献给Khronos组织。在多家硬件和软件供应商的协 同帮助下,Khronos发布了Vulkan标准。
Vulkan并不是目前唯一的新一代3D图形API,它还有多家不同的竞 争者,例如Microsoft的Direct-X 12和Apple的Metal。不过,DirectX只能用于不同的Windows系统,而Metal只能用在Mac系统(OS X和 iOS)。因此,Vulkan得以脱颖而出。它的跨平台特性可以支持所有现 存的OS平台,其中已经包括了Windows(XP、Vista、7、8、10)、 Linux、Tizen、SteamOS和Android。
1.2 Vulkan与OpenGL的对比
Vulkan相比OpenGL有了不少的新特性和性能提升,如下所示。
-
降低驱动负载和CPU的使用量:Vulkan在设计上更为接近于底层 的图形硬件。因此,它可以向应用程序的开发者提供更为直接的控制权力,来操作宿主机上的计算资源,使用GPU来尽可能高效地完成渲染工作。这一特性使得相关软件可以直接访问图形处理器,从而达到 更好的性能要求。
-
多线程的可扩展性:OpenGL中的多线程扩展能力是非常有限的,所以很难利用多线程的各种优势来管理CPU资源。不过,Vulkan在设计时特别考虑了终端用户对于多线程功能的迫切需求,并且通过非常透明的方式予以支持(并不会隐含任何的全局状态变化)。不同线 程下的任务、任务的创建过程,以及任务提交执行的过程之间都是完 全独立的,不存在数据耦合。
-
显式的API定义:OpenGL的API是隐式的,资源管理的工作交给 驱动层去完成。驱动层负责读取应用程序端的提示参数并跟踪资源的 处理,这样带来了很多不必要的负担。
-
Vulkan采用了显式的API定义,驱动并不负责资源以及资源之间相互关系的管理。这些工作由应用程序处理。这种清晰的实现方式更容易预测,驱动层也不需要在用户场景的后面偷偷做资源管理的小动 作了(这正是OpenGL的弊端)。这样的结果是,用户任务的处理可以 直截了当地以流水线的方式完成,从而获得最佳性能和可预测的行为 模式。
-
预编译的中间级着色语言:OpenGL需要使用OpenGL着色语言 (GLSL)源代码的形式来实现着色器,而Vulkan使用可移植的标准化 中间级语言(SPIR-V)作为中间级语言标准,为并行计算和图形处理 提供了着色器支持,其他源代码语言的编译器(例如GLSL、HLSL或者LLVM)必 须将SPIR-V作为输出的目标语言,并且提供工具来实现SPIR-V输入数 据的支持。Vulkan可以读取这种能够马上执行的二进制中间数据,并且 在着色器执行阶段直接使用。
-
驱动层和应用程序层:OpenGL中的应用程序层相比驱动层而言 要单薄得多,因为驱动层会自动完成资源管理和状态跟踪的工作。 Vulkan与之相反。它的驱动层更为接近硬件底层,负载较小。而应用程 序层需要负责逻辑、资源和状态的管理。图1-2给出了这两个API各自的 驱动层和应用程序层代码的总量对比。
- 内存的控制:Vulkan暴露了系统当中的多种不同类型的内存接口,交由应用开发者去选择适合自己的内存类型,实现各种资源的管理和使用。与之相反,OpenGL是通过驱动层的内部处理机制来完成资源存储的,不同的供应商可能因此有完全不同的实现,并且当驱动层改变了资源的存储位置时。很可能产生预期之外的内存碎片,或者降低存储效率。
- 可预测行为:Vulkan与OpenGL相比,其行为具有很高的可预测性,它不会在渲染时产生任何延迟或者抖动。用户任务传递到驱动层之后会被立即提交,而OpenGL的任务提交过程不是立即完成的,它需 要等待驱动层再去进行调度。
- 单一的API:OpenGL有多个独立的版本,包括桌面端的 API(OpenGL)和嵌入式系统的API(OpenGL ES)。Vulkan相比之下 更为清晰,它只提供了单一的API接口来面对所有类型的系统平台。 Vulkan会优先支持移动平台,这一点与OpenGL也是不一样的。 OpenGL通常会优先实现某个功能的桌面端版本,然后再将它更新到 OpenGL ES API当中。
- 直接访问GPU:Vulkan暴露了自己的底层功能和硬件特性,从而给应用层用户提供了大量的控制手段。它提供了多种不同类型的物理设备、内存类型、指令缓存队列,以及功能扩展。这样的模式确保软 件层更接近实际硬件的特性。
1.3 重要术语
在开始深入学习基础知识之前,我们首先了解一些Vulkan中的重要术语。
- 物理设备(physical device)与设备(device):一台计算机系统中可能包含了不止一个支持Vulkan的物理硬件设备。我们所说的物理设备表示一个独立的设备,而设备指的是该物理设备在应用程序中的逻辑表示。
- 队列(queue):队列表示执行引擎与应用程序之间的接口。一个物理设备总是包含了一个或者多个队列(图形、计算、DMA/传输,等等)。队列的职责是收集准备执行的工作(指令缓存)并且分发到物理设备执行。
- 内存类型(memory type):Vulkan暴露了多种内存类型。广义上来说,总共有两种类型的内存:宿主内存和设备内存。之后会讨论这些内容。
- 指令(command):每个指令中都可以执行一些用户行为。指令从广义上可以划分为动作、状态设置,以及同步。
- 动作指令(action command):包括绘制图元、清除表面、复制缓存、查询/时间戳操作,以及子通道的开始/结束操作。这些指令可以修改帧缓存附件、读取或者写入内存(缓存或者图像),以及写入查询池。
- 状态设置指令(set state command):这些指令可以用来绑定流水线、描述字集合以及缓存,或者设置一个动态状态,以及渲染通道/ 子通道的状态。
- 同步指令(synchronization command):同步指令用于处理两个或者更多动作指令同时发生的情况,此时指令之间可能会争夺资源或者依赖于某些内存。该指令用来设置同步事件或者等待事件、插入流水线屏障对象,以及渲染通道/子通道的依赖。
- 指令缓存(command buffer):指令缓存是一组指令的集合,它可以记录多个指令并统一发送到队列中。
下面我们将从总体上了解Vulkan的工作模型和一些基础概念。我们还会尝试理解指令的语法规则,并通过直接阅读API列表的方式简单了解所有的API指令。
1.4.4 对象生命周期与指令语法
Vulkan当中的对象是根据应用程序的逻辑需求显式地创建和销毁 的,应用程序需要自己管理这些对象。
Vulkan的对象需要使用Create指令创建,以及使用Destroy指令销 毁:
- Create语法:对象的创建需要通过vkCreate*指令完成,它需要一 个Vk*Createinfo结构体作为输入参数。
(结构体:C语言的概念,允许用户自己建立不同类型数据组成的组合型数据结构,它称为结构体)
- Destroy语法:使用Create指令创建的对象总是需要使用 vkDestroy*指令销毁。
如果对象是作为已有的对象池或者堆的一部分创建的,那么需要 使用Allocate指令创建,并使用Free指令从池或者堆中销毁。
(对象池顾名思义,就是存放一堆对象的池)
- Allocate语法:一个对象如果是作为对象池的一部分创建,那么需要使用vkAllocate*指令,并且需要一个Vk*AllocateInfo作为输入参数。
- Free语法:已创建的对象需要使用vkFree*指令从对象池或者内存中释放。
上述所有的实现方法都可以通过vkGet*指令轻松获取。而使用了 vkCmd*形式的API接口主要用于把指令记录到指令缓存当中。
1.5 理解Vulkan应用程序
这是我自己用visio总结的一个流程图
1.6流水线设置
1.描述符集以及描述符缓冲池
2.基于SPIR-V的着色器
3.流水线的管理
4.指令的记录
5.队列的提交