Vulkan入门流程

时间:2021-11-08 11:33:11

原文摘自Vulkan入门流程

Vulkan是Khronos Group(OpenGL标准的维护组织)开发的一个新API,它提供了对现代显卡的一个更好的抽象,与OpenGL和Direct3D等现有api相比,Vulkan可以更详细的向显卡描述你的应用程序打算做什么,从而可以获得更好的性能和更小的驱动开销。Vulkan的设计理念与Direct3D 12和Metal基本类似,但Vulkan作为OpenGL的替代者,它设计之初就是为了跨平台实现的,可以同时在Windows、Linux和Android开发。甚至在Mac OS系统上,Khronos也提供了Vulkan的SDK,虽然这个SDK底层其实是使用MoltenVK实现的。

MoltenVK实际上是一个将Vulkan API映射到Metal API的一个框架,Vulkan这里只是相当于一个抽象层。
然而,为了得到一个更好的性能,Vulkan引入了一个非常冗余的API。相比于OpenGL驱动帮我们做了大量的工作,Vulkan与图像api相关的每一个细节,都需要从头设置,包括初始帧缓冲区的创建与缓冲、纹理内存的管理等等。因此,哪怕只画一个三角形,我们都要写数倍于OpenGL的代码。
而Google在Android 7.0后提供了对Vulkan的支持,并且提供了一系列工具链与Validation Layers(后面会进行说明)。在Android Studio中,只要将Shader代码放在src/main/shaders文件夹下面,项目编译时会自动被编译成.spv字节码,可以作为assets使用。
由于Vulkan的使用非常冗长,这篇文章将主要介绍Vulkan API的一般使用模式以及画一个三角形所需要的基本元素,后续将对每一个章节进行详细介绍,并最终绘制出一个三角形。

Vulkan画三角形的各个步骤

1.Instance and physical device selection
Vulkan程序需要创建一个VkInstance来启用vulkan API.在创建Instance时你需要准确的描述你的应用程序的一些属性,以及你需要使用的各种API。在创建Instance之后,你需要查询硬件对vulkan的支持,并选择一个或者多个VkPhysicalDevices。 你会查询各种硬件参数,比如显存和显卡兼容性,来选取最理想的设备。举个例子,相比集显,咱们更喜欢独显。

2.Logical device and queue families
在决定使用哪块显卡之后,你需要创建一个VkDevice (logical device),在创建过程中你还要更详细的描述你用到的硬件特性(VkPhysicalDeviceFeatures),比如multi viewport rendering(该特性VR渲染非常有用)和64位浮点支持(科学计算和HDR都需要),你还需要制定你要使用的queue families。 大部分Vulkan操作, 比如绘制命令和内存/显存操作, 都需要通过提交到VkQueue之后异步执行。 Queue是从queue family分配的, 每个queue family中的Queue只支持某个特定集合的操作。举个例子,对于显卡,执行图形渲染、并行计算和内存交换可能是不同的queue family。在选择physical device时queue family的支持也是重要的参考之一。只支持科学计算不支持图形渲染的显卡有可能存在(事实上绘图相关的queue family是vulkan的扩张而非核心功能),但是这年头支持vulkan但不支持绘图的设备,其实也不多。

3.Window surface and swap chain
除非你不想显示图形(比如你只想离屏渲染),不然你还是需要创建一个窗口来显示的。你可以用各个平台native的API(win32,xlib,xcb,mir,wayland),或者GLFW、SDL等申请window。如果真的想渲染到一个窗口,还需要两样东西: window surface (VkSurfaceKHR) 和 swap chain (VkSwapChainKHR)。注意 KHR 后缀,有openGL开发经验的同学可能清楚,这个后缀表示一个KHR扩展。Vulkan API 是平台无关的, 所以需要一个标准化的 WSI (Window System Interface) 扩展来和窗口系统进行交互。Surface是一个window渲染目标的抽象,创建时需要一个native窗口的句柄(和openGL完全一样)。Swap chain是渲染一系列渲染目标(render target)的集合。他本来是保证我们渲染的image和屏幕上显示的image不是同一个(openGL双缓冲模型,目的是减少渲染时屏幕闪烁)。除了经典的双缓冲模型,还有三缓冲模型等。

4.Image views and framebuffers
在从swap chain获得image之后,我们应该绘制它。为了绘制image,我们需要把这个image用 VkImageView 以及 VkFramebuffer进行包装。 VkImageView 指向被使用的 image , 而 VkFramebuffer 指向具体作为颜色、模板、深度目标的VkImageView。Swap chain中可能存在很多iamge,每个都需要创建image view还有framebuffer。在渲染时,我们需要选择正确的image进行绘制。

5.Render passes
Vulkan中的Render Pass描述了渲染过程中的image类型以及这些image包括他们的内容会如何被使用,在我们这个Hello Triangle程序中,我们只使用一个image作为颜色目标,并且在渲染前这个image的类型会被清除成一个固定的颜色。Render Pass只描述了image的类型,而VkFramebuffer才是真正绑定该image的对象。

6.Graphics pipeline
Vulkan中通过创建VkPipeline对象来创建graphics pipeline,它用来描述显卡各个渲染阶段的参数,比如viewport的长宽、如何使用深度缓冲,以及用VkShaderModule详细描述的可编程管线的状态。Vulkan和其他图形API有个显著区别,就是的几乎所有配置都需要提前创建。即便是更换一个shader或者改变定点参数的布局(vertex layout),你都需要完全从新graphics pipeline创建一个graphics pipeline。这也意味着你需要创建大量的VkPipeline对象,来覆盖你渲染过程中的各种graphics pipeline变化,只有改变viewport或者改变clear color不需要重新生成graphics pipeline。但是正因为渲染管线各阶段都提前准备了,而不是运行时生成,驱动可以更好的执行优化。

7.Command pools and command buffers
前文中已经提到了,Vulkan中的各种操作,都需要靠提交到一个命令队列(queue)的方式进行异步执行。而在提交到命令队列之前,需要在VkCommandBuffer中进行记录。命令缓冲和一个VkCommandPool关联,而VkCommandPool又和queue family关联。即便只画一个简单的三角形,我们也需要生成如下的command buffer:
Begin the render pass Bind the graphics pipeline Draw 3 vertices End the render pass 因为framebuffer中的image由swap chain决定,每个可能的iamge都需要一个command buffer,这就需要创建大量的command buffer。也可以每帧重新生成command buffer,但是效率会低很多。

8.Main loop 在command buffer准备好了之后,主循环就简单多了。我们先使用vkAcquireNextImageKHR方法得到一个image。然后选择合适的command buffer再使用vkQueueSubmit执行。最后使用vkQueuePresentKHR方法想swap chain传递我们准备在屏幕上显示的image。另外需要注意的是提交到命令队列的命令是异步的,因此保证运行顺序,就是开发者的工作了。vulkan提供了用来同步的对象,比如semaphore,fence等。我们可以使用这些同步对象来保证运行的正确顺序。
总结

总而言之,画三角需要如下的步骤:
创建 VkInstance 创建 VkPhysicalDevice 创建 VkDevice 以及 VkQueue 创建 window, window surface 以及 swap chain 使用 VkImageView 包装swap chain中的images 创建 render pass 创建 framebuffers 创建 graphics pipeline 为每个可能用到的image创建command buffer,并记录draw commands 通过获取image,向image绘制,提交到swap chain的方式来绘制一帧

Vulkan Layers

Vulkan作为高性能API,为了降低驱动开销,只提供了非常有限的错误检查。如果默认情况下,它只包含非常有限的错误检查和调试功能。如果发生了错误,驱动会直接崩溃,而不是返回错误信息,或者显示异常等。
Vulkan允许您通过一个名为Validation Layer的特性进行通用的检查。Validation Layer是可以插入到API和图形驱动程序之间的代码片段,用于执行额外的函数参数检查和跟踪内存管理问题。好处是,您可以在开发过程中启用它们,然后在释放应用程序时完全禁用它们,而开销为零。任何人都可以编写自己的验证层,但是LunarG的Vulkan SDK提供了一组标准的验证层,另外还需要注册一个回调函数来接收来自Validation Layer的调试消息。
因为Vulkan实际上对于每一个步骤是非常明确的,所以相比于OpenGL,我们可以更快速的找出出错的地方。
而Google也提供了一些Validation Layers帮助我们做这些事情,如果要在项目中使用它们,只要修改gradle的构建,或者将二进制文件手动的添加到项目的JNI库目录里,这些so可以在ndk的以下目录找到:

${your-ndk-dir}/sources/third_party/vulkan/src/build-android/jniLibs
sourceSets {
  main {
    jniLibs {
      srcDir "${your-ndk-dir}/sources/third_party/vulkan/src/build-android/jniLibs"
    }
  }
}