Vulkan 绘制三角形的冗长过程总结

1,029 阅读9分钟

如果我自己没有搞过 OpenGLMetal,那么学 Vulkan 对我来说就是折磨。网上存在一大堆如何配置环境,画一个三角形的教程,比如

这些文章也对某些变量和函数的作用做了一些解释,但是太分散了而且没啥中文内容,所以这里写一遍文章总结一下,供那些已经画出一个三角形,但是感觉自己啥也不懂的开发者参考。(我也才上手 Vulkan 一星期,还是有很多地方不懂的)

注意,这篇文章并不讲如何配置和使用Vulkan!,也不会列出如何画一个三角形的代码,不懂的请使用上方链接的教程

如果文章哪里有错误的,欢迎在评论区指出,谢谢!!!

简述 Vulkan 绘制一张图像的过程

Vulkan 设备初始化

假设你已经找到了使用 Vulkan 所需的 ProcAddr 了,这个部分我们一般交给某些框架去处理,比如 glfw

Vulkan Instance

学过 OpenGL 的知道它是个状态机,但是开发者对其状态的管理很有限。

同样的, VkInstance 把这些各种状态储存起来,并且供开发者管理,这个类是个 Handle(大家都叫它句柄,其实我也不是很懂,我就把它当作一个神奇的指针,当作参数送给诸如 VkXXX 的函数,它们知道该怎么用,对我们来说就是一个黑盒)。

创建这个 VkInstance 调用vkCreateInstance(...),同时也会自动初始化 Vulkan library

Device

有了上述的 Instance,我们就可以准备冻手了!

我们可以对设备进行初始化,Vulkan 为了平台无关性,又对 Device 进行了一次抽象处理,下面这些依次创建的 Handle 给大家梳理下。

  • VkPhysicalDevice(Handle)

    自信点,你一个抽打游戏的,这个东西可以代表的肯定是那张显卡。(其他魔幻的并行计算设备不熟👋)

    我们一般使用函数 vkEnumeratePhysicalDevices(...) 获得所有可用的设备数量,然后再调用一次储存设备为 VkPhysicalDevice ,接着调用函数 vkGetPhysicalDeviceProperties(...) 获得设备信息,这么做有时候是为了多 GPU 用户挑选最佳设备。(你以为我讲的情况指的是两张独立显卡,其实是一张 I家垃圾核芯显卡 和 一张独立显卡)

    经过上述操作,你就可以挑选出适合的 VkPhysicalDevice(Handle)

  • Queue Family

    不想翻译,因为翻译了更看不懂,直接解释更容易理解。

    现代的 GPU 的作用已经超出了它曾经的定义了,它不仅仅是可以用来处理图形的,因为本质上并行计算,而 Queue 可以看做执行指令的队列。这里列出几类,你就理解了。

    • VK_QUEUE_GRAPHICS_BIT

    • VK_QUEUE_COMPUTE_BIT

    • VK_QUEUE_VIDEO_DECODE_BIT_KHR

    • VK_QUEUE_VIDEO_ENCODE_BIT_KHR

    使用某个 Queue 用的是下标(Index),通过调用函数 vkGetPhysicalDeviceQueueFamilyProperties(...) 获得信息,任君挑选。

  • Logical Device

    写过 Metal 应该很熟悉,经常使用 MTL::Device 干大事。

    这个 VkDevice 的作用就像是 GPU 驱动了,把物理设备给抽象掉了,好处就是可以完成很多魔幻的操作,比如你可以多个 GPU 但只用一个 VkDevice 管理等等。用官方的文档原话讲:这个对象是代表着和实际物理设备连接。

    我们就老老实实,把想要使用的 QueuePhyscialDevice 交给函数vkCreateDevice(...) 去创建,你只需知道后续的很多操作都要用到 VkDevice 这个 Handle 即可。

至此,我们处理掉了 Vulkan 设备部分的内容,即初始化 Vulkan,挑选物理设备,创建相应队列,最后抽象成 VkDevice

Vulkan 绘制部分初始化

这部分是最头疼的,最难懂的,心里有数。

SwapChain

A swapchain object (a.k.a. swapchain) provides the ability to present rendering results to a surface.

Vulkan 中, SwapChain 是作为一个拓展,一般用于输出图像到屏幕上,为什么作为拓展,因为不同的显卡有时候并不需要输出图像到屏幕(比如专门的矿卡或者深度学习,或者纯离屏渲染),我们通过函数 vkEnumerateDeviceExtensionProperties(...) 去获得扩展支持信息。

SwapChain 的工作原理如下:

Screen Shot 2022-06-23 at 11.35.49.png

注意到,我下面标注的 Present Mode, 不同模式下可能会有区别。VkImage 的个数取决于你,怎么使用 VkImage 也取决于你,注意到送到 Present EngineVkImage 会按照队列形式显示,vkAcquireNextImageKHR 的顺序取决于你如何 Present

SwapChain 的创建还得需要其他属性

  • VkSurfaceKHR

    上述的Present Engine 会把 VkImage 输出到 VkSurface 。在这里不得不提到 WSI(windows system integration)

    你不想画个三角形还去配置操作系统提供的 SDK,学一堆诸如 CocoaWin32 之类的东西,那太恶心了。(我本来根据苹果官方的 Metal C++ 文档学习,也是因为处理一些 I/O 之类的东西不得不写 Objective-C,看得头皮发麻,于是转战 Vulkan

    这个 WSI 就帮你处理掉这些平台的东西了,你只要创建一个 VkSurfaceKHRVulkan 会自己去找对应平台的实现,比如调用 glfw 提供的 glfwCreateWindowSurface(...) 即可。

    VkSurfaceKHR 的创建还需要一些颜色空间信息,输出图像大小信息等等,这里就不再赘述。

Vulkan 着手绘制的内容

如果你到这里有点小懵,我就稍微小结一波。

  1. 我们设置了 VkInstace,让我们可以使用 Vulkan 提供的功能
  2. 我们配置了显卡设备, 获得一个厉害的 VkDevice,并得到了提交显卡指令的 VkQueue
  3. 为了显示内容,我们还设置了 VkSwapchain 让他输出到 VkSurfaceKHR

到这里你会发现,上面这些更像是设置一些属性,就像 我要用什么样的画布,要什么样的颜料等等,但是这些东西还没买。 所以,我们可能会开始接触一些带 Buffer 的属性,着手分配资源了。

这部分是最头疼的,最难懂的,建议自己配合文档阅读。

VkRenderPass

刚接触 Metal 的时候,就有个 MTL::RenderPassDescriptor 但是我一直没搞懂。Vulkan 的官方文档也含糊其辞,没有具体定义,但是我们可以看看他的用处。

Draw commands must be recorded within a render pass instance. Each render pass instance defines a set of image resources, referred to as attachments, used during rendering.

我个人理解这个 Pass 更像是 Progress,画一幅画可能有多个步骤,比如构图思考,画线稿,简单上色,纹理细节等等, VkRenderPass 就掌管这些信息,这些子过程可以用VkSubpassXXX 表示。我们之后提到的各种属性的创建函数中都需要 VkRenderPass 作为参数。

VkShaderModule

这个部分没啥好讲的,要是这个都不知道,这篇文章不适合你,建议从 openGL 基础学起。

你可以离线编译,也可以运行时编译 shader。运行时编译可以参考 Google-Shaderc,里面的 example 写得很详细了。

VkCommandBuffer

MetalMTL:CommandBuffer 是同一个东西。

作用也很简单,GPU 还记得上面提过的各类 Queue 吗? 你想要的执行的各种指令比如 Draw 等等,全部收集在这个 Buffer 里,最后一次性提交。

为什么不一条指令一条指令提交呢? CPU 很快, GPU 也很快, 但是总线上的 I/O 就不是了,你一条条指令慢慢送过去 CPUGPU 谁都不好过,一次打包送过去效率就高很多了。

创建 VkCommandBuffer 需要一个 VkCommandPool对象 来管理内存的分配,具体创建过程这里不再赘述。

Graphics Pipelines

这里我讲的还是曾经的渲染管道,现代 GPU 已经有各式各样的管道,比如 Compute Pipeline , Ray Tracing Pipeline 等等。

这部分内容就比较复杂,这里列出 Vulkan 官方的一张大图,你不经会感叹现代 GPU 已经发展得这么复杂了,更幸运的是,这些过程你要自己去配置,而不是像曾经 OpenGL 那样简单的调用一下 glUsePrograme, glDrawXXX,驱动帮这些过程都做好了。

从未有如此美妙的开局,虽然画个三角形变得麻烦了,但是控制怎么去画一个三角形变得可能了,可编程的范围变大了,不用再陷入过去那些各种调库的黑魔法里面去了。

Screen Shot 2022-06-23 at 15.29.16.png

创建 Vulkan 对象一般都需要一个 CreateInfo,这里我们从简单的讲起吧,无视掉曲面多重采样深度模版这三个属性。

typedef struct VkGraphicsPipelineCreateInfo {
    VkStructureType                                  sType;
    const void*                                      pNext;
    VkPipelineCreateFlags                            flags;
    uint32_t                                         stageCount;
    const VkPipelineShaderStageCreateInfo*           pStages;
    const VkPipelineVertexInputStateCreateInfo*      pVertexInputState;
    const VkPipelineInputAssemblyStateCreateInfo*    pInputAssemblyState;
    const VkPipelineTessellationStateCreateInfo*     pTessellationState;
    const VkPipelineViewportStateCreateInfo*         pViewportState;
    const VkPipelineRasterizationStateCreateInfo*    pRasterizationState;
    const VkPipelineMultisampleStateCreateInfo*      pMultisampleState;
    const VkPipelineDepthStencilStateCreateInfo*     pDepthStencilState;
    const VkPipelineColorBlendStateCreateInfo*       pColorBlendState;
    const VkPipelineDynamicStateCreateInfo*          pDynamicState;
    VkPipelineLayout                                 layout;
    VkRenderPass                                     renderPass;
    uint32_t                                         subpass;
    VkPipeline                                       basePipelineHandle;
    int32_t                                          basePipelineIndex;
} VkGraphicsPipelineCreateInfo;
  • VkPipelineShaderStageCreateInfo 比如最简单的,一个顶点着色器和一个像素着色器,也就2个 Stage

  • VkPipelineVertexInputStateCreateInfo (Fixed-Function)

    一般用于设置一些顶点的输入数据,类似于 Metal[[attribute]] 使用方法,即配置 GLSL layout 的内容

  • VkPipelineInputAssemblyStateCreateInfo(Drawing Command)

    提供信息来决定这些顶点数据最后成为什么样的几何形体,不如点,线,三角形

  • VkPipelineViewportStateCreateInfo

    ViewPort,可以配置裁剪空间的信息,比如裁剪的位置和大小等

  • VkPipelineRasterizationStateCreateInfo

    光栅化的一些属性配置,比如深度偏移量,面剔除,填充模式等等

  • VkPipelineColorBlendStateCreateInfo(FrameBuffer)

    定义怎么去 blend,这部分暂时也说不清,你只需要知道用一些运算操作可以在这里来处理输出的颜色

  • VkPipelineDynamicStateCreateInfo

    定义一些 Pipeline 中可以动态修改的内容

  • VkPipelineLayout

    Access to descriptor sets from a pipeline is accomplished through a pipeline layout.

    字面意思,通过 layout 获得各类 descriptor 的信息

FrameBuffer

Graphic Pipeline 已经创建完了,我们已经可以提交命令给 GPU 让他渲染和显示了,不过这里还有最后几个步骤需要去配置

  • VkImageView

    VkImage 就一张图(其实就是个大数组),他怎么可能懂自己是什么格式,自己怎么被画的,而且不能被 Pipeline Shader 直接访问。这个时候就要 VkImageView 来帮忙了,它的行为更像是中介。

    vkGetSwapchainImagesKHR 获得 VkImage,调用函数 vkCreateImageView(...) 把刚刚获得的 VkImage 当参数传入,再加上一些诸如怎么合成,图像大小的信息参数进去。

    有了这个 VkImageView,我们可以开辟显存给 FrameBuffer 使用了。

    其实我们上面讲的那么多东西,都只是属性配置,并没有真正去分配资源所需的存储空间。

  • 录制和提交指令

    vkQueuePresentKHR(...)vkAcquireNextImageKHR(...) 获得 VkImage 的下标,绑定 RenderPassPipeline,然后开始录制 诸如vkCmdDraw(...) 的指令,最后提交给 GPU

小结

这篇文章肯定讲的不清楚,但是能看个大概。真要深入 VulkanVulkan Specification 多查查就行了(官方提供的学习方法推荐),不要局限于具体实现,应该多多联系其他部分的内容。

参考资料

Vulkan Spec # 1.3.218