0%

【Vulkan】常用组件总结

本篇总结 Vulkan 常用组件之间的关系,以便于后续关于 Vulkan 多线程渲染、内存管理、资源绑定、状态管理等部分的讨论。下图展示了 Vulkan 中重要的组件及其工作流程,我们在之后还会反复见到这张图。

vulkan-instance-device-queue

Vulkan 相比于 DX12 和 Metal,其接口包装的最全也最复杂,但基本能够覆盖 DX12 和 Metal 的功能并且具备非常好的跨平台能力。首先来总结 Vulkan 中各种对象之间的关系。

下图展示了 Vulkan 中大部分常用对象和它们之间的关系,下面从上到下具体来介绍一下,这里要注意,实际 API 会有个 Vk 前缀,比如图里 Instance,在代码中是 VkInstance,CommandBuffer 在代码中是 VkCommandBuffer:

v2-adcf46b99591a0e498c0dd345042f3c8_r

  • Instance:全局的 Vulkan 实例,有一些全局设置存在这个对象上,对应于 OpenGL 中的 context。正常情况一个游戏就创建一个。
  • SurfaceKHR:窗口,这个应该不算 Vulkan 内部的,属于扩展,因为毕竟 Vulkan 要显示到实际的系统窗口里,这个对象主要处理和系统窗口之间的关系,各种设置之类。
  • PhysicalDevice:物理设备,这个就是实际的硬件,比如显卡,集成显卡就算两个设备,可以通过全局函数枚举出来所有设备,所以 VkPhysicalDevice 只是对 GPU 的抽象。这里 Queue Family,Memory Heap 就是物理上提供的队列或者显存之类。
  • Device:这个是逻辑设备的封装,一个物理设备可能有多种功能,可以把一种功能归为一个逻辑设备。一个物理设备可以对应多个逻辑设备。因此 VkDevice 是 VkPhysicalDevice 的子功能集抽象;由于 GPU 中有Queue的概念,这一点被 OpenGL 屏蔽了;但是 Vulkan 暴露了出来;GPU 不一定整个使用;可以使用其中部分的 queue,通常图形引擎只用两个:present 和 graphics,因此,用户可以自己选择 queue “打包”为 1 个 Device(逻辑设备)构造出来。Device 是一个基础概念,几乎绝大部分函数都需要 Device 作为输入参数,由于 Vulkan 是 C,没有类的概念,从 C++ 角度来看,Vulkan 大部分函数都是 Device 的成员函数
  • Queue:这个是设备提供的队列,一般来说,提交给硬件的命令,硬件设备也不是马上执行的,而是放在自己队列里再慢慢执行,当然有的设备也提供多个队列。
  • CommandBuffer这个就是具体业务提交的命令缓冲区,drawcall 就是先交到这里。CommandPool 是创建 CommandBuffer 的对象池,因为 CommandBuffer 创建销毁都比较耗,所以有个池子可以重用以提高性能,另外池子本身是绑定 DeviceFamily 的,所以多个设备的命令没法混一起交。CommandBuffer 是先收集一大堆命令,然后用 vkQueueSubmit 提交给设备的 Queue。
  • Buffer/ImageBuffer 可以理解为一段一维的内存数据,就像我们平常代码里写的 char* 指针加一个大小表示的区域,也可以说是一维数组,Image 可以理解为一段多维的内存数据,也可以说是多维数组,贴图一般都是二维的,所以要用这个表示,当然也能表示一维的,最高三维,这里都是纯数据。Vulkan 本身没有 VertexBuffer 或 IndexBuffer 这样的概念,他们都是Buffer。
  • BufferView/ImageView:这两个就是对应 Buffer 和 Image 的视图,本身没存数据,相当于是 Buffer 和 Image 的解读说明书,让 Vulkan 知道具体怎么解释一段内存,要和 Buffer 或者 Image 绑定使用。
  • Sampler:采样器,就是个数据的壳,也是告诉 Vulkan 具体怎么解读数据的。但和 ImageView 不一样,他不需要绑定到 Image 上
  • DescriptorSet:描述符集,shader 没法直接访问资源,要通过 DescriptorSet 来访问,DescriptorSet 其实就是个内存到 shader 的映射器。DX12 里叫描述符堆 DescriptorHeap。为什么要搞一个这东西呢?主要是为了让 shader 复用资源,把需要的资源接口定义好,当贴图或 Buffer 什么的发生了变化,只要还符合接口格式要求,那么就还是能复用相同 shader。假如没有这一层,直接让 shader 绑死资源,那么换一个贴图就要一个新的 shader,这样就太不灵活了。DescriptorSet 也是通过池来创建的,需要先指定布局 DescriptorSetLayout,相当于是个模板。多个 DescriptorSet 可以通过 PipelineLayout 绑定到 CommandBuffer 上。
  • FrameBuffer就是最后要画到屏幕的那个 RT。BeginRenderPass 的时候,就要带上 FrameBuffer 这个参数,这样 Vulkan 才知道往哪画。每个 RenderPass 都对应一个 FrameBuffer,也就是说可以创建多个 FrameBuffer,然后多个线程同时画,除了上面说的在 CommandBuffer 上能多线程,这里也能多线程。vkCmdBeginRenderPass 和 vkCmdEndRenderPass 之间的代码,就是真正绘制的代码。默认 RenderPass 只有一个 Subpass,创建 RenderPass 时候的参数里可以设多个 Subpass,绘制时候通过 vkCmdNextSubpass 切换。
  • Pipeline:就是最外层的一个大壳,要设置整个渲染管线每一步的流程的参数。分两种,一个图形管线,一个计算管线。计算管线就一个阶段,而图形管线有很多个阶段,是从上到下执行的。VkPipeline 其实对应的是 OpenGL 的 ShaderProgram,但是 shaderProgram 的创建只需要几个 shader 就行了。而创建一个 VkPipeline 要复杂的多得多,因为 VkPipeline 几乎把所有渲染的配置都包含进去了,比如 OpenGL 的 glViewport() 这个函数可以随时运行,但是在 Vulkan 就不行,Pipeline 创建时必须指定视口变换的参数(当然开放了接口可以修改),还有什么多重采样,面剔除,光栅化等等。概括来说,OpenGL 那些配置参数的函数被 Vulkan 分类打包为若干个结构体,这些结构在创建 VkPipeline 时必须事先准备好
  • Pass 和 Subpass:一个 Pass 可以包含多个 Subpass,一个 Subpass 才是一套渲染管线状态下的一次渲染,因此在创建管线 Pipeline 的时候要绑定 RenderPass 和对应的 Subpass 索引,那么 Subpass 的意义是什么呢?使用 Subpass 可以不用把渲染结果回写回内存,就可以在 Subpass 之间传递数据,这样就能很快。尤其是移动端,一般都是分 Tile 绘制的,比如 3 个 pass,如果不用 Subpass 绘制,可能就要先画每个 Tile 的 pass1,再画每个 Tile 的 pass2,再画每个 Tile 的 pass3。如果 pass2 要利用 pass1 的绘制结果,pass1 会先把结果拷回内存,pass2 要再从内存上拷到显存里,这样两次来回读写就有额外的带宽开销。而如果使用 Subpass,会先把第一个 tile 的Subpass1 ~ Subpass3执行完,再执行第二个 tile 的Subpass1 ~ Subpass3,直到所有 tile 执行完,如果 Subpass2 要复用 Subpass1 的结果,不需要拷回内存,直接 fetch 就可以了,这样就不会有额外的开销,就像是有了免费的 G-Buffer 一样,但缺点就是只能复用 Tile 内的结果。
---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。