如果我自己没有搞过 OpenGL
和 Metal
,那么学 Vulkan
对我来说就是折磨。网上存在一大堆如何配置环境,画一个三角形的教程,比如
-
网页中间的
Vulkan Tutorials
栏目有很多学习资源。
这些文章也对某些变量和函数的作用做了一些解释,但是太分散了而且没啥中文内容,所以这里写一遍文章总结一下,供那些已经画出一个三角形,但是感觉自己啥也不懂的开发者参考。(我也才上手 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
管理等等。用官方的文档原话讲:这个对象是代表着和实际物理设备连接。我们就老老实实,把想要使用的
Queue
和PhyscialDevice
交给函数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
的工作原理如下:
注意到,我下面标注的 Present Mode
, 不同模式下可能会有区别。VkImage
的个数取决于你,怎么使用 VkImage
也取决于你,注意到送到 Present Engine
, VkImage
会按照队列形式显示,vkAcquireNextImageKHR
的顺序取决于你如何 Present
。
SwapChain
的创建还得需要其他属性
-
VkSurfaceKHR
上述的
Present Engine
会把VkImage
输出到VkSurface
。在这里不得不提到WSI(windows system integration)
你不想画个三角形还去配置操作系统提供的 SDK,学一堆诸如
Cocoa
或Win32
之类的东西,那太恶心了。(我本来根据苹果官方的Metal C++
文档学习,也是因为处理一些 I/O 之类的东西不得不写Objective-C
,看得头皮发麻,于是转战Vulkan
)这个
WSI
就帮你处理掉这些平台的东西了,你只要创建一个VkSurfaceKHR
,Vulkan
会自己去找对应平台的实现,比如调用glfw
提供的glfwCreateWindowSurface(...)
即可。VkSurfaceKHR
的创建还需要一些颜色空间信息,输出图像大小信息等等,这里就不再赘述。
Vulkan 着手绘制的内容
如果你到这里有点小懵,我就稍微小结一波。
- 我们设置了
VkInstace
,让我们可以使用Vulkan
提供的功能 - 我们配置了显卡设备, 获得一个厉害的
VkDevice
,并得到了提交显卡指令的VkQueue
- 为了显示内容,我们还设置了
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
和 Metal
的 MTL:CommandBuffer
是同一个东西。
作用也很简单,GPU 还记得上面提过的各类 Queue
吗? 你想要的执行的各种指令比如 Draw
等等,全部收集在这个 Buffer
里,最后一次性提交。
为什么不一条指令一条指令提交呢? CPU
很快, GPU
也很快, 但是总线上的 I/O 就不是了,你一条条指令慢慢送过去 CPU
和 GPU
谁都不好过,一次打包送过去效率就高很多了。
创建 VkCommandBuffer
需要一个 VkCommandPool
对象 来管理内存的分配,具体创建过程这里不再赘述。
Graphics Pipelines
这里我讲的还是曾经的渲染管道,现代 GPU 已经有各式各样的管道,比如 Compute Pipeline
, Ray Tracing Pipeline
等等。
这部分内容就比较复杂,这里列出 Vulkan
官方的一张大图,你不经会感叹现代 GPU 已经发展得这么复杂了,更幸运的是,这些过程你要自己去配置,而不是像曾经 OpenGL
那样简单的调用一下 glUsePrograme
, glDrawXXX
,驱动帮这些过程都做好了。
从未有如此美妙的开局,虽然画个三角形变得麻烦了,但是控制怎么去画一个三角形变得可能了,可编程的范围变大了,不用再陷入过去那些各种调库的黑魔法里面去了。
创建 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]]
使用方法,即配置 GLSLlayout
的内容 -
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
的下标,绑定RenderPass
和Pipeline
,然后开始录制 诸如vkCmdDraw(...)
的指令,最后提交给 GPU
小结
这篇文章肯定讲的不清楚,但是能看个大概。真要深入 Vulkan
, Vulkan Specification
多查查就行了(官方提供的学习方法推荐),不要局限于具体实现,应该多多联系其他部分的内容。