Vulkan入门:从基础概念到渲染流程详解
Vulkan入门:从基础概念到渲染流程详解
Vulkan是新一代的图形API,相比传统的OpenGL和Direct3D,Vulkan提供了更底层的硬件访问能力,同时也带来了更高的开发难度。本文将从Vulkan的基本概念入手,详细介绍Vulkan的核心对象和使用方法,并分享一些学习和开发Vulkan的经验。
基本概念
要理解Vulkan,最重要的是理清Vulkan的各种对象的作用及其关联性。这些对象与实际使用的上层概念有一定距离,且关系较为复杂,只有深入理解这些基础概念,才能顺畅地使用Vulkan。
Queue
队列(Queue)是从Mantle世代API引入的概念,可以看作是对硬件执行流水线的抽象。通过提交任务来执行图形计算。简单来说,提交的内容在一个队列上只能按顺序执行。GPU的并行执行并不是像多线程那样可以随意fire多个task,而是按照一定的规则排列。
- QueueFamily与Queue:QueueFamily(队列家族)包含若干个Queue。常见的QueueFamily有三种:Graphic、Compute和Transfer。Graphic队列通常功能最全面,包含的Queue数量也最多;Compute队列不支持Graphic相关功能;Transfer队列专门用于数据上传。这种分类不是绝对的,具体取决于硬件提供的QueueFamily数量和支持的功能。Vulkan中单个Queue禁止同一时间被多个线程操作,因此申请多个Queue可以实现多线程Submit。
Command
录制使用的对象是Command Pool和Command Buffer。录制过程类似于push_back操作,每次调用vkCmd开头的函数都会向Command Buffer中添加内容。Command Buffer在CPU还是GPU上取决于硬件和驱动实现,对Vulkan程序员来说是透明的。录制完成后,可以将Command Buffer提交给Queue执行。
Buffer
Vulkan主要有两种基本对象:Buffer和Image。Buffer用于存储顶点、索引和Uniform数据,而Image用于存储位图数据,即贴图。Buffer要真正使用还需要配备一个VkDeviceMemory。
Mesh
Mesh由Vertex Buffer和Index Buffer组成,每个Buffer都需要一个Memory对象。Vulkan教程中对这部分的解释已经很清晰。
Texture
VkImage除了需要VkDeviceMemory,还需要VkImageView和VkImageSampler。VkImageView相当于一个访问器,VkImage和VkImageView通常绑定在一起使用。VkImageSampler用于设置各向异性过滤、Mipmaps等采样参数,多个图像可以共享一个采样器。
Uniform
Uniform类似于Mesh,但需要特别注意内存对齐问题。不同编译器和平台生成的内存对齐模式可能不同,而Vulkan对传入的数据有对齐要求。这个问题在教程中有所提及,但容易被忽视。
Memory
在Vulkan中,GPU内存的管理交给了程序员。VkDeviceMemory对象代表分配的显存。这类似于C/C++中的手动内存管理,允许开发者实现更高级的分配策略。Vulkan不鼓励小分配,每个设备的分配次数都有上限,因此推荐使用子分配(suballocation)。可以使用现成的Vulkan Memory Allocator来简化内存管理。
Synchronization
Vulkan要求开发者手动处理同步问题:
- Fence:用于CPU和GPU之间的同步,CPU可以通过等待Fence来知道GPU是否完成任务。
- Semaphore:用于两个提交之间建立依赖关系,避免低效的Fence等待。
- Barrier:用于处理缓存一致性问题,确保数据在不同阶段正确同步。
- Event:使用较少。
- Subpass Dependency:提供类似Barrier的功能,专门用于Subpass。
Pipeline
VkPipeline定义了管线状态,包括隐藏面剔除、混合等设置,最重要的是在这里绑定Shader。一旦Pipeline创建完成,就不能修改,需要更换Shader时必须创建新的Pipeline。Compute Shader需要独立的Compute Pipeline。Pipeline Cache用于加速Shader加载,因为Shader代码需要编译为GPU专用的最优代码。
FrameBuffer
FrameBuffer类似于OpenGL中的Attachment,通过引用各种ImageView打包成一个集合。从SwapChain获得的Image创建ImageView后,就可以用于GPU渲染。
RenderPass
RenderPass是Vulkan中最复杂的概念之一。它描述了一次渲染所需的绘制目标的配置。创建RenderPass时需要指定Attachment的描述,如颜色附件和深度附件。RenderPass通过FrameBuffer绑定实际的Image。Vulkan要求在创建RenderPass时绑定一个FrameBuffer,以确保兼容性。
Descriptor
Shader中读取数据主要通过Uniform实现。Uniform数据通过DescriptorSet或PushConstant传输:
- DescriptorSet:引用用于作为Uniform的Buffer数据,包括VkBuffer和VkImage。DescriptorSet需要从DescriptorPool分配,并通过vkUpdateDescriptorSets()方法绑定具体对象。
- DescriptorSetLayout:规范约束DescriptorSet的结构,定义每个Set中包含的Buffer和Image数量。
- PipelineLayout:结合DescriptorSetLayout和PushConstant,用于创建Pipeline时指定Shader的数据布局。
渲染流程
Vulkan的渲染流程主要包括以下步骤:
- vkAcquireNextImageKHR:从SwapChain获取下一个可绘制到屏幕的Image。
- vkResetCommandPool/vkResetCommandBuffer:清除上一次录制的CommandBuffer。
- vkBeginCommandBuffer:开始录制CommandBuffer。
- vkCmdBeginRenderPass:启用RenderPass并绑定FrameBuffer。
- vkCmdBindPipeline:绑定Pipeline,更换Shader需要重新绑定。
- vkCmdBindDescriptorSets:绑定DescriptorSets,需要给出PipelineLayout。
- vkCmdBindVertexBuffers/vkCmdBindIndexBuffer:绑定模型数据。
- vkCmdDrawIndexed:执行绘制命令。
- vkCmdEndRenderPass:结束RenderPass。
- vkEndCommandBuffer:结束CommandBuffer录制。
- vkQueueSubmit:提交渲染任务。
- vkQueuePresentKHR:呈现渲染数据。
学习资源推荐
- Khronos Group Vulkan Samples:官方示例代码仓库,包含大量Vulkan应用实例。
- Sascha Willems Vulkan Samples:另一个知名的Vulkan示例代码仓库。
- NVIDIA DesignWorks Samples:NVIDIA的Vulkan示例代码仓库。
- Writing an efficient Vulkan renderer:关于Vulkan最佳实践的文章,提供了许多可行方案。
- 2017 DevU - 04 - Synchronization:关于Vulkan同步机制的视频教程。
- RenderDoc:用于Vulkan应用调试的工具,可以帮助定位渲染问题。
学习建议
- 先学习上层渲染框架:建议先使用Unity等上层渲染框架实现一套完整的渲染管线,这样可以更好地理解Vulkan的底层细节。
- 关注性能优化:Vulkan开发中性能优化是最重要的,需要在数据结构和多线程开发等方面进行深入研究。
- 利用官方资源:Vulkan的官方规格文档是最权威的参考资料,遇到问题可以到Khronos Group的官方论坛寻求帮助。