问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Vulkan入门:核心概念与使用指南

创作时间:
作者:
@小白创作中心

Vulkan入门:核心概念与使用指南

引用
1
来源
1.
https://www.cnblogs.com/ArsenalfanInECNU/p/18096919

Vulkan是新一代的跨平台图形API,以其高性能和低开销的特点在游戏开发和高性能计算领域得到广泛应用。本文将从Vulkan的基本概念入手,深入讲解其核心对象和使用方法,并分享作者在学习和使用Vulkan过程中的经验和建议。

要理解Vulkan,最重要的是理清其各个对象的作用及其关联性。这些对象与实际使用的上层概念有一定距离,且关系较为复杂,只有深入理解这一部分才能顺畅地使用Vulkan。

Queue和Command

在介绍Queue和Command之前,需要先了解Mantle世代API与上一代API的主要区别:这一代API将图形绘制命令分成了Record和Submit两个阶段,Vulkan中对应了Command Buffer和Queue。凡是vkCmd开头的调用都是录制命令,而vkQueue开头的调用都是提交命令(vkQueuePresentKHR可以理解为特殊的提交)。

  • Queue:队列是从Mantle世代API引入的概念,可以看作是对硬件执行流水线的抽象,通过提交任务执行图形计算。简单理解,提交的内容在一个Queue上只能依序执行。GPU的并行执行并非像多线程那样可以随意fire多个task,而是按照一定规则排列。

  • QueueFamily与Queue:翻译过来应该叫做队列家族,每个Family里会有若干queue,常见的Family有三种:Graphic、Compute和Transfer。Graphic队列通常是全能的,支持所有功能,且数量最多;Compute队列不支持Graphic相关功能;Transfer队列专门用于数据上传。这种分类不是绝对的,具体取决于硬件提供的queueFamily数量和支持的功能。Vulkan中对单个queue禁止同一时间有多个线程操作,因此申请多个queue可以实现多个线程提交。

Command

录制使用的对象是Command Pool和Command Buffer。录制本质上就是一个类似push_back的工作,每次调用vkCmd就是在Command Buffer中添加内容。至于这个Command Buffer是在CPU还是GPU上,完全取决于硬件和驱动实现,对Vulkan程序员来说是透明的。录制完成后就可以提交给Queue执行,理论上这时GPU才开始真正执行。这种设计主要是为了支持多线程。在Vulkan中,所谓的多线程渲染基本可以理解为多线程录制,多线程提交其实不太常见,也不好写,而且有更好的方案(往Command Buffer录制并不是直接就能多线程操作的,有很多限制,Vulkan里有一个叫Secondary Command Buffer的东西可以直接用于多线程录制,不需要任何额外同步)。

Buffer和Image

Vulkan主要有两种数据对象:Buffer和Image。Buffer一般用于存储顶点数据、索引数据以及Uniform数据,Image用于存储位图数据,也就是贴图。要真正使用这些数据,还需要配备一个VkDeviceMemory。

  • Mesh:由vertex和index各自对应的Buffer对象和Memory对象组成。Tutorial中对此有详细说明。

  • Texture:VkImage除了需要VkDeviceMemory,还需要VkImageView和VkImageSampler。VkImageView相当于一个访问器,具体操作都需要通过这个访问器,因此VkImage和VkImageView通常是一个强绑定关系。VkImageSampler是采样器,用于设置各向异性过滤、MIP映射等参数,多个图像可以共享一个采样器。

  • Uniform:类似于Mesh,但需要注意的是,Uniform Buffer对应的结构体必须注意内存对齐问题。不同编译器和平台编译出来的内存对齐模式可能不同,而Vulkan对传过来的数据有对齐要求。这个问题很容易被忽视,但一旦出现问题,Validation Layer也无法检查出来。

Memory

这是Vulkan与OpenGL等传统API的第二大区别。在OpenGL中,创建一个Texture后可以直接上传数据并使用,Uniform数据也是通过API直接传输。而在Vulkan中,GPU内存的管理也交给了程序员,VkDeviceMemory对象代表你分配的显存。这类似于C/C++中自己手写内存分配器,可以定制更高级的分配策略。Vulkan非常不鼓励小分配,每个设备的分配次数都有上限,因此建议进行子分配(suballocation)。这种工作可以使用现成的Vulkan Memory Allocator来完成。

Synchronization

Vulkan不仅让程序员管理内存,还要求手工处理同步问题。

  • Fence:充当CPU和GPU之间的同步工具,CPU通过等待Fence来知道GPU是否完成任务。

  • Semaphores:用于两个提交之间建立依赖关系。如果没有Semaphores,用Fence监听提交再通知提交会非常低效,因此两次提交之间通过Semaphores直接在GPU内部建立依赖。

  • Barrier:用于处理缓存一致性问题。Vulkan上传的数据都有一个状态(usage、access mask、VkImageLayout等),这个状态可以给GPU提供优化信息,同时在操作数据时由于GPU直接操作的数据是先在一个缓存里,在另一个阶段读取这个数据的时候可能不是同一套核心的寄存器,这时候就会发生缓存不一致的问题,所以插入一个barrier就能告诉gpu这段写完下个阶段你得先同步缓存。

  • Event:使用较少。

  • Subpass Dependency:提供了类似Barrier的功能,是Subpass专用的功能。Subpass是Vulkan特有的功能,RenderPass必须包含至少一个Subpass,当只有一个Subpass时,建议将dependency留空,Vulkan会补上默认值,自己写容易出错。Tutorial中的实现就有问题。

Pipeline

VkPipeline定义了管线状态,包括隐藏面剔除、混合等功能,最重要的是在这里绑定Shader。一旦Pipeline创建好了就不能修改,要换Shader就需要创建一个新的Pipeline。Compute Shader需要独立的Compute Pipeline,类型同样是VkPipeline,但创建方法不同。提一下Pipeline Cache,这个东西是用来加速Shader加载的,因为Shader即使是SPIR-V这样的底层代码,执行对应GPU的Shader代码还是需要一趟编译变为GPU专用的最优内容,为了节约这个编译时间就搞出了Pipeline Cache。

FrameBuffer

这个类似于OpenGL中的Attachment,只不过Framebuffer通过引用各种ImageView打包成一个集合。从SwapChain获得的Image创建ImageView后放到Framebuffer里,就能供GPU使用了。

RenderPass

这是学习Vulkan时最让人困惑的部分之一。Vulkan的RenderPass本质上描述了一次渲染需要的绘制目标的特征。创建RenderPass时参数里的pAttachments不是真正的attachment,而是attachment的描述,比如第一个attachment是color,第二个是depth。RenderPass通过Framebuffer才能绑定真正的Image(vkRenderPassBeginInfo里绑定),位置一一对应。创建时需要给一个Framebuffer,主要是为了限定这个RenderPass不要乱来,能在一创建就能保证和某个Framebuffer相容。之后的渲染流程中只要符合vk定义的Render Pass Compatibility的Framebuffer,这个Framebuffer就能拿来绑定。

Descriptor

Shader中读取数据在Vulkan中除了顶点数据(layout(location = n) in的形式)外,最常见的就是Uniform了,要传输Uniform就需要通过DescriptorSet或PushConstraint。

  • DescriptorSet:作用是引用用于作为Uniform的Buffer数据,主要是VkBuffer和VkImage两种。VkDescriptorSet类似于Command Buffer,需要从一个DescriptorPool分配出来,然后通过vkUpdateDescriptorSets()方法绑定对应的对象。

  • DescriptorSetLayout:规范约束你这里有多少个set,每个set里有多少buffer和image。Vulkan支持一个Shader用多个DescriptorSet读取数据(layout(set=m,binding = n)形式)。

  • PipelineLayout:包含了DescriptorSetLayout和PushConstraint。在创建VkPipeline时也需要给出PipelineLayout,也就是说Pipeline里的Shader也需要遵守这个Layout。PushConstraint专门用于小数据传输,性能好但容量也小,直接存储在Command Buffer里,而不是VkBuffer,所以也不需要每次更新去map memory然后memcpy。

渲染步骤

大致描述一下渲染步骤的主体:

  1. vkAcquireNextImageKHR:从SwapChains获取下一个可以绘制到屏幕的Image。
  2. vkResetCommandPool/vkResetCommandBuffer:清除上一次录制的CommandBuffer,可以不清但一般每帧的内容都可能发生变化一般都是要清理的。
  3. vkBeginCommandBuffer:开始录制。
  4. vkCmdBeginRenderPass:启用一个RenderPass,这里就连同绑定了一个Framebuffer。
  5. vkCmdBindPipeline:绑定Pipeline,想要换Shader就得在这里做。
  6. vkCmdBindDescriptorSets:绑定DescriptorSets,可以一次绑多个Set,也可以多次绑定多个Set,同时需要给出PipelineLayout。
  7. vkCmdBindVertexBuffers&vkCmdBindIndexBuffer:绑定模型数据。
  8. vkCmdDrawIndexed:最关键的绘制命令,这里可以根据显卡的特性支持情况换更高级的绘制命令比如indirect,相应的数据绑定也需要改。
  9. vkCmdEndRenderPass:结束RenderPass。
  10. vkEndCommandBuffer:结束CommandBuffer。
  11. vkQueueSubmit:提交执行渲染任务。
  12. vkQueuePresentKHR:呈现渲染数据,这时候调用可能vkQueueSubmit还没执行完,但Semaphores会帮我们打点好。

学习资源推荐

学习建议

  • 熟悉工具:RenderDoc等调试工具对调试有很大帮助,要学会使用这些工具。
  • 参考官方文档:Khronos Group的官方spec是最权威的参考,远比市面上的书来得靠谱。
  • 社区支持:遇到问题时,Khronos Group的官方论坛和Reddit的Vulkan版块都是很好的求助渠道。
  • 先学习上层框架:建议先使用一个现有的渲染框架(如Unity)实现一套管线,熟悉图形学的基本概念和流程。
  • 注重性能优化:Vulkan的开发需要特别注重性能优化,特别是在多线程和状态追踪方面。
  • 实践优先:可以先在Unity等更简单的平台上快速实现验证,搞清思路和可能的性能短板,然后再在渲染器中实现完整版。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号