我们将首先开始向 VulkanEngine 类添加我们需要的句柄,以及 init_commands()
函数
class VulkanEngine {
public:
// ---- other code -----
VkQueue _graphicsQueue; //queue we will submit to
uint32_t _graphicsQueueFamily; //family of that queue
VkCommandPool _commandPool; //the command pool for our commands
VkCommandBuffer _mainCommandBuffer; //the buffer we will record into
private:
//----- other code----
void init_commands();
};
确保从我们的主 init()
函数调用 init_commands()
函数
void VulkanEngine::init()
{
// --- other code (SDL Stuff)---
init_vulkan();
init_swapchain();
init_commands();
}
获取队列
幸运的是,VkBootstrap 库允许我们直接获取队列和队列族。
转到 init_vulkan()
函数的末尾,我们在那里初始化了核心 Vulkan 结构。
在末尾,添加这段代码
void VulkanEngine::init_vulkan(){
// ---- other code ----
vkb::Device vkbDevice = deviceBuilder.build().value();
// Get the VkDevice handle used in the rest of a Vulkan application
_device = vkbDevice.device;
_chosenGPU = physicalDevice.physical_device;
// use vkbootstrap to get a Graphics queue
_graphicsQueue = vkbDevice.get_queue(vkb::QueueType::graphics).value();
_graphicsQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::graphics).value();
}
上面部分应该已经在 init-code 部分中。我们只是添加了 2 行新代码,以从 vkbDevice 请求队列,并让库为我们处理。
我们正在请求一个图形队列,它支持本指南所需的一切。
创建 VkCommandPool
对于命令池,我们开始在 init_commands()
中添加代码,与之前不同,从现在开始,VkBootstrap 库将不会为我们做任何事情,我们将开始直接调用 Vulkan 命令。
void VulkanEngine::init_commands()
{
//create a command pool for commands submitted to the graphics queue.
VkCommandPoolCreateInfo commandPoolInfo = {};
commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
commandPoolInfo.pNext = nullptr;
//the command pool will be one that can submit graphics commands
commandPoolInfo.queueFamilyIndex = _graphicsQueueFamily;
//we also want the pool to allow for resetting of individual command buffers
commandPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
VK_CHECK(vkCreateCommandPool(_device, &commandPoolInfo, nullptr, &_commandPool));
}
大多数 Vulkan Info 结构,用于所有 VkCreateX 函数以及许多其他 Vulkan 结构,都需要设置 sType 和 pNext。这用于扩展,因为某些扩展仍将调用 VkCreateX 函数,但使用与普通结构类型不同的结构。sType 帮助实现了解函数中正在使用的结构。
对于 Vulkan 结构,非常重要的是我们这样做
VkCommandPoolCreateInfo commandPoolInfo = {};
通过执行 `= {}` 操作,我们让编译器将整个结构初始化为零。这至关重要,因为通常 Vulkan 结构会将其默认值设置为相对安全的值 0。通过这样做,我们确保不在结构中留下未初始化的数据。
我们将 queueFamilyIndex 设置为我们之前获取的 _graphicsQueueFamily。这意味着命令池将创建与该“graphics”族中的任何队列兼容的命令。
我们还在 .flags 参数中设置了一些内容。许多 Vulkan 结构都有 .flags 参数,用于额外的选项。我们正在发送 VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,这告诉 Vulkan 我们希望能够重置从该池创建的各个命令缓冲区。
最后,我们最终调用 VkCreateCommandPool,为其提供我们的 VkDevice、用于创建参数的 commandPoolInfo 以及指向 _commandPool 成员的指针,如果成功,该成员将被覆盖。
要检查命令是否成功,我们使用 VK_CHECK() 宏。如果发生任何事情,它将立即中止。
创建 VkCommandBuffer
现在我们已经创建了 VkCommandPool 并将其存储在 _commandPool 成员中,我们可以从中分配我们的命令缓冲区。
void VulkanEngine::init_commands()
{
// --- other code ----
//allocate the default command buffer that we will use for rendering
VkCommandBufferAllocateInfo cmdAllocInfo = {};
cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmdAllocInfo.pNext = nullptr;
//commands will be made from our _commandPool
cmdAllocInfo.commandPool = _commandPool;
//we will allocate 1 command buffer
cmdAllocInfo.commandBufferCount = 1;
// command level is Primary
cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
VK_CHECK(vkAllocateCommandBuffers(_device, &cmdAllocInfo, &_mainCommandBuffer));
}
与命令池一样,我们需要填写 sType 和 pNext 参数,然后继续 Info 结构的其余部分。
我们让 Vulkan 知道我们命令的父级将是我们刚刚创建的 _commandPool,并且我们只想创建一个命令缓冲区。
.commandBufferCount 参数允许您一次分配多个缓冲区。确保您发送到 VkAllocateCommandBuffer 的指针有足够的空间容纳这些缓冲区。
.level 设置为 Primary 。命令缓冲区可以是 Primary 或 Secondary 级别。Primary 级别是发送到 VkQueue 的级别,并完成所有工作。这就是我们将在本指南中使用的内容。Secondary 级别是可以充当主缓冲区的“子命令”的级别。它们最常用于高级多线程场景。我们不会使用它们。
VkInit 模块
如果您还记得探索项目文件的文章,我们评论说 vk_initializers 模块将包含 Vulkan 结构初始化的抽象。让我们将 2 个 Info 结构抽象到那里,以便于阅读。
vk_initializers.h
namespace vkinit {
VkCommandPoolCreateInfo command_pool_create_info(uint32_t queueFamilyIndex, VkCommandPoolCreateFlags flags = 0);
VkCommandBufferAllocateInfo command_buffer_allocate_info(VkCommandPool pool, uint32_t count = 1, VkCommandBufferLevel level = VK_COMMAND_BUFFER_LEVEL_PRIMARY);
}
2 个新函数,command_pool_create_info()
和 command_buffer_allocate_info()
。我们还使用默认参数 flags = 0
,这样就不必为基本内容输入所有参数。现在,让我们将代码复制到这两个函数的实现中。
vk_initializers.cpp
VkCommandPoolCreateInfo vkinit::command_pool_create_info(uint32_t queueFamilyIndex, VkCommandPoolCreateFlags flags /*= 0*/)
{
VkCommandPoolCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
info.pNext = nullptr;
info.queueFamilyIndex = queueFamilyIndex;
info.flags = flags;
return info;
}
VkCommandBufferAllocateInfo vkinit::command_buffer_allocate_info(VkCommandPool pool, uint32_t count /*= 1*/, VkCommandBufferLevel level /*= VK_COMMAND_BUFFER_LEVEL_PRIMARY*/)
{
VkCommandBufferAllocateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
info.pNext = nullptr;
info.commandPool = pool;
info.commandBufferCount = count;
info.level = level;
return info;
}
我们现在已经抽象了调用,所以让我们更改 VulkanEngine::init_commands() 以使用它。
void VulkanEngine::init_commands()
{
//create a command pool for commands submitted to the graphics queue.
//we also want the pool to allow for resetting of individual command buffers
VkCommandPoolCreateInfo commandPoolInfo = vkinit::command_pool_create_info(_graphicsQueueFamily, VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
VK_CHECK(vkCreateCommandPool(_device, &commandPoolInfo, nullptr, &_commandPool));
//allocate the default command buffer that we will use for rendering
VkCommandBufferAllocateInfo cmdAllocInfo = vkinit::command_buffer_allocate_info(_commandPool, 1);
VK_CHECK(vkAllocateCommandBuffers(_device, &cmdAllocInfo, &_mainCommandBuffer));
}
更好更简洁了。在本指南中,vk_initializers 模块将随着越来越多的结构而不断增长。考虑到它非常简单,您将能够在其他项目中安全地重用该模块。
清理
和以前一样,我们创建了什么,就必须删除什么
void VulkanEngine::cleanup()
{
if (_isInitialized) {
vkDestroyCommandPool(_device, _commandPool, nullptr);
// --- rest of code
}
}
由于命令池是最新的 Vulkan 对象,我们需要在其他对象之前销毁它。无法单独销毁 VkCommandBuffer,因为销毁它们的父池将销毁从中分配的所有命令缓冲区。
VkQueue 也无法销毁,就像 VkPhysicalDevice 一样,它们不是真正创建的对象,更像是已经存在的事物的句柄。
现在我们有了队列和命令缓冲区,我们准备开始执行命令,但它们的可用性将受到限制,因为我们仍然缺少执行图形命令所需的结构。
此时,如果您想执行离线计算,则可以执行纯计算命令。但是要进行渲染,我们需要先进行渲染通道。
下一步:Vulkan 渲染通道