这一节讨论 Vulkan 的资源绑定和状态管理机制,过程中可以加深对 Pipeline、Pipeline layout、Descriptor、Descriptor Set、Descriptor set layout、Descriptor binding 等概念的理解。
1 Pipeline 状态管理
Vulkan中的管线分为两种:Compute Pipeline 和 Graphics Pipeline。这两种Pipeline的作用跟其他CG API并没有太大的差别。
- Compute Pipeline 用于异构并行计算,Vulkan 的 Compute Pipleline 比较简单,API 也比较少,基本上我们只需要关注 compute shader 本身就好。
- Graphics Pipeline 用于绘制渲染,相比于 Compute Pipleline,Vulkan 的 Graphics Pipeline 更加复杂,包含了 Rasterzation(光栅化)、Shading(着色)、Geometry (几何着色)、Tessellation (细分)等可编程管线的重要环节。
回忆一下 OpenGL 的设计,每一次的 API 调用你都需要知道将要设置的 OpenGL 状态,所有的状态都由 OpenGL 的 VM 进行管理,因此在使用 OpenGL 时需要时刻将 VM 的状态熟记于心,而 Vulkan 则完全相反,你需要将所有的渲染状态显式的设置到一个原始的 VkPipeline 对象上,该对象打包了所有可编程 (programable) 渲染状态以及所有的固定功能 (fixed-function) 状态。
VkPipeline 对象的创建在加载阶段,可以在运行游戏时动态创建,也可以直接从文件中读取预编译的对象,预先创建 Pipeline 对象可以减少游戏过程中的卡顿(hitch)现象,下面的代码片段是创建一个 Pipeline 对象所必须设置的状态:
1 | typedef struct VkGraphicsPipelineCreateInfo { |
其中每一个 CreateInfo 又包含一大堆参数,可以说相当繁琐,我们需要设置 GPU 在当前管线下工作时的每个状态。
从早期开始,OpenGL 状态机就提供了更细颗粒度的状态控制,比如:
1 | glEnable(GL_BLEND); |
但精细颗粒度的控制对驱动程序带来了负担,驱动程序不得不对状态进行缓存和运行时编译,但是现在新的 API,几乎把整个 GPU state vector 封装到一个 object 中。比如,如果我们想切换状态,是通过切换 pipeline A 和 pipeline B。而不是像 OpenGL 中那样更改标志位。
这样粗颗粒度的状态对象的优势是方便驱动程序的编译和验证,有助于避免在状态改变时造成的暂停现象。Vulkan 的 Pipeline状态管理参考的是 Mantel,提供了 Pipeline State Object(PSO)进行状态管理:
这些状态包括:
Vertex input state:Vertex input state 用来管理 Vertex 数据的位置、索引、布局等信息
Input assembly state:Input assembly state 管理顶点数据的组装方式(点、线、三角形等)
Tessellation State:Tessellation State 管理 Tessellation control shader 和 Tessellation evaluation shader 的状态
Viewport state:Viewport state 用来管理 Viewport,Viewport 会将设备坐标(device coordinate)转换为窗口坐标(window coordinate),是进行光栅化之前最后一次坐标变换
Rasterization state:Rasterization state 用来管理光栅化的一些状态,包括多边形的填充模式(PolygonMode)、剔除模式(正面剔除、背面剔除)、深度信息的处理等
Color Blend state:Color Blend 是 Vulkan Graphics Pipeline 的最后阶段,这个阶段负责将片段写入 color attachments。在许多情况下,这是一个简单的操作,仅用 Fragment Shader 输出的值覆盖 attachments 中的值。除此之外,color blend 还可以将这些值与 FrameBuffer 中的值混合,并进行简单的逻辑运算。
Depth/Stencil state:Depth/Stencil state 控制着 Depth 和 Stencil 测试的方式
Multi-Sampling State:多重采样的目的是为了抗锯齿,Multi-Sampling State 管理多重采样
Vulkan 还允许动态状态管理,因为 Graphics Pipeline 很复杂,包含很多状态,一些图形应用希望能以更高的频率更改某些状态。如果每种状态更改都要创建一个新的 Pipeline 对象,非常不利于管理。Dynamic state 可以管理 viewport、stencil、line widths(线宽)、blending constants、stencil comparison masks 等状态。应用无需重建 Pipeline 对象,只需要通过 Command Buffer 就可以实现状态更新。
接下来对一些状态进行进一步说明。
1.1 ShaderMoudle
Shader 是 Pipeline 状态中最为核心的一个,Vulkan 完全摒弃了固定渲染管线。Vulkan 中 shader 对象被称为 VkShaderMoudle,在图形渲染管线(VkGraphicPipeline)中必不可少的 Shader 有 Vertex shader 和 Fragment shader,当然在一些特殊应用中也可以有细分 shader 以及几何 shader。
Vulkan 并没有特定的 Shader语法,而是使用了一种编译后的中间格式——SPIRV,通常情况下一般使用第三方库将 GLSL 或 HLSL 离线或实时编译成 SPIRV 格式,例如对于 GLSL 可以使用 LunaG 中提供的工具离线将其编译成 SPIRV 格式,也可以使用第三方库 Glslang 将其实时编译成 SPIRV 格式(但这样会有一定的运行时开销,可以将编译后的 SPIRV 格式保存存文件,下次直接加载)。
1.2 Input Assembly State
该状态一般也缩写为 IA,用来指定图元的拓扑方式(topology),如 Point, Line, TriangleList 等,作用等同于 OpenGL 中 glDrawElements 中的 Mode 参数:
1 | typedef enum VkPrimitiveTopology { |
1.3 Rasterization
Pipeline 对象中需要显式指定光栅化操作中需要的状态,Vulkan 不支持光栅化的可编程操作,但支持光栅化模块的参数配置,如 CullMode,FrontFace 的缠绕(winding)方式,depth 偏移(bias),线宽等:
1 | typedef struct VkPipelineRasterizationStateCreateInfo { |
1.4 Multisampling
Vulkan支持多重采样(Multisampling),由于多重采样在是渲染管线之中,因此也也需要在 Pipeline 对象中指定多重采样状态,一般支持 1、4、8、16 个采样点,这依赖于硬件的支持情况,Vulkan 最多支持 64 个多重采样点。如果采样点数量不为 1 时,attchment 必须支持相应的多重采样,即创建 VkImage 时的 samples 参数必须与之相同。
1.5 Blend
Blend 同样也是 fixed-function 阶段,可以使用不同的参数进行配置,混合只发生于 color attchment,因此需要为不同的颜色 attchement 指定相应的混合参数,典型的混合参数有 VK_BLEND_FACTOR_ZERO
, VK_BLEND_FACTOR_ONE
等,典型的混合操作有如 VK_BLEND_OP_ADD
, VK_BLEND_OP_SUBTRACT
等。
混合参数也可以指定常量 VK_BLEND_FACTOR_CONSTANT_COLOR
,这时会使用创建 Pipeline 对象时指定的 VkPipelineColorBlendStateCreateInfo
中的 blendConstants
参数,也可以使用命令 vkCmdSetBlendConstants
设置的常量。
1.6 RenderPass
RenderPass 是指一组 attachment,Subpass 以及 Subpass 之间的依赖,Pipeline 对象引用了一个 VkRenderPass 对象。对于 RenderPass 而言,最为核心的是 Attachment 上的操作,RenderPass 可以指定 Pass 开始时对 attachment 的操作,如是否需要 Clean,在某种情况下,RenderPass 中参数的合理选择可以显著提高游戏的渲染效率。
1.7 总结
总之,在 Vulkan 中,配置 Graphics Pipeline 需要三个步骤:
- 提供 shader
- 绑定资源
- 管理状态
接下来讨论资源的绑定。
2 Vulkan 资源绑定
Vulkan 的资源绑定涉及到以下几个概念:
- Descriptor
- Descriptor Set
- Descriptor set layout
- Descriptor binding
- Pipeline layout
- Push constant
2.1 Descriptor
Descriptor 是一个 GPU 特定编码格式的数据块。Descriptor 所指的内容不同,就可以表示不同的数据类型。比如指向纹理的 texture descriptor 可能包括指向纹理数据的指针,以及宽度/高度,格式等信息。
Descriptor 的内存不可以分配和释放,只能 write,copy 和 move。
2.2 Descriptor Set
Descriptor Set 是一个绑定在 Pipeline 上资源集合。多个 Descriptor Set 可以同时绑定到一个 Pipeline 上。
每个 Descriptor Set 都有一个 layout,Descriptor set Layout 会控制当前资源集当中的资源排布方式,给 shader 提供了读取资源的接口。
2.3 Descriptor set layout
Descriptor set layout 本质上是 Descriptor bindings 的集合。Descriptor binding 可以是一个 texture descriptor、buffer descriptor 或者 sampler descriptor 等。
正是通过 Descriptor binding,Descriptor Set 才实现了 shader 和资源的绑定,方便了 shader 对资源的读写操作。
2.4 Pipeline layout
Pipeline layout 是由 Pipeline 可以访问的 descriptor set layouts 和 push constant ranges 组成,它表示 Pipeline 可以访问的完整资源集。其中,Push constant 提供了一个快速更新 shader 中常量的方法。
2.5 Binding model
我们把上述元素组织在一起,就是 Vulkan 的资源绑定模型。
Descriptor Set 包含一组 Descriptor 和 Descriptor Set layout:
- 每个 Descriptor 都可以表示不同的数据类型
- Descriptor Set Layout 由 Descriptor binding 组成,会控制每个 Descriptor 的排列方式
- Descriptor binding 将 Descriptor 和 shader 进行绑定,给 shader 提供了对资源操作的接口。
Constants register array(Root Table) 把多个Descriptor Set 和 push constant 组织在一起。
Pipeline layout 表示整个渲染管线的资源布局。最后看一下资源和状态管理的整体架构: