六一的部落格


关关难过关关过,前路漫漫亦灿灿。



Khronos Vulkan Tutorial - Overview

本节将介绍Vulkan和其设法解决的问题. 稍后, 我们将专注于绘制三角形的必要组成, 从中也可窥见后续章节的组织方式.

我们会逐一覆盖Vulkan API的组织结构, 并给出通用的使用模板.


Vulkan起源

设计之初, Vulkan旨在作为适用于不同GPU的跨平台抽象接口. 此时的图形硬件受限于固定的可配置功能, 相应地, 这也造就大多数API在设计上的困境. 程序员需要以规范格式提供顶点数据 vertex data , 光照和着色的配置受到GPU生产商的摆布.

随着显卡架构的成熟, 才允许更多可编程功能的出现. 这些新增的功能需与已有API以某种方式整合, 这使得接口抽象的过程让步于现实, 将程序员意图映射到现代图形架构的过程更多地基于对图形驱动的推测. 这是如此多改善甚至大幅提升游戏性能的驱动更新出现的原因. 由于驱动的复杂性, 应用开发者们也许要应对不同供应商之间的不一致, 比如着色器允许的语法.

除去新特性, 移动设备使得大量强有劲的图形硬件出现. 由于移动设备的供电和内存要求, 其GPU的架构不同于主机. 比如分块渲染 tiled render , 程序员在此功能上可以有更多的控制, 以改善性能.

另外, 很多API不一定支持多线程 multi-threading , 这会造成CPU方面的瓶颈.

Vulkan严格按照现代图形学架构设计, 能够解决上述问题. 程序员可以通过复杂性更高的API清楚地描述其意图, 减少来自驱动的辖制; Vulkan支持多线程并行创建和提交指令.

同样, 通过编译器将着色器转换成标准字节码格式, Vulkan解决了不同着色器语法规范不一致的问题.

最后, Vulkan通过将图形和计算功能统一到单个API实现了现代图形显卡均应具有的处理能力.


绘制三角形的必要组成


实例和物理设备的选择

Vulkan应用程序一开始会通过 VkInstance 搭建Vulkan API. 我们需描述应用程序以及所需的API扩展 extension 来创建实例.

创建实例后, 我们可以查询Vulkan支持哪些硬件, 并选择一个或多个 VkPhysicalDevices 在之后的操作中使用.

我们也可以查询属性, 如 VRAM 的大小, 设备功能, 以选择期望的设备, 比如倾向于使用专用图形显卡.


逻辑设备和队列族

选择了正确的硬件设备后, 我们需要创建逻辑设备 VkDevice , 此时需要描述将使用的 VkPhysicalDeviceFeatures , 如多视口渲染 multi viewport rendering 和64位浮点数.

我们还需指定要使用的队列族 Queue family .

我们通过Vulkan执行的操作, 如绘制命令和内存操作, 都是提交到 VkQueue 异步执行. 队列由队列族进行分配, 每个队列族的队列支持特定的一套操作集合.

比如, 可以有相应的队列族用于图形, 计算和内存传输 memory transfer 操作. 当我们选择物理设备时, 也可将队列族的适用性纳入参考.

Vulkan支持的设备可以不提供任何图形功能, 不过, 目前Vulkan所支持的显卡均支持我们所需的所有队列操作.


窗口上层 window surface 和交换链 swap chain

除离屏渲染 offscreen rendering 外, 我们都需要创建窗口以呈现渲染好的图像.

我们可以使用 GLFW 库或 SDL 库来创建窗口. 本教程使用 GLFW .

渲染窗口需要用到两个组件: 窗口上层 VkSurfaceKHR 和交换链 VkSwapchainKHR . KHR 后缀表明组件作为Vulkan扩展的一部分.

Vulkan API本身无关平台, 因此, 与窗口管理的互动由标准化 WSI 扩展完成. Window System Interface

窗口上层是窗口的跨平台抽象,对其实例化将得到窗口句柄 window handle 的引用, 好比Windows系统的 HWND . GLFW 库的内置函数负责处理特定平台的细节.

交换链是渲染目标的集合. 其基本意图为区分正在渲染的图像和已经呈现到屏幕的图像. 我们需要确保只有渲染完成的图像才会显示在屏幕.

每次我们绘制帧之前, 要求交换链提供要渲染的图像; 绘制帧完成后, 再将图像返回给交换链, 在之后的某个时候显示到屏幕上.

渲染目标的个数和将已完成图像显示到屏幕的条件由当前模式 present mode 决定. 常见的当前模式有双缓冲 double buffering vsync 和三缓冲 triple buffering .

有的平台允许直接渲染到显示器, 而无需通过 VK_KHR_displayVK_KHR_display_swapchain 扩展与窗口管理互动. 这种情形下, 我们可以创建代表整个屏幕的窗口上层实例, 在自定义的窗口管理中使用.


图像视图 image view 和帧缓冲 framebuffer

要绘制交换链提供的图像, 我们需将图像纳入 VkImageViewVkFramebuffer .

图像视图引用了将要使用的图像的特定部分, 而帧缓冲引用了要用于颜色, 深度 depth 和模板目标 stencil target 的图像视图.

交换链中有多个不同的图像, 我们会为每个图像预先创建图像视图和帧缓冲, 并在绘制时使用对应的.


渲染通道 render passes

Vulkan中的渲染通道描述了进行渲染操作期间的图像类型, 以及如何使用这些图像类型, 如何处理其内容.

之后的三角形渲染程序中, 我们只有一个图像作为颜色目标 color target , 在绘制操作之前, 被清空为纯色 solid color .

渲染通道仅用来描述图像类型, 而 VkFramebuffer 则将特定图像绑定到插槽 slot .


图形管线 graphics pipeline

Vulkan的图形管线通过 VkPipeline 实例搭建. 该实例描述了显卡的可配置状态, 如视口大小 viewport size , 深度缓冲区 depth buffer , VkShaderModule 实例的可编程状态.

我们使用着色字节码创建 VkShaderModule 实例.

我们通过引用渲染通道指定渲染目标, 而驱动需要知道管线中用到的渲染目标.

和其他API相比, Vulkan的一个显著特点是所有图形管线需要预先配置好. 即, 如果我们想要切换到另一个着色器, 或者稍微改变顶点布局, 我们需要重新创建图形管线. 也就是说, 我们需要为多种渲染操作组合提前创建若干个 VkPipeline 实例.

只有一些基础配置, 如视口大小 viewport size 和清空颜色 clear color , 可以动态改变.

所有状态需要明确描述, 比如, 颜色混合状态 color blend state 没有默认值.

由于这些要求相当于从即时编译 just-in-time compilation 提升到预编译 ahead-of-time compilation , 我们会有更多的驱动优化机会, 与此同时, 运行性能也更可知, 因为大的状态改变, 如切换到另一个图形管线, 是非常明确的.


命令池 command pool 和命令缓冲 command buffer

前面提到过, 执行Vulkan操作时, 如绘制操作, 需要提交到队列. 这些操作在提交前会先被记录到 VkCommandBuffer . 与指定队列族 queue family 关联的 VkCommandPool 负责分配命令缓冲区.

以下是绘制三角形时需要记录在命令缓冲中操作:

  • 开始渲染通道

  • 绑定图形管线

  • 绘制3个顶点

  • 结束渲染通道

由于帧缓冲的图像由交换链提供, 每个可能的图像都有一个记录好的命令缓冲, 绘制时, 才可以选择需要的那个.

另一种方法是每帧都重新记录命令缓冲, 但效率比前者低.


主循环

main loop

到这里, 命令缓冲中已有绘制命令, 主循环则十分简单. 首先, 我们从交换链中获取图像, 保存到 vkAcquireNextImageKHR .

然后, 我们选择与图像对应的命令缓冲, 通过将命令提交到 vkQueueSubmit 执行它们.

最后, 我们将图像返回给交换链, 通过 vkQueuePresentKHR 将图像呈现到屏幕.

提交到队列的操作异步执行. 因此, 我们需要使用同步工具如信号量 semaphore 来确保正确的执行顺序.

执行绘制命令缓冲必须等待图像获取的完成, 否则, 会出现渲染还未读取完成的图像的情况.

渲染完成后, 才能调用 vkQueuePresentKHR . 第二个信号量在渲染完成后发送信号.


总结

到这里, 我们应该对绘制三角形所需的工作有了基本的了解. 实际写程序时, 会包含更多步骤, 比如分配顶点缓冲 vertex buffer , 创建 uniform buffer , 上传纹理图像.

我们会从简单的开始, 因为Vulkan的学习曲线 learning curve 较为陡峭.

一开始, 我们会在顶点着色器 vertex shader 直接给出顶点坐标 vertex coordinate , 而不是使用顶点缓冲 vertex buffer . 因为顶点缓冲要求了解命令缓冲.

简而言之, 绘制三角形需要以下步骤:

  • 创建 VkInstance 实例

  • 选择Vulkan支持的显卡 VkPhysicalDevice

  • 创建绘制 drawing 和呈现 presentation 要使用的 VkDeviceVkQueue

  • 创建窗口, 窗口上层和交换链

  • 将交换链图像纳入 VkImageView

  • 创建渲染通道并指定渲染目标和使用方法

  • 为渲染通道创建帧缓冲

  • 搭建图形管线

  • 为每个可能的交换链图像分配命令缓冲, 并记录绘制命令

  • 绘制帧: 获取图像, 提交相应的绘制命令缓冲, 将图像返回给交换链


API概念

初步讲解Vulkan API的组织方式


命名规范

code convention

所有的Vulkan函数, 枚举和结构体定义在头文件 vulkan.h 中.

函数以 vk 打头.

结构体和枚举类型以 Vk 打头, 枚举值以 VK_ 打头.

API中使用了大量的结构体作为函数参数.

Vulkan中的很多结构体要求在 sType 字段显式给出结构体类型.

pNext 字段指向扩展结构体, 在本教程中始终为 nullptr .

创建/销毁对象的函数拥有 VkAllocationCallbacks 类型参数, 支持自行分配驱动内存, 在本教程中始终为 nullptr .

几乎所有的函数返回枚举类型 VkResult , VK_SUCCESS 表示成功, 其他为错误码 error code . 每个函数可以返回的错误码, 以及对应的含义有规范给出.


示例

 1VkXXXCreateInfo createInfo{};
 2createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
 3createInfo.pNext = nullptr;
 4createInfo.foo = ...;
 5createInfo.bar = ...;
 6
 7VkXXX object;
 8if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) {
 9    std::cerr << "failed to create object" << std::endl;
10    return false;
11}

校验层

validation layer

Vulkan旨在获得高性能并较少受到驱动的辖制. 因此, 其默认提供十分有限的错误检查 error checking 和调试功能 debugging capability .

当执行了不当操作时, 驱动会崩溃而不是返回错误码 error code . 或者更糟糕地, 在某块显卡上可以正确工作, 但在其他显卡不行.

Vulkan允许我们通过校验层使能检查. 校验层代码被插入到API和图形驱动之间, 执行诸如函数参数, 内存管理跟踪问题的检查. 我们可以在开发过程中使能, 发行程序时移除, 从而不添加任何负载.

我们可以自行实现校验层, 不过Vulkan SDK提供了校验层标准, 我们将在本教程中使用该标准.

我们还需要注册回调函数, 以接收校验层的调试信息.


Overview


Khronos Vulkan Tutorial - Overview

本节将介绍Vulkan和其设法解决的问题. 稍后, 我们将专注于绘制三角形的必要组成, 从中也可窥见后续章节的组织方式.

我们会逐一覆盖Vulkan API的组织结构, 并给出通用的使用模板.


Vulkan起源

设计之初, Vulkan旨在作为适用于不同GPU的跨平台抽象接口. 此时的图形硬件受限于固定的可配置功能, 相应地, 这也造就大多数API在设计上的困境. 程序员需要以规范格式提供顶点数据 vertex data , 光照和着色的配置受到GPU生产商的摆布.

随着显卡架构的成熟, 才允许更多可编程功能的出现. 这些新增的功能需与已有API以某种方式整合, 这使得接口抽象的过程让步于现实, 将程序员意图映射到现代图形架构的过程更多地基于对图形驱动的推测. 这是如此多改善甚至大幅提升游戏性能的驱动更新出现的原因. 由于驱动的复杂性, 应用开发者们也许要应对不同供应商之间的不一致, 比如着色器允许的语法.

除去新特性, 移动设备使得大量强有劲的图形硬件出现. 由于移动设备的供电和内存要求, 其GPU的架构不同于主机. 比如分块渲染 tiled render , 程序员在此功能上可以有更多的控制, 以改善性能.

另外, 很多API不一定支持多线程 multi-threading , 这会造成CPU方面的瓶颈.

Vulkan严格按照现代图形学架构设计, 能够解决上述问题. 程序员可以通过复杂性更高的API清楚地描述其意图, 减少来自驱动的辖制; Vulkan支持多线程并行创建和提交指令.

同样, 通过编译器将着色器转换成标准字节码格式, Vulkan解决了不同着色器语法规范不一致的问题.

最后, Vulkan通过将图形和计算功能统一到单个API实现了现代图形显卡均应具有的处理能力.


绘制三角形的必要组成


实例和物理设备的选择

Vulkan应用程序一开始会通过 VkInstance 搭建Vulkan API. 我们需描述应用程序以及所需的API扩展 extension 来创建实例.

创建实例后, 我们可以查询Vulkan支持哪些硬件, 并选择一个或多个 VkPhysicalDevices 在之后的操作中使用.

我们也可以查询属性, 如 VRAM 的大小, 设备功能, 以选择期望的设备, 比如倾向于使用专用图形显卡.


逻辑设备和队列族

选择了正确的硬件设备后, 我们需要创建逻辑设备 VkDevice , 此时需要描述将使用的 VkPhysicalDeviceFeatures , 如多视口渲染 multi viewport rendering 和64位浮点数.

我们还需指定要使用的队列族 Queue family .

我们通过Vulkan执行的操作, 如绘制命令和内存操作, 都是提交到 VkQueue 异步执行. 队列由队列族进行分配, 每个队列族的队列支持特定的一套操作集合.

比如, 可以有相应的队列族用于图形, 计算和内存传输 memory transfer 操作. 当我们选择物理设备时, 也可将队列族的适用性纳入参考.

Vulkan支持的设备可以不提供任何图形功能, 不过, 目前Vulkan所支持的显卡均支持我们所需的所有队列操作.


窗口上层 window surface 和交换链 swap chain

除离屏渲染 offscreen rendering 外, 我们都需要创建窗口以呈现渲染好的图像.

我们可以使用 GLFW 库或 SDL 库来创建窗口. 本教程使用 GLFW .

渲染窗口需要用到两个组件: 窗口上层 VkSurfaceKHR 和交换链 VkSwapchainKHR . KHR 后缀表明组件作为Vulkan扩展的一部分.

Vulkan API本身无关平台, 因此, 与窗口管理的互动由标准化 WSI 扩展完成. Window System Interface

窗口上层是窗口的跨平台抽象,对其实例化将得到窗口句柄 window handle 的引用, 好比Windows系统的 HWND . GLFW 库的内置函数负责处理特定平台的细节.

交换链是渲染目标的集合. 其基本意图为区分正在渲染的图像和已经呈现到屏幕的图像. 我们需要确保只有渲染完成的图像才会显示在屏幕.

每次我们绘制帧之前, 要求交换链提供要渲染的图像; 绘制帧完成后, 再将图像返回给交换链, 在之后的某个时候显示到屏幕上.

渲染目标的个数和将已完成图像显示到屏幕的条件由当前模式 present mode 决定. 常见的当前模式有双缓冲 double buffering vsync 和三缓冲 triple buffering .

有的平台允许直接渲染到显示器, 而无需通过 VK_KHR_displayVK_KHR_display_swapchain 扩展与窗口管理互动. 这种情形下, 我们可以创建代表整个屏幕的窗口上层实例, 在自定义的窗口管理中使用.


图像视图 image view 和帧缓冲 framebuffer

要绘制交换链提供的图像, 我们需将图像纳入 VkImageViewVkFramebuffer .

图像视图引用了将要使用的图像的特定部分, 而帧缓冲引用了要用于颜色, 深度 depth 和模板目标 stencil target 的图像视图.

交换链中有多个不同的图像, 我们会为每个图像预先创建图像视图和帧缓冲, 并在绘制时使用对应的.


渲染通道 render passes

Vulkan中的渲染通道描述了进行渲染操作期间的图像类型, 以及如何使用这些图像类型, 如何处理其内容.

之后的三角形渲染程序中, 我们只有一个图像作为颜色目标 color target , 在绘制操作之前, 被清空为纯色 solid color .

渲染通道仅用来描述图像类型, 而 VkFramebuffer 则将特定图像绑定到插槽 slot .


图形管线 graphics pipeline

Vulkan的图形管线通过 VkPipeline 实例搭建. 该实例描述了显卡的可配置状态, 如视口大小 viewport size , 深度缓冲区 depth buffer , VkShaderModule 实例的可编程状态.

我们使用着色字节码创建 VkShaderModule 实例.

我们通过引用渲染通道指定渲染目标, 而驱动需要知道管线中用到的渲染目标.

和其他API相比, Vulkan的一个显著特点是所有图形管线需要预先配置好. 即, 如果我们想要切换到另一个着色器, 或者稍微改变顶点布局, 我们需要重新创建图形管线. 也就是说, 我们需要为多种渲染操作组合提前创建若干个 VkPipeline 实例.

只有一些基础配置, 如视口大小 viewport size 和清空颜色 clear color , 可以动态改变.

所有状态需要明确描述, 比如, 颜色混合状态 color blend state 没有默认值.

由于这些要求相当于从即时编译 just-in-time compilation 提升到预编译 ahead-of-time compilation , 我们会有更多的驱动优化机会, 与此同时, 运行性能也更可知, 因为大的状态改变, 如切换到另一个图形管线, 是非常明确的.


命令池 command pool 和命令缓冲 command buffer

前面提到过, 执行Vulkan操作时, 如绘制操作, 需要提交到队列. 这些操作在提交前会先被记录到 VkCommandBuffer . 与指定队列族 queue family 关联的 VkCommandPool 负责分配命令缓冲区.

以下是绘制三角形时需要记录在命令缓冲中操作:

  • 开始渲染通道

  • 绑定图形管线

  • 绘制3个顶点

  • 结束渲染通道

由于帧缓冲的图像由交换链提供, 每个可能的图像都有一个记录好的命令缓冲, 绘制时, 才可以选择需要的那个.

另一种方法是每帧都重新记录命令缓冲, 但效率比前者低.


主循环

main loop

到这里, 命令缓冲中已有绘制命令, 主循环则十分简单. 首先, 我们从交换链中获取图像, 保存到 vkAcquireNextImageKHR .

然后, 我们选择与图像对应的命令缓冲, 通过将命令提交到 vkQueueSubmit 执行它们.

最后, 我们将图像返回给交换链, 通过 vkQueuePresentKHR 将图像呈现到屏幕.

提交到队列的操作异步执行. 因此, 我们需要使用同步工具如信号量 semaphore 来确保正确的执行顺序.

执行绘制命令缓冲必须等待图像获取的完成, 否则, 会出现渲染还未读取完成的图像的情况.

渲染完成后, 才能调用 vkQueuePresentKHR . 第二个信号量在渲染完成后发送信号.


总结

到这里, 我们应该对绘制三角形所需的工作有了基本的了解. 实际写程序时, 会包含更多步骤, 比如分配顶点缓冲 vertex buffer , 创建 uniform buffer , 上传纹理图像.

我们会从简单的开始, 因为Vulkan的学习曲线 learning curve 较为陡峭.

一开始, 我们会在顶点着色器 vertex shader 直接给出顶点坐标 vertex coordinate , 而不是使用顶点缓冲 vertex buffer . 因为顶点缓冲要求了解命令缓冲.

简而言之, 绘制三角形需要以下步骤:

  • 创建 VkInstance 实例

  • 选择Vulkan支持的显卡 VkPhysicalDevice

  • 创建绘制 drawing 和呈现 presentation 要使用的 VkDeviceVkQueue

  • 创建窗口, 窗口上层和交换链

  • 将交换链图像纳入 VkImageView

  • 创建渲染通道并指定渲染目标和使用方法

  • 为渲染通道创建帧缓冲

  • 搭建图形管线

  • 为每个可能的交换链图像分配命令缓冲, 并记录绘制命令

  • 绘制帧: 获取图像, 提交相应的绘制命令缓冲, 将图像返回给交换链


API概念

初步讲解Vulkan API的组织方式


命名规范

code convention

所有的Vulkan函数, 枚举和结构体定义在头文件 vulkan.h 中.

函数以 vk 打头.

结构体和枚举类型以 Vk 打头, 枚举值以 VK_ 打头.

API中使用了大量的结构体作为函数参数.

Vulkan中的很多结构体要求在 sType 字段显式给出结构体类型.

pNext 字段指向扩展结构体, 在本教程中始终为 nullptr .

创建/销毁对象的函数拥有 VkAllocationCallbacks 类型参数, 支持自行分配驱动内存, 在本教程中始终为 nullptr .

几乎所有的函数返回枚举类型 VkResult , VK_SUCCESS 表示成功, 其他为错误码 error code . 每个函数可以返回的错误码, 以及对应的含义有规范给出.


示例

 1VkXXXCreateInfo createInfo{};
 2createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
 3createInfo.pNext = nullptr;
 4createInfo.foo = ...;
 5createInfo.bar = ...;
 6
 7VkXXX object;
 8if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) {
 9    std::cerr << "failed to create object" << std::endl;
10    return false;
11}

校验层

validation layer

Vulkan旨在获得高性能并较少受到驱动的辖制. 因此, 其默认提供十分有限的错误检查 error checking 和调试功能 debugging capability .

当执行了不当操作时, 驱动会崩溃而不是返回错误码 error code . 或者更糟糕地, 在某块显卡上可以正确工作, 但在其他显卡不行.

Vulkan允许我们通过校验层使能检查. 校验层代码被插入到API和图形驱动之间, 执行诸如函数参数, 内存管理跟踪问题的检查. 我们可以在开发过程中使能, 发行程序时移除, 从而不添加任何负载.

我们可以自行实现校验层, 不过Vulkan SDK提供了校验层标准, 我们将在本教程中使用该标准.

我们还需要注册回调函数, 以接收校验层的调试信息.