第三章 队列和命令 第3节 设备队列
你将在本章中学到
l 队列是什么,如何使用它
l 如何创建命令并把它们发送给Vulkan
l 如何保证设备已完成任务
Vulkan设备对外暴露多个队列来完成任务。在本章,我们讨论多种队列类型并详解如何以命令buffer的形式向他们提交任务。我们也演示如何指示一个队列完成你发送给它的任务。
设备队列
Vulkan中每一个队列都有一个或多个队列。队列是设备中真正执行工作的。它可以被认为是对外暴露了设备功能的子设备。在一些实现中,每一个独立饿甚至是系统内物理性分离。
多个设备被分组到一个或者多个队列集中,一个队列集包含一个或者多个队列。在一个集中的多个队列基本上是相同的。他们的能力、性能级别、对资源的访问也是相同的,除了同步的时候,相互之间转移工作也是没有损耗的。如果设备哟多个具有相同能力但性能不同的核心对内存的访问或其他会导致表现不同的因素,这会让他们不同的集明显的表现各异。
如在第一章讨论过的,你可以调用vkGetPhysicalDeviceQueueFamilyProperties()查询每一个物理设备队列集的属性。这个函数向一个你给定的VkQueueFamilyProperties类型的数据写入队列集的属性。
当你创建设备的时候,队列数量和类题型能够必须要确定。如你在第一章所见,你传入vkCreateDevice()的VkDeviceCreateInfo结构,包含queueCreateInfoCount和pQueueCreateInfos这两个成员。第一章简介了他们,现在我们来做详细的讲解。queueCreateInfoCount成员包含了一个由pQueueCreateInfos指针指向的数组中VkDeviceQueueCreateInfo类型对象的数量。VkDeviceQueueCreateInfo结构如下:
typedef struct VkDeviceQueueCreateInfo {
VkStructureType sType;
const void* pNext;
VkDeviceQueueCreateFlags flags;
uint32_t queueFamilyIndex;
uint32_t queueCount;
const float* pQueuePriorities;
} VkDeviceQueueCreateInfo;
如多数Vulkan数据结构一样,sType域是数据的类型,在此种情况下是VK_STRUCTURE_TYPE_QUEUE_CREATE_INFO,pNext域被拓展使用,当没有拓展时应当设置为nullptr。flags 域包含对队列的控制标记信息,但是,当前的Vulkan并没有定义任何标志,所以此成员应当被设置为0。
我们感兴趣的域是queueFamilyIndex 和 queueCount。queueFamilyIndex域指定了从哪个集中分配队,queueCount域指定了需分配的队列的个数。从多个集合中分配队列时,只需向VkDeviceCreateInfo类型对象的pQueueCreateInfos成员,传入多于一个VkDeviceQueueCreateInfo类型对象的数组即可。
当设备被创建的时候队列就被构造了。所以,我们无需创建队列,但是,为了获取他们,我们需要调用vkGetDeviceQueue():
void vkGetDeviceQueue (
VkDevice device,
uint32_t queueFamilyIndex,
uint32_t queueIndex,
VkQueue* pQueue);
这个函数以你需要从哪个设备获取队列的device,集的索引queueFamilyIndex,集合中队列的索引queueIndex 作为参数。pQueue参数指向了一个VkQueue类型handle,就是你想要的队列的handle。queueFamilyIndex、queueIndex必须参考device被创建时的参数。如果正确的这么做了,一个正确的队列 的handle将会被赋值到pQueue,否则,pQueue的值就会是VK_NULL_HANDLE。
创建命令buffer
队列的意义就是在应用程序内处理任务。任务是通过一串的命令表示的,命令被记录到命令缓冲区(command buffer)中。你的应用将会创建包含任务的命令缓冲区进而提交到队列来执行。在你记录任何命令之前,你需要创建命令缓冲区。命令缓冲区并不被直接创建,需要从pool中分配。你可以调用vkCreateCommandPool() 函数来创建pool,其原型如下:
VkResult vkCreateCommandPool (
VkDevice device,
const VkCommandPoolCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkCommandPool* pCommandPool);
和Vulkan中绝大多数对象创建一样,它的第一个参数device,就是拥有该pool的设备,对该pool的描述信息是通过一个指向某个数据结构的指针pCreateInfo来传递的。这个数据结构就是VkCommandPoolCreateInfo,定义如下:
typedef struct VkCommandPoolCreateInfo {
VkStructureType sType;
const void* pNext;
VkCommandPoolCreateFlags flags;
uint32_t queueFamilyIndex;
} VkCommandPoolCreateInfo;
与大多数Vulkan数据结构类似,前两个域是sType 和 pNext,前一包含了数据结构的类型,后一个是一指向包含更多关于pool创建信息的数据。这里,我们设置sType为K_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO。我们不需要传递任何其他信息,pNext需被设置为nullptr。
flags域包含标志,它控制了pool和从pool分配得倒的命令缓冲区的行为。其值为VkCommandPoolCreateFlagBits枚举类型。这个枚举现在有两个值被定义了:
l VK_COMMAND_POOL_CREATE_TRANSIENT_BIT 表示命令缓冲区使用周期短,使用完后马上退回给pool。不设置这个枚举值,就意味着告诉Vulkan,你将长时间的持有这个命令缓冲区。
l VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT表示允许单独的命令缓冲区可通过重置或重启而被重用。如果没有这个bit标志,那么只有pool本身能够被重置,这隐式的回收所有由它分配出的命令缓冲区。
每一个bit标志位都会增加一些开销,因为Vulkan实现需要跟踪资源或者改变其自身的分配策略。例如,设置VK_COMMAND_POOL_CREATE_TRANSIENT_BIT这个标志位会导致Vulkan实现跟踪每一个命令缓冲区的重置状态而不是简单的只是在pool内跟踪。在这种情形下,我们实际上需要把两个标志位都设置上。这将给我们极大的灵活性,可能在一些性能损失的条件下,我们可以批量的管理命令缓冲区。
最后,VkCommandPoolCreateInfo 的queueFamilyIndex域指定了从这个pool分配的命令缓冲区需要提交的队列的所属的集合。这是必需的,因为,一个设备上拥有相同性能并具有相同命令的两个队列,发送相同的命令到他们,表现也会各异。
pAllocator参数是应用程序用来管理主机内存的,这部分在第二章有讲解。假设一个命令池已被成功的创建,它的handle被赋值给pCommandPool,vkCreateCommandPool()就会返回VK_SUCCESS。
一旦我们有了一个可以分配命令缓冲区的pool,我们可以通过调用vkAllocateCommandBuffers()得到新的命令缓冲区,其原型如下:
VkResult vkAllocateCommandBuffers (
VkDevice device,
const VkCommandBufferAllocateInfo* pAllocateInfo,
VkCommandBuffer* pCommandBuffers);
分配额命令缓冲区的设备通过device参数传递,剩余的参数描述了命令缓冲区,是通过一个VkCommandBufferAllocateInfo类型的数据传递的,缓冲区的地址是通过pCommandBuffers参数传递的。VkCommandBufferAllocateInfo的原型如下:
typedef struct VkCommandBufferAllocateInfo {
VkStructureType sType;
const void* pNext;
VkCommandPool commandPool;
VkCommandBufferLevel level;
uint32_t commandBufferCount;
} VkCommandBufferAllocateInfo; sType域应当被赋值为VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,当我们仅仅使用核心功能集的时候,需要设置pNext为nullptr。我们在之前创建的命令池被放入了commandPool参数。
level参数表示我们需要分配的命令缓冲区的级别。它可以被赋值为VK_COMMAND_BUFFER_LEVEL_PRIMARY或者VK_COMMAND_BUFFER_LEVEL_SECONDARY.。Vulkan允许主命令缓冲区来调用次命令缓冲区。在我们最初的几个例子中,我们将只使用主级别的命令缓冲区。将在本书的后面讲到次级命令缓冲区。
最后,commandBufferCount指定了我们想要从pool中分配的命令缓冲区的数量。注意,我们并没有告诉Vulkan任何关于我们需要的命令缓冲区的大小和个数。表示设备命令的内部内部数据将自由的变动以适应任意尺寸大小的command。Vulkan会替你管理好命令缓冲区的内存。
如果vkAllocateCommandBuffers()运行成功,它会返回VK_SUCCESS,并且把分配好的多个命令缓冲区的handle放到pCommandBuffers这个数组中。这个数组应当足够的大以容纳这些handle。当然,如果你想仅仅分配一个命令缓冲区,你可以只声明一个普通的VkCommandBuffer类型的变量即可。
需要使用vkFreeCommandBuffers()来释放命令缓冲区,其声明如下:
void vkFreeCommandBuffers (
VkDevice device,
VkCommandPool commandPool,
uint32_t commandBufferCount,
const VkCommandBuffer* pCommandBuffers);
device参数是进行命令缓冲区分配所在的设备。commandPool是pool的handle,commandBufferCount是需要释放的命令缓冲区的个数,pCommandBuffers是需要被释放的命令缓冲区的handle组成的数组。注意,释放一个命令缓冲区并不意味着需要释放与它相关的资源,只是把它们放回了他们被创建时所在的pool。
为了释放一个命令池所用的所有资源和它创建的所有命令缓冲区,需要调用vkDestroyCommandPool(),原型如下:
void vkDestroyCommandPool (
VkDevice device,
VkCommandPool commandPool,
const VkAllocationCallbacks* pAllocator);
拥有命令池的设备是通过device参数传入的,需要销毁的命令池通过commandPool参数传递。pAllocator参数应当和pool创建时所用的该参数保持一致。如果vkCreateCommandPool()的pAllocator参数是nullptr,这个函数中的pAllocator参数也应该为nullptr。
在pool被销毁之前,没有必要显式的释放所有的从它分配出来的命令缓冲区。当pool被销毁时,分配出来的命令缓冲区,作为pool的组成部分,会自动的被释放,每个缓冲区相关的资源同样会被释放。然而,这里需要注意,需要保证当vkDestroyCommandPool()被调用时,从pool分配出来的命令缓冲区正在执行。