0%

【Vulkan】多线程渲染

这一节来学习 Vulkan 多线程渲染的设计理念和执行过程,学习过程中可以深入理解 Vulkan 中 Queue 和 Command Buffer 的关系与作用。

1 Vulkan 多线程设计理念

Vulkan 不仅仅是图形 API,而是一个面向图形和计算的编程接口。支持 Vulkan 的设备可以是 GPU,也可以是 DSP 或者固定功能的硬件。

Vulkan 中的计算模型主要基于并行计算,因此支持多线程是 Vulkan 设计的核心理念之一。

为了减少 Vulkan 内部因为互斥同步等操作造成的卡顿问题,Vulkan 内部默认认为对任何资源的访问不存在多线程竞争,所有的资源同步操作由应用开发者去负责,因为对资源的访问和使用没有人比应用开发者自己更加清楚。Vulkan 称之为外部同步(external synchronization)。

因为这个原因,资源管理和线程同步工作成为编写 Vulkan 程序的最大难点之一。想要让 Vulkan 多线程正常运行,你需要做大量的工作。当然,换来的是 Vulkan 有了更加干净的线程模型以及比其它 CG API 高得多的性能。

2 Instances、Devices 和 Queues

Vulkan 多线程模型与几个概念紧密相关,我们再次回顾这些概念:

  • Instances 可以看做是应用的子系统,从逻辑上把 Vulkan 与应用程序上下文中的其他逻辑隔开。Instances 可以看做是 Vulkan 的上下文,它会跟踪所有状态,从逻辑上把所有支持 Vulkan 的设备整合在一起。
  • Physical devicesLogical device 都是 Devices,Physical devices 通常代表一个或者多个支持 Vulkan 的硬件设备,这些设备具有特定功能,可以提供一系列 Queues。图形显卡、加速器、DSP 等都可以是 Vulkan 的 Physical devices。Logical device 是 Physical devices 的软件抽象,用于预订一些硬件资源。
  • Queues 可以理解为一个“GPU 线程”,它是实现 Vulkan 多线程的关键元素之一,用于响应应用的请求,大部分时间,应用都在与其进行交互。

三者之间的层次关系如下图所示:

image-20220717093523311

3 Queues 和 Command Buffer

3.1 Queues

Queue 代表一个 GPU 线程,Vulkan 设备执行的就是提交到 Queues 中的工作。物理设备中 Queue 可能不止一个,每一个 Queue 都被包含在 Queue Families 中。

Queue Families 是一个有相同功能的 Queues 的集合,它们的性能水平和对系统资源的访问是相同的,并且在它们之间数据传输工作没有任何成本(同步之外)。

一个物理设备中可以存在多个 Queue Families,不同的 Queue Families 有不同的特性。相同 Queue Families 中的 Queues 的功能相同,并且可以并行运行。如下图:

image-20220717093746797

按照 Queue 的能力,可以将其划分为:

  • Graphics(图形):该系列中的 Queues 支持图形操作,例如绘制点,线和三角形。

  • Compute(计算):该系列中的 Queues 支持诸如 computer shader 之类的计算操作。

  • Transfer(传输,拷贝):该系列中的 Queues 支持传输操作,例如复制缓冲区和图像内容。

  • Sparse binding(稀疏绑定):该系列中的 Queues 支持用于更新稀疏资源(sparse resource)的内存绑定操作。

3.2 Command Buffer

传统 CG API 是单线程的,性能的提升只能依赖于 CPU 主频的提高。能有的优化方案也不外乎主线程和渲染线程分开,或者某些资源的异步加载、离线处理。

image-20220717094133278

Vulkan 为了充分发挥 CPU 多核多线程的作用,引入了 command buffer 的概念。多个线程可以同时协作,每个 CPU 线程都可以往自己的 command buffer 中提交渲染命令,然后统一提交到对应的 Queue 中,大大提高了 CPU 的利用率。

image-20220717094701486

应用在绘制时会提交一系列绘制命令给 GPU 驱动,但是这些绘制命令不会立刻被执行,而是被简单的添加到 Command Buffer 的末尾。

在其他 CG APIs 中,驱动程序在应用不感知的情况下,把 API 调用翻译成 GPU command 并储存在 command buffer 中,最终提交给 GPU 处理。command buffer 的创建和销毁都由驱动负责。

而在 Vulkan 中,你需要自己从 Command Buffer Pool 中申请 command buffer,将想要记录的命令放入 command buffer 中。

image-20220717094416108

Command Buffer 可以记录(Record)很多命令,比如设置状态、绘制操作、数据拷贝等等,如下图所示:

image-20220717094500235

理论上,一个线程可以把 Command 记录到多个 Command Buffer 中,多个线程也可以共享同一个 Command Buffer,但是一般不鼓励多个线程共享一个 Command Buffer。

Vulkan 的关键设计原则之一就是做到高效的多线程。想实现这一点,应用程序要注意因为资源竞争导致的多线程彼此阻塞。因此,每个线程最好有一个或者多个 Command Buffer,不要尝试共享一个。另外,Command Buffer 由 Command Buffer Pool 分配,应用可以为每一个线程创建一个 Command Buffer Pool,让各个工作线程从 Command Buffer Pool 中分配 Command Buffer,无需参与竞争。

3.3 Command Buffer 生命周期

从 Command Buffer 创建开始,会经历不同的状态,如下图所示:

image-20220717105544433

  • Initial 状态:在 Command-Buffer 刚刚创建时,它就是处于初始化的状态。从此状态,可以达到 Recording 状态,另外,如果重置之后,也会回到该状态。
  • Recording 状态:调用 vkBeginCommandBuffer 方法从 Initial 状态进入到该状态。一旦进入该状态后,就可以调用 vkCmd* 等系列方法记录命令。
  • Executable 状态:调用 vkEndCommandBuffer 方法从 Recording 状态进入到该状态,此状态下,Command-Buffer 可以提交或者重置。
  • Pending 状态:把 Command-Buffer 提交到 Queue 之后,就会进入到该状态。此状态下,物理设备可能正在处理记录的命令,因此不要在此时更改 Command-Buffer,当处理结束后,Command-Buffer 可能会回到 Executable 状态或者 Invalid 状态。
  • Invalid 状态:一些操作会使得 Command-Buffer 进入到此状态,该状态下,Command-Buffer 只能重置、或者释放。

4 Vulkan 同步机制

4.1 显式同步操作

Vulkan 把同步的操作交给了我们的应用程序(external synchronization),绝大多数的 Vulkan 命令根本不提供同步,需要应用自己负责。Vulkan 给应用提供了同步原语,帮助应用进行同步操作。

Vulkan 中主要有四种同步原语(synchronization primitives):

  • Fences:最大颗粒度的同步原语,用来保证物理设备和应用程序之间的同步,比如说向 Queue 中提交了 Command-Buffer 后,具体的执行交由物理设备去完成了,这是一个异步的过程,而应用程序如果要等待执行结束,就要使用 Fence 机制。因此 Fences 给 CPU 端提供了一种方法,可以使其知道 GPU 或者其他 Vulkan Device 什么时候把提交的工作全部做完。

  • Semaphores:颗粒度比 Fences 更小一点,主要是用来向 Queue 中提交 Command-Buffer 时实现同步。比如说某个 Command-Buffer-B 在执行的某个阶段中需要等待另一个 Command-Buffer-A 执行成功后的结果,同时 Command-Buffer-C 在某阶段又要等待 Command-Buffer-B 的执行结果,那么就应该使用 Semaphore 机制实现同步;此时 Command-Buffer-B 提交到 Queue 时就需要两个 VkSemaphor ,一个表示它需要等待的 Semaphore,并且指定在哪个阶段等待;一个是它执行结束后发出通知的 Semaphore

  • Events:颗粒度更小,可以用于 Command Buffer 之间的同步工作

  • Barriers:Vulkan 流水线(Pipeline)阶段内用于内存访问管理和资源状态移动的同步机制

image-20220717100401919

4.2 隐式执行顺序

在没有同步原语的情况下,Vulkan 的执行顺序其实是有一定的潜规则的:

  • Command Buffer 中的 Command,先记录的先执行
  • 先提交的 Command Buffer 先执行
  • 同一个 Queue 中,一起提交的 Command Buffer1 和 Command Buffer2 按照下标的顺序执行,即 Command Buffer1 先执行

4.3 Barriers

Barriers 需要开发者了解渲染管线的各个阶段,能清晰的把握管线中每个步骤对资源的读写顺序。

Vulkan 中将 Pipeline 的各个阶段定义为:

  • TOP_OF_PIPE_BIT
  • DRAW_INDIRECT_BIT
  • VERTEX_INPUT_BIT
  • VERTEX_SHADER_BIT
  • TESSELLATION_CONTROL_SHADER_BIT
  • TESSELLATION_EVALUATION_SHADER_BIT
  • GEOMETRY_SHADER_BIT
  • FRAGMENT_SHADER_BIT
  • EARLY_FRAGMENT_TESTS_BIT
  • LATE_FRAGMENT_TESTS_BIT
  • COLOR_ATTACHMENT_OUTPUT_BIT
  • TRANSFER_BIT
  • COMPUTE_SHADER_BIT
  • BOTTOM_OF_PIPE_BIT

对应于管线流程图:

image-20220717100954068

假设我们有个两个渲染管线 P1 和 P2,P1 会通过 Vertex Shader 往 buffer 写入顶点数据,P2 需要在 Compute Shader 中使用这些数据。

如果使用 fence 去同步,我们的流程应该是这样:P1 的 Command 提交后,P2 通过 fence 确保 P1 的操作已经被全部执行完,再开始工作。

image-20220717101041745

但是这种大颗粒度的同步操作无疑造成了耗时操作:P1 的数据在 Vertex Shader 阶段就已经准备好了,我们为什么要等到它所有操作执行完再开始?P2 平白多等待了很长时间,而且在这个期间 P2 的其他阶段并没有使用到 P1 的数据,也是可以同步执行的。

Barriers 的引入完全解决了这个问题,我们只需要告诉 Vulkan,我们在 P2 的 Compute Shader 阶段才会等待 P1 Vertex Shader 里面的数据,其他阶段并不关心,可以同步进行。

image-20220717101122332

5 具体用法

接下来总结上述 4 种组件的具体用法。首先回顾 Vulkan 中重要的组件及其工作流程:

vulkan-instance-device-queue

在 Vulkan 的 API 中有一些固定的调用套路 :

  1. 要创建某个对象,先提供一个包含创建信息的对象。
  2. 创建时通过传递引用的方式来传参。

5.1 Instance 组件

vkCreateInstance 函数中有个名为 VkInstanceCreateInfo 类型的参数,这就是包含了 VKInstance 要创建的信息:

1
2
3
4
5
6
7
8
9
10
typedef struct VkInstanceCreateInfo {
VkStructureType sType; // 一般为方法对应的类型
const void* pNext; // 一般为 null 就好了
VkInstanceCreateFlags flags; // 留着以后用的,设为 0 就好了
const VkApplicationInfo* pApplicationInfo; // 对应新的一个结构体 VkApplicationInfo
uint32_t enabledLayerCount; // layer 和 extension 用于调试和拓展
const char* const* ppEnabledLayerNames;
uint32_t enabledExtensionCount;
const char* const* ppEnabledExtensionNames;
} VkInstanceCreateInfo;

除此之外还需要创建一个 VkApplicationInfo 对象:

1
2
3
4
5
6
7
8
9
typedef struct VkApplicationInfo {
VkStructureType sType;
const void* pNext;
const char* pApplicationName;
uint32_t applicationVersion;
const char* pEngineName;
uint32_t engineVersion;
uint32_t apiVersion;
} VkApplicationInfo;

下面是创建一个 instance 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VkApplicationInfo app_info = {};

app_info.apiVersion = VK_API_VERSION_1_0;
app_info.applicationVersion = 1;
app_info.engineVersion = 1;
app_info.pNext = nullptr;
app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app_info.pEngineName = APPLICATION_NAME;
app_info.pApplicationName = APPLICATION_NAME;

VkInstanceCreateInfo instance_info = {};
// type 就是结构体的类型
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.pNext = nullptr;
instance_info.pApplicationInfo = &app_info;
instance_info.flags = 0;
// Extension and Layer 暂时不用,可空
instance_info.enabledExtensionCount = 0;
instance_info.ppEnabledExtensionNames = nullptr;
instance_info.ppEnabledLayerNames = nullptr;
instance_info.enabledLayerCount = 0;

VkResult result = vkCreateInstance(&instance_info, nullptr, &instance);

当每调用一个创建函数后,返回的类型都是 VkResult ,只要 VkResult 大于 0 ,那么执行就是成功的。

5.2 Device 组件

有了 Instance 组件,就可以创建 Device 组件了,按照调用的套路,肯定还会有一个 VkDeviceCreateInfo 的结构体表示 Device 的创建信息。

Device 具体指的是逻辑上的设备,可以说是对物理设备的一个逻辑上的封装,而物理设备就是 VkPhysicalDevice 对象。

在某些情况下,可能会具有多个物理设备,因此要先枚举一下所有的物理设备:

1
2
3
uint32_t gpu_size = 0;
// 第一次调用只为了获得个数
VkResult res = vkEnumeratePhysicalDevices(instance, &gpu_size, nullptr);

vkEnumeratePhysicalDevices 方法中,传入的第二个参数为 GPU 的个数,第三个参数为 null,这样的一次调用会返回 GPU 的个数到 gpu_size 变量。

1
2
3
4
vector<VkPhysicalDevice> gpus;
gpus.resize(gpu_size);
// 第二次调用获得所有的数据,vector.data() 方法转换成指针类型
res = vkEnumeratePhysicalDevices(instance, &gpu_size, gpus.data());

当再一次调用 vkEnumeratePhysicalDevices 函数时,第三个参数不为 null,而是相应的 VkPhysicalDevice 容器,那么 gpus 会填充 gpu_size 个的 VkPhysicalDevice 对象。

有了 VkPhysicalDevice 对象之后,可以查询 VkPhysicalDevice 上的一些属性,以下函数都可以查询相关信息:

  • vkGetPhysicalDeviceQueueFamilyProperties
  • vkGetPhysicalDeviceMemoryProperties
  • vkGetPhysicalDeviceProperties
  • vkGetPhysicalDeviceImageFormatProperties
  • vkGetPhysicalDeviceFormatProperties

QueueFamilyProperties 为例,获得该属性的方法调用方式和获得 VkPhysicalDevice 数据方式一样,也是一个两次调用:

1
2
3
4
5
6
7
8
9
10
// 第一次调用,获得个数
uint32_t queue_family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(gpus[0], &queue_family_count, nullptr);
assert(queue_family_count != 0);

// 第二次调用,获得实际数据
vector<VkQueueFamilyProperties> queue_family_props;
queue_family_props.resize(queue_family_count);
vkGetPhysicalDeviceQueueFamilyProperties(gpus[0], &queue_family_count, queue_family_props.data());
assert(queue_family_count != 0);

QueueFamilyProperties 的结构体含义如下:

1
2
3
4
5
6
typedef struct VkQueueFamilyProperties {
VkQueueFlags queueFlags; // 标识位:表示 Queue 的功能
uint32_t queueCount;
uint32_t timestampValidBits;
VkExtent3D minImageTransferGranularity;
} VkQueueFamilyProperties;

其中的 queueFlags 表示该 Queue 的能力,有的 Queue 是用来渲染图像的,还有的 Queue 是用来计算的,具体的 Flag 标识如下:

1
2
3
4
5
6
7
8
typedef enum VkQueueFlagBits {
VK_QUEUE_GRAPHICS_BIT = 0x00000001, // 图像相关
VK_QUEUE_COMPUTE_BIT = 0x00000002, // 计算相关
VK_QUEUE_TRANSFER_BIT = 0x00000004, // 传输,拷贝
VK_QUEUE_SPARSE_BINDING_BIT = 0x00000008, // 稀疏绑定
VK_QUEUE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkQueueFlagBits;
typedef VkFlags VkQueueFlags;

接下来是创建一个 Device,在 VkDeviceCreateInfo 结构体中需要一个参数是 VkDeviceQueueCreateInfo ,因此要先创建 VkDeviceQueueCreateInfo,再创建 VkDeviceCreateInfo,最后调用 vkCreateDevice 创建一个 Device:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 创建 Queue 所需的相关信息
VkDeviceQueueCreateInfo queue_info = {};
// 找到属性为 VK_QUEUE_GRAPHICS_BIT 的索引
bool found = false;
for (unsigned int i = 0; i < queue_family_count; ++i) {
if (queue_family_props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
queue_info.queueFamilyIndex = i;
found = true;
break;
}
}

float queue_priorities[1] = {0.0};
// 结构体的类型
queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info.pNext = nullptr;
queue_info.queueCount = 1;
// Queue 的优先级
queue_info.pQueuePriorities = queue_priorities;

// 创建 Device 所需的相关信息类
VkDeviceCreateInfo device_info = {};

device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
device_info.pNext = nullptr;
// Device 所需的 Queue 相关信息
device_info.queueCreateInfoCount = 1; // Queue 个数
device_info.pQueueCreateInfos = &queue_info; // Queue 相关信息
// Layer 和 Extension 暂时为空,不影响运行,后续再补上
device_info.enabledExtensionCount = 0;
device_info.ppEnabledExtensionNames = NULL;
device_info.enabledLayerCount = 0;
device_info.ppEnabledLayerNames = NULL;
device_info.pEnabledFeatures = NULL;

VkResult res = vkCreateDevice(gpus[0], &device_info, nullptr, &device);

5.3 Queue 组件

完成了 Device 创建之后,Queue 的创建也简单多了,直接调用如下函数就好了:

1
2
3
4
5
6
7
8
typedef void (VKAPI_PTR *PFN_vkGetDeviceQueue)
(VkDevice device, // 创建的 Device 对象
uint32_t queueFamilyIndex, // queueFlags 为 VK_QUEUE_GRAPHICS_BIT 的索引
uint32_t queueIndex,
VkQueue* pQueue); // 要创建的 Queue

// 代码示例
vkGetDeviceQueue(info.device, info.graphics_queue_family_index, 0, &info.queue);

完成了 InstanceDeviceQueue 组件的创建之后,还有一件要做的事情就是释放它们,销毁组件。

按照先进后出的方式进行销毁,Instance 最先创建因此最后销毁,和 Device 相关联的 QueueDevice 销毁时就随之销毁了。

1
2
3
4
// 销毁 Device
vkDestroyDevice(info.device, nullptr);
// 销毁 Instance
vkDestroyInstance(info.instance, nullptr);

5.4 Command Buffer 组件

在前面的学习中,我们已经创建了 InstanceDeviceQueue 三个组件,并且知道了 Queue 组件是用来和物理设备沟通的桥梁,而具体的沟通过程就需要 Command-Buffer 组件,它是若干命令的集合,我们向 Queue 提交 Command-Buffer,然后才交由物理设备 GPU 进行处理。

5.4.1 创建 Command Pool

在创建 Command-Buffer 之前,需要创建 Command-Pool 组件,从 Command-Pool 中去分配 Command-Buffer 。还是老套路,我们需要先创建一个 VkCommandPoolCreateInfo 的结构体,结构体每个参数的释义还是要多参考官方的文档。

1
2
3
4
5
6
7
8
9
10
// 创建 Command-Pool 组件
VkCommandPool command_pool;
VkCommandPoolCreateInfo poolCreateInfo = {};
poolCreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
// 可以看到 Command-Pool 还和 Queue 相关联
poolCreateInfo.queueFamilyIndex = info.graphics_queue_family_index;
// 标识命令缓冲区的一些行为
poolCreateInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
// 具体创建函数的调用
vkCreateCommandPool(info.device, &poolCreateInfo, nullptr, &command_pool);

有几个参数需要注意:

  1. queueFamilyIndex 参数表示创建 Queue 时选择的那个 queueFlagsVK_QUEUE_GRAPHICS_BIT 的索引,从 Command-Pool 中分配的的 Command-Buffer 必须提交到同一个 Queue 中。
  2. flags 有如下的选项,分别指定了 Command-Buffer 的不同特性:
1
2
3
4
5
typedef enum VkCommandPoolCreateFlagBits {
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT = 0x00000001,
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT = 0x00000002,
VK_COMMAND_POOL_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkCommandPoolCreateFlagBits;
  • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:表示该 Command-Buffer 的寿命很短,可能在短时间内被重置或释放
  • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:表示从 Command-Pool 中分配的 Command-Buffer 可以通过 vkResetCommandBuffer 或者 vkBeginCommandBuffer 方法进行重置,如果没有设置该标识位,就不能调用 vkResetCommandBuffer 方法进行重置。
5.4.2 创建 Command Buffer

接下来就是从 Command-Pool 中分配 Command-Buffer,通过 VkCommandBufferAllocateInfo 函数。首先需要一个 VkCommandBufferAllocateInfo 结构体表示分配所需要的信息:

1
2
3
4
5
6
7
typedef struct VkCommandBufferAllocateInfo {
VkStructureType sType;
const void* pNext;
VkCommandPool commandPool; // 对应上面创建的 command-pool
VkCommandBufferLevel level;
uint32_t commandBufferCount; // 创建的个数
} VkCommandBufferAllocateInfo;

这里有个参数也要注意:

  • VkCommandBufferLevel 指定 Command-Buffer 的级别。

有如下级别可以使用:

1
2
3
4
5
6
7
8
typedef enum VkCommandBufferLevel {
VK_COMMAND_BUFFER_LEVEL_PRIMARY = 0,
VK_COMMAND_BUFFER_LEVEL_SECONDARY = 1,
VK_COMMAND_BUFFER_LEVEL_BEGIN_RANGE = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
VK_COMMAND_BUFFER_LEVEL_END_RANGE = VK_COMMAND_BUFFER_LEVEL_SECONDARY,
VK_COMMAND_BUFFER_LEVEL_RANGE_SIZE = (VK_COMMAND_BUFFER_LEVEL_SECONDARY - VK_COMMAND_BUFFER_LEVEL_PRIMARY + 1),
VK_COMMAND_BUFFER_LEVEL_MAX_ENUM = 0x7FFFFFFF
} VkCommandBufferLevel;

一般来说,使用 VK_COMMAND_BUFFER_LEVEL_PRIMARY 就好了。

具体创建代码如下:

1
2
3
4
5
6
7
VkCommandBuffer commandBuffer[2];
VkCommandBufferAllocateInfo command_buffer_allocate_info{};
command_buffer_allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
command_buffer_allocate_info.commandPool = command_pool;
command_buffer_allocate_info.commandBufferCount = 2;
command_buffer_allocate_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
vkAllocateCommandBuffers(info.device, &command_buffer_allocate_info, commandBuffer);
5.4.3 Command Buffer 记录与提交命令

回顾上面的 Command Buffer 记录命令流程图:

image-20220717094500235

vkBeginCommandBuffervkEndCommandBuffer 方法之间可以记录和渲染相关的命令,我们先不考虑中间的过程,直接创建提交。

首先,还是需要创建一个 VkCommandBufferBeginInfo 结构体用来表示 Command-Buffer 开始的信息:

1
2
3
4
VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(commandBuffer[0], &beginInfo);

这里要注意的参数是 flags ,表示 Command-Buffer 的用途:

1
2
3
4
5
6
typedef enum VkCommandBufferUsageFlagBits {
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT = 0x00000001,
VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT = 0x00000002,
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT = 0x00000004,
VK_COMMAND_BUFFER_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkCommandBufferUsageFlagBits;

我们用的 VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT 表示该 Command-Buffer 只使用提交一次,用完之后就会被重置,并且每次提交时都需要重新记录。

直接调用 vkEndCommandBuffer 方法就可以结束记录,此时就可以提交了:

1
vkEndCommandBuffer(commandBuffer[0]);

接下来通过 vkQueueSubmit 方法将 Command-Buffer 提交到 Queue 上。同样的还是需要创建一个 VkSubmitInfo 结构体:

1
2
3
4
5
6
7
8
9
10
11
typedef struct VkSubmitInfo {
VkStructureType sType;
const void* pNext;
uint32_t waitSemaphoreCount; // 等待的 Semaphore 数量
const VkSemaphore* pWaitSemaphores; // 等待的 Semaphore 数组指针
const VkPipelineStageFlags* pWaitDstStageMask; // 在哪个阶段进行等待
uint32_t commandBufferCount; // 提交的 Command-Buffer 数量
const VkCommandBuffer* pCommandBuffers; // 具体的 Command-Buffer 数组指针
uint32_t signalSemaphoreCount; //执行结束后通知的 Semaphore 数量
const VkSemaphore* pSignalSemaphores; //执行结束后通知的 Semaphore 数组指针
} VkSubmitInfo;

它的参数比较多,并且涉及到 Command-Buffer 之间的同步关系了,上面已经提到过 Semaphore 和 Fence 的相关内容。

如果只是简单的提交 Command-Buffer,那就不需要考虑 Semaphore 这些同步机制了,把相应的参数都设置为 nullptr,或者直接不设置也行,最后提交就好了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 简单的提交过程
// 开始记录
VkCommandBufferBeginInfo beginInfo1 = {};
beginInfo1.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo1.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(commandBuffer[0], &beginInfo1);

...
// 省略中间的 vkCmdXXXX 系列方法

// 结束记录
vkEndCommandBuffer(commandBuffer[0]);

VkSubmitInfo submitInfo1 = {};
submitInfo1.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
// pWaitSemaphores 和 pSignalSemaphores 都不设置,只是提交
submitInfo1.commandBufferCount = 1;
submitInfo1.pCommandBuffers = &commandBuffer[0];

// 注意最后的参数 临时设置为 VK_NULL_HANDLE,也可以设置为 Fence 来同步
vkQueueSubmit(info.queue, 1, &submitInfo1, VK_NULL_HANDLE);

以上就完成了 Command-Buffer 提交到 Queue 的过程,省略了 SemaphoresFences 的同步机制,当然也可以把它们加上。

我们在 vkQueueSubmit 的最后一个参数设置为了 VK_NULL_HANDLE ,这是 Vulkan 中设置为 NULL 的一个方法(其实是设置了一个整数 0 ),也可以设置 Fence ,表示我们要等待该 Command-BufferQueue 执行结束,当 vkQueueSubmit 的最后参数传入 Fence 后,就可以通过 Fence 等待该 Command-Buffer 执行结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建 Fence
VkFence drawFence;
VkFenceCreateInfo fenceCreateInfo = {};
fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
// 该参数表示 Fence 的状态,如果不设置或者为 0 表示 unsignaled state
fence_info.flags = 0;
vkCreateFence(info.device, &fenceCreateInfo, nullptr, &drawFence);

...

// 最后的参数设置为 Fence 来同步
vkQueueSubmit(info.queue, 1, &submitInfo1, &drawFence);

// 等待 Command buffer 执行结束
VkResult res;
do {
res = vkWaitForFences(info.device, 1, &drawFence, VK_TRUE, UINT64_MAX);
} while (res == VK_TIMEOUT);
---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。