Command Buffers
Creating Command Buffers
创建队列最主要的目的就是代替的应用程序执行任务。一个任务有一系列录制到command buffer中的命令够称。应用程序需要创建包含命令的command buffer然后将命令提交 队列当中去执行。在你开始录制任何命令之前,你需要创建一个command buffer。command buffers不是直接创建的,而是通过command pool分配得来。想创建一个pool,调用接口vkCreateCommandPool。
VkCommandPoolCreateInfo结构中,flags字段决定了pool和其分配的command buffers的行为。flags属于VkCommandPoolCreateFlagBits枚举的成员,包含:
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:表明了从该pool分配到的command buffer都是短周期的,当command buffer使用完毕便会回到pool中。如果你想让command buffer活得长一点,不要设置该位。VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:允许command buffers通过重置(reset)或重新开始(restart)来达到复用的目的。如果没有设置该位,那么只允许pool能够重置,这会隐式的将它分配的所有command buffers重置
以上位的设置都有可能给vulkan带来额外的开销。比如VK_COMMAND_POOL_CREATE_TRANSIENT_BIT可能导致vulkan对pool采用更先进的分配策略,避免因为频繁分配command buffer后,在返回时出现空间碎片。设置VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT会让vulkan追踪每一个command buffer的重置状态。我们可以同时设置这两位。这会给予我们更多的灵活性,不过在批量管理command buffer时会牺牲一些性能。
最后,queueFamilyIndex指出了从该pool分配的command buffers会将命令提交到哪个队列家族当中去。这一步是必要的,因为即便设备上的两个队列拥有相同的性能和指令集,某些特定的命令可能依然会产生区别。
有了command pool,就可以调用接口vkAllocateCommandBuffers来申请command buffers
VkCommandBufferAllocateInfo结构体中,level字段指出了该command buffer的等级。它可以设置为VK_COMMAND_BUFFER_LEVEL_PRIMARY或者VK_COMMAND_BUFFER_LEVEL_SECONDARY。最后,commandBufferCount指出了我们想从pool中申请多少个command buffer。注意到我们没有告诉vulkan关于创建的command buffer任何的大小、长度信息。因为设备内部命令的数据结构差异巨大,任何测量单位比如字节或命令都没有多大意义,所以vulkan将自己管理command buffer的内存。
虽然在释放command pool时,其分配的command buffer资源也会自动释放。但是还是需要注意,当调用vkDestroyCommandPool时,任何command buffer中的命令确保没有正在执行。
Recording Commands
使用vulkan 的命令接口可以将命令录制到command buffer中,所有接口第一个参数都是command buffer句柄。访问command buffer必须是同步的,这意味着两个线程不能同时录制一个command buffer。然而,以下行为是允许的:
- 一个线程可以向多个command buffer录制
- 多个线程可以参与录制一个command buffer,只要保证他们不会同时发生。
vulkan的设计原则之一就是实现高效的多线程任务。所以应用程序的线程最好不要互相阻塞,比如设置锁或者保护临界资源。因此,最好比起多个线程共享command buffers,每个线程最好拥有自己的command buffers。再远一点,我们可以为每个线程创建自己的command pool,这样就允许线程自行在需要的时候创建command buffer而不需要任何交互。
在录制之前,你需要启动一个command buffer,将其重置为初始状态。调用接口vkBeginCommandBuffer。
结构VkCommandBufferBeginInfo中,flags字段指出vulkan如何使用该command buffer,它是一个枚举VkCommandBufferUsageFlagBits的位组合,包含:
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT:command buffer会录制、提交一次,然后就会被销毁或回收。VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT:command buffer会在renderpass 内部使用,并且该位只对secondary command buffer有效。VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT:command buffer可能被执行或待执行超过一次。
设置flags为0是安全的,意味着我们可能执行command buffer超过一次但不是同时执行,而且不需要一个secondary command buffer
pInheritanceInfo字段是在启动一个secondary command buffer时使用的,用来决定一些从调用它的主command buffer的继承状态。
有一件关于vulkan命令需要注意的事情,命令并不是调用后就立即执行的。他们只是简单的加入到command buffer的尾部。如果你向从host可见的内存数据进行复制或写入操作,那么你必须确保:
- 在你执行命令之前,数据存在源位置。
- 在你执行命令之后,数据是可用的。
- 在你执行命令之后,你才能读取复制后的数据。
第一条是最有趣的,因为我们可以在数据还没有到达源位置之前,将命令录制到command buffer中。只要确保命令真正执行的时候,数据在应该的位置就可以了。
在将命令准备好送给设备执行的之后,我们需要告诉vulkan停止录制。调用接口vkEndCommandBuffer。
Recycling Command Buffers
在许多应用程序中,每帧的渲染命令是相似的。因此,你可能重复录制类似的命令一遍又一遍。如果每次你都执行分配操作,然后录制,再将这些command buffers释放掉,将其内存返回给各自的pool中,这将耗费大量的工作。相比之下,使用重置 操作更有效率。这回将command buffer重置到起始状态而不需要和command pool有任何的交互。因此,如果的command buffer动态地从pool中分配资源,这种操作可以暂时保留这些资源,避免重新分配造成的成本。重置command buffer调用接口vkResetCommandBuffer。
接口参数flags指出了重置时的额外操作。唯一的可能是VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT,当设置该位时,该command buffer从command pool分配的资源都会返回到pool中。即使设置了该位,调用reset接口可能仍然比重新分配一个command buffer高效。
可能需要重置pool中的所有command buffer,调用vkResetCommandPool。
接口参数flags唯一的可能是VK_COMMAND_POOL_RESET_RELEASE_RESOURCES_BIT。设置该位时,任何从该pool中动态分配的资源都会释放掉。
从pool中分配的command buffer不会因为重置命令释放掉,而是进入到初始状态就像他们被重新分配一样。重置pool命令通常用在一帧结束,重置所有的command buffer而不是单独的重置。
如果重置command buffer而不把资源返回给pool,应尽量注意需要保持command buffer在多次使用中的复杂一致性。当command buffer增长时,它会动态地从pool中申请资源,然后pool则从系统处申请资源。command buffer消耗的资源数是没有限制的,因为没有硬件限制单个command buffer中录制的命令数量。如果应用程序混合使用非常小和非常大的command buffers,有可能会导致最终所有的command buffers都会像最复杂的command buffer一样大。所以可以定期的使用VK_COMMAND_{BUFFER,POOL}_RESET_RELEASE_RESOURCES_BIT将资源返回给pool。
Submission of Commands
调用接口vkQueueSubmit。
该接口可以向设备一次提交多个command buffer来执行。所有的command buffer都有可以提交到该queue的command pool申请而来(VkCommandPoolCreateInfo中的queueFamilyIndex字段),queue必须是该family中的一员。
每个VkSubmitInfo结构代表多个将要执行的command buffers。每组command buffers可以在开始执行时共同等待一组信号量,也可以在结束执行时向一个或多个信号量发出信号。信号量是一种同步单元,允许不同队列执行的任务被正确安排和协调。
我们想要执行的command buffers以数组形式放置,传递给pCommandBuffers字段,在调用vkQueueSubmit之后,command buffer中的命令会在设备处开始执行。提交给同一设备不同队列的命令可能会并行执行。vkQueueSubmit会在指定的command buffer被安排好后立即返回,可能这时距离他们执行还有很久。
接口参数fence对应一个fence对象,可以用来等待提交的命令执行完毕。栅栏 是另一种类型的同步单元。我们可以等待所有提交到特定队列的任务完成,调用接口vkQueueWaitIdle。
当接口返回时,所有提交到该队列的command buffer都保证完成执行。如果想要等到所有队列中的所有命令完成,调用vkDeviceWaitIdle。
并不推荐调用以上两个接口,他们会完全刷新队列和设备上的任何工作,这些都是重量级的操作。合适的情况是在关闭应用程序或重新初始化应用程序子系统时调用他们,比如线程管理、内存管理等,这些地方很可能需要一个暂停。

浙公网安备 33010602011771号