画一个三角形

现在,让我们来看下如何使用Vulkan绘制三角形。这里用到的所有概念会在下一章节进行详细地说明。

步骤1:实例和物理设备选择

我们的应用程序是通过VkInstance来使用Vulkan API的。应用程序创建VkInstance后,就可以查询Vulkan支持的硬件,选择其中一个或多个VkPhysicalDevices进行操作。我们可以通过查询设备属性,选择一个适合我们的设备。

步骤2:逻辑设备和队列族

选择完合适的硬件设备后,我们需要使用更详细的VkPhysicalDevice特性(比如多视口,64位浮点)来创建一个逻辑设备VkDevice。还需要指定我们想要使用的队列族。Vulkan将诸如绘制指令、内存操作提交到VkQueue中,进行异步执行。队列由队列族分配,每个队列族支持一个特定操作集合。比如,图形,计算和内存传输操作可以使用独立的队列族。队列族可以作为物理设备选择时的一个参考。比如,一个支持Vulkan的设备可能没有提供任何图形功能,但对于支持Vulkan的显卡设备而言,支持所有队列操作。

步骤3:窗口表面和交换链

如果不是进行离屏渲染,通常我们需要创建一个窗口来显示渲染的图像。这一工作可以通过原生平台的窗口API或像GLFW或SDL这样的库来完成,在这里,我们使用的是GLFW,有关GLFW的更多信息,我们会在下一章介绍。

我们还需要两个组件才能完成窗口渲染:窗口表面(VkSurfaceKHR)和交换链(VkSwapChainKHR)。可以注意到这两个组件都有一个KHR后缀,这表示它们属于Vulkan扩展。Vulkan API本身是完全平台无关的,需要我们使用WSI(Window System Interface,窗口系统接口)扩展与原生的窗口管理器进行交互。表面(Surface)是一个跨平台抽象,通常它是由原生窗口系统句柄作为参数实例化得到。不过,这一部分工作,GLFW已经帮我们处理了,所以不用我们关心。

交换链是一个渲染目标集合。它可以保证我们正在渲染的图像和当前屏幕图像是两个不同的图像。这可以确保显示出来的图像是完整的。每次绘制一帧时,可以请求交换链提供一张图像。绘制完成后,图像被返回到交换链中,在之后某个时刻,图像被显示到屏幕上。渲染目标数量和图像显示到屏幕的时机依赖于显示模式。常用的显示模式有双缓冲(vsync,垂直同步)和三缓冲。我们将在创建交换链章节讨论这些问题。

步骤4:图像视图和帧缓冲

从交换链获取图像后,还不能直接在图像上进行绘制,需要将图像先包装进VkImageView和VkFramebuffer中去。一个图像视图可以引用图像的特定部分,一个帧缓冲可以引用图像视图作为颜色,深度和模板目标。交换链中可能有多个不同的图像,我们可以预先为它们每一个都创建好图像视图和帧缓冲,然后在绘制时选择对应的那个。

步骤5:渲染流程

渲染流程描述了渲染操作使用的图像类型,图像的使用方式,图像的内容如何处理。对于我们这个绘制三角形的程序,我们使用了一张图像作为颜色目标,在执行绘制操作前清除整个图像。渲染流程只描述了图像的类型,图像绑定是通过VkFramebuffer完成的。

步骤6:图形管线

Vulkan的图形管线可以通过VkPipeline对象建立。它描述了显卡的可配置状态,比如视口大小和深度缓冲操作,以及使用VkShaderModule对象的可编程状态。VkShaderModule对象由着色器字节码创建而来。驱动程序知道哪些渲染目标被图形管线使用。

Vulkan与之前的图形API的一个最大不同是几乎所有图形管线的配置都需要提前完成。这意味着如果我们想要使用另外一个着色器或者顶点布局,需要重新创建整个图形管线。显然效率很低,这迫使我们提前创建出所有我们需要的图形管线,在需要时直接使用已经创建好的图形管线。图形管线只有很少一部分配置可以动态修改,比如视口大小和清除颜色。图形管线的所有状态也需要显式地描述,比如,不存在默认的颜色混合状态。

这样做的好处类似于预编译相比于即时编译,驱动程序可以有更大的优化空间,并且以图形管线为切换单位,渲染效果的预期也变得十分容易,不用担心切换时,遗漏某个微小的设置,造成结果的巨大差异。

步骤7:指令池和指令缓冲

之前提到,Vulkan的许多操作需要提交到队列才能执行。这些操作首先被记录到一个VkCommandBuffer对象中,然后提交给队列。VkCommandBuffer对象由一个关联了特定队列族的VkCommandPool分配而来。为了绘制三角形,我们需要记录下列操作到VkCommandBuffer对象中去:

  • 开始渲染

  • 绑定图形管线

  • 绘制三角形

  • 结束渲染

由于帧缓冲绑定的图像依赖于交换链给我们的图像,我们可以提前为每个图像建立指令缓冲,然后在绘制时,直接选择对应的指令缓冲使用。当然在每一帧记录指令缓冲也是可以的,但这样做效率很低。

步骤8:主循环

将绘制指令包装进指令缓冲后,主循环变得非常直白。我们首先使用vkAcquireNextImageKHR函数从交换链获取一张图像。接着使用vkQueueSubmit函数提交图像对应的指令缓冲。最后,使用vkQueuePresentKHR函数将图像返回给交换链,显示图像到屏幕。

提交给队列的操作会被异步执行。我们需要采取同步措施比如信号量来确保操作按正确的顺序执行。绘制指令的执行必须在获取图像之后,否则,可能会出现读写冲突,屏幕正在读取图像数据的同时,绘制操作在进行绘制操作,造成屏幕读取显示的数据并非来自同一帧。同样,vkQueuePresentKHR函数调用需要在绘制完成后进行。