Command Buffers and Synchronization

Command Buffers and Synchronization

[TOC]

内容

Creating a command pool
Allocating command buffers
Beginning a command buffer recording operation
Ending a command buffer recording operation
Resetting a command buffer
Resetting a command pool
Creating a semaphore
Creating a fence
Waiting for fences
Resetting fences
Submitting command buffers to a queue
Synchronizing two command buffers
Checking if processing of a submitted command buffer has finished
Waiting until all commands submitted to a queue are finished
Waiting for all submitted commands to be finished
Destroying a fence
Destroying a semaphore
Freeing command buffers
Destroying a command pool

Vulkan的low-level APIs给予了我们队硬件更多的控制权.包括对资源的创建、管理、操作,以及与硬件的交互.command buffer是最重要的object.它允许我们记录操作并提交给硬件执行.更重要的是能在多线程中记录他们,而且是由驱动隐形记录并不需要开发者控制它们.vulkan允许我们重复使用已经存在的command buffers.给予了便利的同时也让开发者有更多的责任.

为此需要格外小心命令的提交,尤其要处理提交command buffer到GPU时如何同步,为此引入了semaphores和fences.

本节讨论command buffers的allocate、record、submit,如何创建synchronizatin primitives并使用它们控制提交操作,如何同步GPU上的command buffers,如何同步app和硬件.

command pool

创建command pool

command pool是command buffer申请缓存的地方.缓存是隐性和动态创建的,但如果没有它command buffer就没地方记录commands了.所以先创建command pool

1
2
3
4
5
6
VkCommandPoolCreateInfo command_pool_create_info = {
VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
nullptr,
parameters,
queue_family
};

VK_COMMAND_POOL_CREATE_TRANSIENT_BIT 从pool里申请一个cb,存在非常短的时间.

VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,能立即重置cb.如果没有这个flag,我们只能通过group进行操作(pool的cbs一起).如果没有这个flag,只能记录一次cb.如果想再次记录,需要重置整个pool.

command pools控制command buffers提交的队列.这二十用过queue family index实现,必须在创建Pool时提供.特定pool提供的cb只能提交给特定的family.

1
2
3
4
5
6
7
VkResult result = vkCreateCommandPool( logical_device,
&command_pool_create_info, nullptr, &command_pool );
if( VK_SUCCESS != result ) {
std::cout << "Could not create command pool." << std::endl;
return false;
}
return true;

command pools在多线程里不能同时被访问.这就是为什么将在其上记录命令缓冲区的每个应用程序线程都应该使用单独的command pools的原因.

command buffer

allocate command buffers

command buffers从command pools进行aloocate,这运行我们控制整个groups的一些属性.

  • 只能提交到当创建command pool时选择的family的queue里.
  • command pools不能异步使用,所以需要为每个线程创建自己的command pools,减少同步消耗,提升性能

command buffers也有自己的属性,一些是记录操作时指定,但在allocate command buffer时有一些重要参数,是否想allocate主或次command buffers:

  • primary command buffers能直接提交到queues.也能执行(call)次command buffers.
  • 次command buffers只能通过主command buffers执行.不能提交.

这些参数通过VkCommandBufferAllocateInfo指定

1
2
3
4
5
6
7
VkCommandBufferAllocateInfo command_buffer_allocate_info = {
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
nullptr,
command_pool,
level,
count
};

allocate

1
2
3
4
5
6
7
8
command_buffers.resize( count );
VkResult result = vkAllocateCommandBuffers( logical_device,
&command_buffer_allocate_info, &command_buffers[0] );
if( VK_SUCCESS != result ) {
std::cout << "Could not allocate command buffers." << std::endl;
return false;
}
return true;

开始command buffer record操作

当想用硬件执行操作时,需要记录它们并提交给queue.命令记录在command buffers里.当我们记录它们时,我们需要选择一个command buffer.

vulkan里能做的最重要的事情就是记录command buffers了.也是告诉硬件做什么怎么做的唯一方式.当开始record command buffers时,它们的状态时无定义的.通常,command buffer不继承任何状态.当我们记录操作时,我们需要记住设置与这些操作相关的状态.比如drawing command,使用vertex attributes和indices.在我们记录一次drawing操作前,需要用顶点数据和索引数据绑定到各自适当的缓冲区.

主command buffers能call(执行)记录在次command buffers里的命令.执行次command buffers不继承主command buffers的状态.主command buffer的状态在执行次command buffer后也是未定义的(当我们record 一个主command buffer,并且执行了它的一个次command buffer后,我们如果想继续record主command buffer,需要重新设置状态).只有一个意外的state继承规则–当主command buffer在一个render pass里,我们执行一个它的次command buffer,主command buffer的render pass和subpass states被保持着.

1
2
3
4
5
6
VkCommandBufferBeginInfo command_buffer_begin_info = {
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
nullptr,
usage,
secondary_command_buffer_info
};

为了性能考虑,需要避免command buffer使用下列标记

1
K_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT

开始记录

1
2
3
4
5
6
7
8
VkResult result = vkBeginCommandBuffer( command_buffer,
&command_buffer_begin_info );
if( VK_SUCCESS != result ) {
std::cout << "Could not begin command buffer recording operation." <<
std::endl;
return false;
}
return true;

现在可以向command buffer选择操作了.但我们如何知道哪些操作可以记录到command buffer里.这类函数的名字以vkCmd开头,第一个参数全是VkCommandBuffer.但需要记住不是所有命令都能给主和次command buffers使用.

结束command buffer recording operation

当我们不想记录更多commands到command buffer,我们需要停止recording它.

为了更快记录和更小影响性能,记录commands时不会有任何报错,所有错误都在vkEndCommandBuffer函数汇报.

所以停止record时需要判断record是否成功

1
2
3
4
5
6
7
VkResult result = vkEndCommandBuffer( command_buffer );
if( VK_SUCCESS != result ) {
std::cout << "Error occurred during command buffer recording." <<
std::endl;
return false;
}
return true;

如果报错,就不能submit它,而需要重置它.

重置command buffer

如果一个command buffer报错了,必须reset才能重新record.可以通过开始另一个record操作来隐性重置它,也可以显示做到这点.

可以通过重置整个command pool来重置command buffers.也可以独立进行.只有在使用K_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT标志创建了用于分配命令缓冲区的池时,才能执行单独的重置.

显示重置让我们控制command buffer从创建它的pool里申请欢成.在显示重置时,我们能决定是否向归还缓存给pool,或者是否command buffer需要继续保留并在下次command record时复用它.

1
2
3
4
5
6
7
VkResult result = vkResetCommandBuffer( command_buffer, release_resources ?
VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT : 0 );
if( VK_SUCCESS != result ) {
std::cout << "Error occurred during command buffer reset." << std::endl;
return false;
}
return true;

重置command pool

当我们不想单独重置command buffer或者如果我们创建的pool没有标记VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,我们能一次重置pool的所有command buffer.

重置command pool会导致所有由它创建的command buffers回到初始状态.这和单独重置所有command buffers一样,但更快速.并且没有指定VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT标记.

当command buffers被record,它们从pool里得到缓存,这被自动执行.当重置command buffer时,可以选择是否command buffers保留它们的缓存以备后用,或者返回给pool.

1
2
3
4
5
6
7
VkResult result = vkResetCommandPool( logical_device, command_pool,
release_resources ? VK_COMMAND_POOL_RESET_RELEASE_RESOURCES_BIT : 0 );
if( VK_SUCCESS != result ) {
std::cout << "Error occurred during command pool reset." << std::endl;
return false;
}
return true;

semaphore 和 fence

创建semaphore

在提交commands和利用(utilize)设备的处理能力前,我们需要知道如何同步操作.semaphores时用于同步的原语(primitives).它们允许我们不仅在一个队列中而且在一个逻辑设备中的不同队列之间协调提交到队列的操作.

semaphores当我们提交commands给queus使用.所以在我们使用它们前需要创建.

semaphores由两个状态:signaled或者unsignaled.semaphores在command buffer提交时使用.我们将它们提供给一个要发出信号的信号量列表时,一旦给定批中提交的所有工作完成,它们就会将状态更改为signaled.相同方式,当我们提交commands给queues,我们能指定提交的commands进入等待状态直到特定list里的所有semaphores变为signaled.通过这种方式,我们可以协调提交到队列的工作,并推迟对依赖于其他命令结果的命令的处理.

当所有semaphores处于signaled且所有等待它们的命令恢复(变为un-signaled)然后复用.

semaphores也用于从swapchain请求images,此时,semaphores必须当我们向相关的images提交commands时使用.这些commands必须等待知道images不再被presentation engine使用.

vkCreateSemaphore

1
2
3
4
5
6
7
8
9
10
11
12
VkSemaphoreCreateInfo semaphore_create_info = {
VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
nullptr,
0
};
VkResult result = vkCreateSemaphore( logical_device,
&semaphore_create_info, nullptr, &semaphore );
if( VK_SUCCESS != result ) {
std::cout << "Could not create a semaphore." << std::endl;
return false;
}
return true;

semaphores只能用于同步提交给queues的工作,位于硬件核心.app不能获得semaphore的状态,如果app需要同步提交了的commands需要使用fences.

创建fences

fences是用来同步app和提交给图形硬件的commands的.

1
2
3
4
5
6
7
8
9
10
11
12
VkFenceCreateInfo fence_create_info = {
VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
nullptr,
signaled ? VK_FENCE_CREATE_SIGNALED_BIT : 0
};
VkResult result = vkCreateFence( logical_device, &fence_create_info,
nullptr, &fence );
if( VK_SUCCESS != result ) {
std::cout << "Could not create a fence." << std::endl;
return false;
}
return true;

wait fences

vkWaitForFences

1
2
3
4
5
6
7
8
9
10
if( fences.size() > 0 ) {
VkResult result = vkWaitForFences( logical_device,
static_cast<uint32_t>(fences.size()), &fences[0], wait_for_all, timeout );
if( VK_SUCCESS != result ) {
std::cout << "Waiting on fence failed." << std::endl;
return false;
}
return true;
}
return false;

reset fences

semaphores自动reset,但当fence标记为signaled后,app负责reset.

1
2
3
4
5
6
7
8
9
10
if( fences.size() > 0 ) {
VkResult result = vkResetFences( logical_device,
static_cast<uint32_t>(fences.size()), &fences[0] );
if( VK_SUCCESS != result ) {
std::cout << "Error occurred when tried to reset fences." << std::endl;
return false;
}
return VK_SUCCESS == result;
}
return false;

submit

submit command buffers to a queue

1
2
3
4
struct WaitSemaphoreInfo {
VkSemaphore Semaphore;
VkPipelineStageFlags WaitingStage;
};
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
std::vector<VkSemaphore> wait_semaphore_handles;
std::vector<VkPipelineStageFlags> wait_semaphore_stages;
for( auto & wait_semaphore_info : wait_semaphore_infos ) {
wait_semaphore_handles.emplace_back( wait_semaphore_info.Semaphore );
wait_semaphore_stages.emplace_back( wait_semaphore_info.WaitingStage );
}
VkSubmitInfo submit_info = {
VK_STRUCTURE_TYPE_SUBMIT_INFO,
nullptr,
static_cast<uint32_t>(wait_semaphore_infos.size()),
wait_semaphore_handles.size() > 0 ? &wait_semaphore_handles[0] : nullptr,
wait_semaphore_stages.size() > 0 ? &wait_semaphore_stages[0] : nullptr,
static_cast<uint32_t>(command_buffers.size()),
command_buffers.size() > 0 ? &command_buffers[0] : nullptr,
static_cast<uint32_t>(signal_semaphores.size()),
signal_semaphores.size() > 0 ? &signal_semaphores[0] : nullptr
};


VkResult result = vkQueueSubmit( queue, 1, &submit_info, fence );
if( VK_SUCCESS != result ) {
std::cout << "Error occurred during command buffer submission." <<
std::endl;
return false;
}
return true;

出于性能考虑,需要尽可能在少的dc中提交更多的batches

当一个cb提交后还没执行完,不能再次提交,除非使用flag:VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT,但为了性能考虑要尽量避免这个flag.

同步两个cb

semaphores

1
2
3
4
struct WaitSemaphoreInfo {
VkSemaphore Semaphore;
VkPipelineStageFlags WaitingStage;
};
1
2
3
4
5
6
7
8
std::vector<VkSemaphore> first_signal_semaphores;
for( auto & semaphore_info : synchronizing_semaphores ) {
first_signal_semaphores.emplace_back( semaphore_info.Semaphore );
}
if( !SubmitCommandBuffersToQueue( first_queue, first_wait_semaphore_infos,
first_command_buffers, first_signal_semaphores, VK_NULL_HANDLE ) ) {
return false;
}

这里同时使用handles和pipeline stages,第二个batch会等待特定pipeline stages的所有semaphores,这意味着提交的cbs部分会开始执行,当到达提供的stages时会暂停

1
2
3
4
5
if( !SubmitCommandBuffersToQueue( second_queue, synchronizing_semaphores,
second_command_buffers, second_signal_semaphores, second_fence ) ) {
return false;
}
return true;

这显示了如何同步从同一逻辑设备提交到不同队列的多个命令缓冲区的工作.从第二次提交开始的命令缓冲区处理将被推迟,直到第一批中的所有命令完成.

检查提交后的cb是否完成

当使用semaphores时,app没有被cb的同步给牵制.

当我们想知道cb何时结束,我们需要使用fences

创建fence,准备cb,提交给queue,记住提交时使用的fence

1
2
3
4
if( !SubmitCommandBuffersToQueue( queue, wait_semaphore_infos,
command_buffers, signal_semaphores, fence ) ) {
return false;
}

然后等待

1
return WaitForFences( logical_device, { fence }, VK_FALSE, timeout );

这样可以确保cb成功执行才开始接下来的任务.

但是,通常,渲染不应该造成app完全暂停.我们需要检查fence是否signaled,如果没有,就继续执行其他任务,比如ai、物理.当fence signaled,然后执行需要以来提交的commands状态的任务.

fences在我们想reuse cb时也有用,在我们re-cecord它们前,必须确保不再被device执行.我们需要有一定数量的command buffers 一个接一个 记录和提交.只有这样,我们使用其中一个时,就启动一个fence(每个submitted batch需要有一个关联的fence).分批的cb越多,花费在等待fences上的时间越少.(但效率不一定)

等待queue的所有commands结束

并不总希望使用fences,app等待选择的queue执行结束也是可行的.

vkQueueWaitIdle

但这种同步在非常罕见的情况下执行.GPU比CPU快且可能需要不断提交工作以充分利用申请的性能.

在应用程序端执行等待可能会在图形硬件的管道中造成停顿,从而导致设备的利用效率低下

1
2
3
4
5
6
7
VkResult result = vkQueueWaitIdle( queue );
if( VK_SUCCESS != result ) {
std::cout << "Waiting for all operations submitted to queue failed." <<
std::endl;
return false;
}
return true;

等待提交的commands结束

这种等待一般要关闭app和想要销毁创建的资源时用到

1
2
3
4
5
6
VkResult result = vkDeviceWaitIdle( logical_device );
if( VK_SUCCESS != result ) {
std::cout << "Waiting on a device failed." << std::endl;
return false;
}
return true;

destroy

1
2
3
4
if( VK_NULL_HANDLE != fence ) {
vkDestroyFence( logical_device, fence, nullptr );
fence = VK_NULL_HANDLE;
}
1
2
3
4
if( VK_NULL_HANDLE != semaphore ) {
vkDestroySemaphore( logical_device, semaphore, nullptr );
semaphore = VK_NULL_HANDLE;
}
1
2
3
4
5
if( command_buffers.size() > 0 ) {
vkFreeCommandBuffers( logical_device, command_pool,
static_cast<uint32_t>(command_buffers.size()), &command_buffers[0] );
command_buffers.clear();
}
1
2
3
4
if( VK_NULL_HANDLE != command_pool ) {
vkDestroyCommandPool( logical_device, command_pool, nullptr );
command_pool = VK_NULL_HANDLE;
}