目录
- Vulkan及其演化史
- Vulkan 基本概念
- 基本术语
- Vulkan 的原理
- Vulkan应用程序
- Vulkan的编程模型
- 硬件初始化
- 窗口展示表面
- 资源设置
- 流水线设置
- 描述符和描述符缓冲池
- 基于SPIR-V的着色器
- 流水线管理
- 指令的记录
- 队列的提交
Vulkan及其演化史
目前主流的图形渲染API有OpenGL
、OpenGL ES
、DirectX
、Metal
等
OpenGL
的应用领域较为广泛,支持多种操作系统平台(如Windows、UNIX、Linux、macOS等)基于其开发的应用可以方便、低成本地在不同操作系统平台之间移植。既可以用于开发游戏,又可以用于开发工业、行业应用
OpenGL-ES
则是OpenGL针对移动端的裁剪版本。Direct-X
是微软针对Win系统下图形渲染的技术,Metal
则是针对Mac/iOS系统下图形渲染技术,从占有率而言DirectX
是远远超过Metal
的。
那么OpenGL
在和Vulkan
相比,Vulkan
能够更好的调动GPU的性能,OpenGL在使用GPU前需要CPU处理很多数据,而Vulkan能够提供更小的运行开销、更直接的GPU控制、和更低的CPU负载。 Vulkan的原始概念是由AMD基于他们的私有的Mantle API设计和实现的,这个API几款不同的API中体现了自己的先进特性
Vulkan 基本概念
基本术语
-
物理设备(
physical device
)和设备(device
)
指的是物理设备在应用程序的中逻辑表示,一个计算机系统中可能包含多个物理设备 -
队列(
queue
)
队列是执行引擎(GPU)和物理设备之间的接口,一个物理设备总是包含一个或者多个队列(图形,计算,DMA/传输等),队列的职责是收集指令缓存并且分发到物理设备执行 -
内存类型(
memory type
)
广义上来讲有两种内存类型,设备内存和宿主内存,在后面会具体讨论 -
指令(
command
)
指令包含下面类型的指令:
-
动作指令(
action command
)
包括绘制图元,清除表面,复制缓存,查询/时间戳操作,子通道的开始结束操作。这些指令可以修改帧缓存附件,读取或者写入内存,以及写入查询池 -
状态设置指令(
set state command
)
这些指令可以用于绑定流水线,描述字集合以及缓存,或者设置一个动作状态,以及渲染通道,子通道的状态等等 -
同步指令(
synchronization command
)
用于处理两个或者更多的动作指令同时发生的情况,此时的指令之间可能会产生争夺资源或者依赖于某些内存,该指令用于设置同步事件或者等待事件,插入流水线屏障对象,渲染通道子通道的依赖 -
指令缓存(
command buffer
)
指令缓存是一组指令的集合,它可以记录多个指令并统一发送到队列中
Vulkan 的原理
支持Vulkan
的系统可以直接查询系统信息,并返回可用的物理设备的数量。每个物理设备可以支持一个或者多个队列,这些队列被划分到不同的族群之中
每个族群都有自己的独特的功能设定,比如一个族群可能会支持图形,计算,数据传输,或者内存管理相关的内容。
队列族群每个成员可能包含一个或者多个相似的队列,因此它们之间相互是兼容的,比如: 在某个具体的驱动实现中,可能在同一个队列里同时支持数据传输和图形操作.
Vulkan
允许用户显示在应用程序中管理和控制内存,它暴露了设备中所有支持的不同类型的内存堆(heap
),每个堆属于一个不同的内存区域。
Vulkan
的执行模型是非常简单和直接的,在这里指令缓存会被发送到队列中,后者将被物理设备按照顺序执行和消耗,Vulkan
应用程序负责控制一组Vulkan
设备,将一系列指令记录到指令缓存中,并发送到队列。驱动会读取队列并按照记录的顺序依次执行各个工作。
此外,有些指令缓存在应用程序中可以以多线程的方式并行同步的构建
下面是简化后的Vulkan
执行模型:
这里应用程序记录了两个指令缓存,其中包含了多个指令,这些指令按照作业性质的不同,被传递给一个或者多个队列。
队列将这些缓存作业提交给设备加以执行,最后,设备处理得到结果,并将结果显示到输出设备,或者返回给设备做进一步的处理。
Vulkan
中,应用程序主要负责下面的工作:
- 生产指令执行所必须得先决内容
包含资源的准备,着色器的预编译,将资源关联到着色器,设置着色器状态,构建流水线以及绘制调用等 - 内存管理
- 同步(宿主和设备之间,设备上的不同对列之间)
- 风险管理
Vulkan应用程序
下图给出了 Vulkan 中的不同组件,以及不同组件在系统重的内部关系:
WSI
: 窗口系统集成库是由khronos
提供的一套功能扩展,可以将不同操作系统的展示层结合起来
SPIR-V
:SPIR-V
提供了一套预编译的二进制数据格式,用来设置给Vulkan
的着色器,不同的着色器源代码语言,比如 GLSL
和HSLS
的各种变种,都可以通过预编译产生SPIR-V
格式的数据
LunarG SDK
:LunarG 提供了一套Vulkan
的SDK,其中带有很多不同的资源和工具,可以辅助Vulkan程序的开发,这些工具和资源包括Vulkan的加载器,验证层,跟踪和回放工具。
Vulkan的编程模型
下图表示了Vulkan
应用程序的编程模型自顶向下的实现过程:
硬件初始化
加载器:加载器是一段应用程序启动时候执行的代码,可以用平台无关的方法定位Vulkan
驱动
注入层:加载器允许在允许过程中随时注入不同类型的层,这样做的巨大好处是,驱动不需要做任何验证,
可注入的层实现的功能包括:
- 跟踪
Vulkan
API指令的执行 - 捕获渲染的场景,稍后在继续执行
- 为了满足调试的需要,进行错误处理和验证
窗口展示表面
窗口展示层用于和当前系统的窗口系统进行链接
创建展示图像和创建窗口的工作和平台很相关,OpenGL
中是通过底层平台进行链接的,窗口系统负责创建设备/环境以及对应的帧缓存
和OpenGL
不同的是,Vulkan
在创建设备和环境时完全不需要包含一套窗口系统,这是通过WSI(Window System Intergration)
完成的
WSI
支持多个窗口系统,比如Waylad
, X
,windows
,它还支持通过交换链的方式实现窗口系统所有权的管理,也就是Double Buffer
功能
WSI实现交换链需要下面的步骤实现:
- 创建一个本地窗口
- 创建
WSI
表面并且关联到窗口上 - 创建交换链来显示表面
- 从创建后的交换链中获取绘制的图像
资源设置
设置资源的含义是讲数据存储到内存区域中,数据可以是下面的类型:顶点属性比如位置,颜色或者图像类型/名称,数据是保存在内存中的,以便于Vulkan
访问
OpenGL
是通过隐式的方式管理场景背后的内存数据,不同的是Vulkan
提供了一套底层接口来控制和管理内存,同时在物理设备上提供了不同类型的内存数据
Vulkan
中的资源是应用程序显式进行管理的:
- 资源对象(
Resource Object
): 设置资源的时候,应用程序需要负责分配资源所用的内存,资源可以是图像也可以是缓存对象 - 分配(
Allocation
)和子分配(SubAllocation
)
创建了资源对象以后,我们只是关联了一个逻辑地址,并没有真正的物理地址可用,分配的过程就是分配物理地址并且将逻辑地址绑定到内存,完全的分配是很耗时的,
子分配是一种高效的内存管理方式,它可以将物理内存的很大一部分立即分配完成并且存入不同的资源对象
子分配是由应用程序负责完成的,下图给出了物理内存中实现对象子分配的流程:
在资源设置阶段,应用程序需要完成的工作是:
- 创建一个资源对象
- 查询应用程序内存实例,创建一个内存对象,比如缓存或者图像
- 获取对象分配相应的内存需求
- 分配空间并且保存数据到其中
- 将内存绑定到我们创建的资源对象上
流水线设置
流水线是指根据应用程序逻辑定义的一系列事件,他们按照固定的顺序执行。事件主要包含下面几种:设置着色器,绑定到资源,以及状态的管理
描述符和描述符缓冲池
描述符集合指的是资源和着色器之间的接口,可以将着色器绑定到资源,比如图像或者缓存,可以将资源内存关联或者绑定到准备使用的着色器的实例上
基于SPIR-V的着色器
SPIR-V着色器的特点包括:多重输入,离线编译,glslang 验证器和多重程序入口
流水线管理
物理设备需要定义发送的集合输入数据是如何进行解释和绘制的,这些状态的设置被统称为流水线状态
流水线状态包括光栅化状态,深度、模板状态此外还包括了输入几何数据的图元拓扑类型和渲染所用的着色器
指令的记录
指令的记录是逐渐构成指令缓存的过程。指令缓存是从指令缓存池中分配而来的。指令池可以用来同时分配多个指令缓存
应用程序定义了指令的开始和结束位置以后,就可以将指令记录到指令缓存之中
图中描述的渲染过程如下:
- 范围(scope): 记录了指令缓存记录的起始和截止位置
- 渲染通道(render pass):
- 流水线(pipeline): 包含了流水线对象用到的各种静态和动态的信息
- 描述符(decriptor): 负责将资源信息绑定到流水线
- 绑定资源(bind resource): 负责设置顶点缓存、图像几何相关的信息
- 视口(view port): 绘制表面上可供执行图元渲染的部分矩形
- 裁切器(scissor): 定义了一个矩形空间区域,舍弃这个区域之外的所有绘制信息
- 绘制(drawing): 绘制指令将设置几何体的缓存属性,例如开始索引、总计数值等
队列的提交
Vulkan
向应用程序暴露了不同类型的队列接口,比如:图形,DMA传输 或者计算队列
对列的执行需要下面的操作:
- 从交换链中获取当前的图像,决定下一帧绘制所用的表面
- 执行各种同步的机制,比如信号量,Fence等
- 收集指令缓存,并且发布到对应的设备队列中,准备处理
- 请求将输出设备中已经渲染完毕的图像显示出来