我们将首先在 vk_engine.h 头文件中编写我们的 FrameData
结构体。这将保存我们绘制给定帧所需的结构和命令,因为我们将使用双缓冲,GPU 在运行某些命令的同时,我们将写入其他命令。
struct FrameData {
VkCommandPool _commandPool;
VkCommandBuffer _mainCommandBuffer;
};
constexpr unsigned int FRAME_OVERLAP = 2;
我们还需要将这些添加到 vulkan 引擎类中,以及我们将用于存储队列的成员。
class VulkanEngine{
public:
FrameData _frames[FRAME_OVERLAP];
FrameData& get_current_frame() { return _frames[_frameNumber % FRAME_OVERLAP]; };
VkQueue _graphicsQueue;
uint32_t _graphicsQueueFamily;
}
我们将不会在初始化逻辑之外直接访问 _frames
数组。因此我们添加一个 getter,它将使用我们用于计数帧的 _frameNumber
成员来访问它。这样它将在我们拥有的 2 个结构体之间翻转。
获取队列
我们现在需要找到一个有效的队列族并从中创建一个队列。我们想要创建一个可以执行所有类型命令的队列,以便我们可以将其用于引擎中的所有内容。
幸运的是,VkBootstrap 库允许我们直接获取队列和族。
转到 init_vulkan()
函数的末尾,我们在那里初始化了核心 Vulkan 结构。
在其末尾,添加此代码。
void VulkanEngine::init_vulkan(){
// ---- other code, initializing vulkan 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();
}
我们首先从 vkbootstrap 请求图形类型的队列族和队列。
创建命令结构
对于池,我们开始在 init_commands()
中添加代码,与之前不同,从现在开始 VkBootstrap 库将不会为我们做任何事情,我们将开始直接调用 Vulkan 命令。
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 = {};
commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
commandPoolInfo.pNext = nullptr;
commandPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
commandPoolInfo.queueFamilyIndex = _graphicsQueueFamily;
for (int i = 0; i < FRAME_OVERLAP; i++) {
VK_CHECK(vkCreateCommandPool(_device, &commandPoolInfo, nullptr, &_frames[i]._commandPool));
// 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;
cmdAllocInfo.commandPool = _frames[i]._commandPool;
cmdAllocInfo.commandBufferCount = 1;
cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
VK_CHECK(vkAllocateCommandBuffers(_device, &cmdAllocInfo, &_frames[i]._mainCommandBuffer));
}
}
大多数 Vulkan 信息结构,用于 VkCreateX 函数,以及许多其他 Vulkan 结构,都需要设置 sType 和 pNext。这用于扩展,因为某些扩展仍将调用 VkCreateX 函数,但使用与普通类型不同的结构。sType 帮助实现了解函数中正在使用的结构。
对于 Vulkan 结构,非常重要的是我们这样做
VkCommandPoolCreateInfo commandPoolInfo = {};
通过执行 ` = {}` 操作,我们让编译器将整个结构初始化为零。这至关重要,因为通常 Vulkan 结构会以 0 相对安全的方式设置其默认值。通过这样做,我们确保我们不会在结构中留下未初始化的数据。
我们将 queueFamilyIndex
设置为我们之前获取的 _graphicsQueueFamily
。这意味着命令池将创建与该“图形”族中的任何队列兼容的命令。
我们还在 .flags 参数中设置了一些内容。许多 Vulkan 结构都有 .flags 参数,用于额外的选项。我们正在发送 VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT
,它告诉 Vulkan 我们希望能够重置从该池创建的单个命令缓冲区。另一种方法是一次重置整个命令池,这将重置所有命令缓冲区。在那种情况下,我们将不需要该标志。
最后,我们最终调用 VkCreateCommandPool
,为其提供我们的 VkDevice
,commandPoolInfo 用于创建参数,以及指向 _commandPool
成员的指针,如果成功,该成员将被覆盖。
要检查命令是否成功,我们使用 VK_CHECK()
宏。如果发生任何事情,它将立即中止。
现在我们已经创建了 VkCommandPool
,并存储在 _commandPool
成员中,我们可以从中分配我们的命令缓冲区。
与命令池一样,我们需要填写 sType 和 pNext 参数,然后继续 Info 结构的其余部分。
我们让 Vulkan 知道我们命令的父级将是我们刚刚创建的 _commandPool
,并且我们只想创建一个命令缓冲区。
.commandBufferCount 参数允许您一次分配多个缓冲区。确保您发送到 VkAllocateCommandBuffer 的指针有这些缓冲区的空间。
.level 设置为 Primary。命令缓冲区可以是 Primary 或 Secondary 级别。Primary 级别是发送到 VkQueue 中的级别,并完成所有工作。这就是我们将在指南中使用的内容。Secondary 级别是可以充当主缓冲区的“子命令”的级别。当您想要从多个线程记录单个通道的命令时,它们最常用。我们不会使用它们,因为使用我们将要做的架构,我们将不需要多线程命令记录。
您可以在此处找到这些信息结构的详细信息和参数
VkInit 模块
如果您还记得探索项目文件的文章,我们评论说 vk_initializers 模块将包含 Vulkan 结构初始化的抽象。让我们看看这 2 个结构的实现。
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*/)
{
VkCommandBufferAllocateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
info.pNext = nullptr;
info.commandPool = pool;
info.commandBufferCount = count;
info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
return info;
}
我们将硬编码命令缓冲区级别为 VK_COMMAND_BUFFER_LEVEL_PRIMARY
。由于我们永远不会使用辅助命令缓冲区,我们可以忽略它们的存在和配置参数。通过使用与您的引擎匹配的默认值抽象事物,您可以稍微简化事情。
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);
for (int i = 0; i < FRAME_OVERLAP; i++) {
VK_CHECK(vkCreateCommandPool(_device, &commandPoolInfo, nullptr, &_frames[i]._commandPool));
// allocate the default command buffer that we will use for rendering
VkCommandBufferAllocateInfo cmdAllocInfo = vkinit::command_buffer_allocate_info(_frames[i]._commandPool, 1);
VK_CHECK(vkAllocateCommandBuffers(_device, &cmdAllocInfo, &_frames[i]._mainCommandBuffer));
}
}
更好更短。在整个指南中,我们将继续使用 vkinit 命名空间。鉴于它非常简单,您将能够在其他项目中安全地重用该模块。请记住,starting_point 分支已编写了它,如第 0 章中建议的那样。
清理
与之前一样,我们创建的内容,我们必须删除
void VulkanEngine::cleanup()
{
if (_isInitialized) {
//make sure the gpu has stopped doing its things
vkDeviceWaitIdle(_device);
for (int i = 0; i < FRAME_OVERLAP; i++) {
vkDestroyCommandPool(_device, _frames[i]._commandPool, nullptr);
}
// --- rest of code
}
}
由于命令池是最新的 Vulkan 对象,我们需要在其他对象之前销毁它。无法单独销毁 VkCommandBuffer
,销毁它们的父池将销毁从中分配的所有命令缓冲区。
VkQueue 也无法销毁,就像 VkPhysicalDevice
一样,它们不是真正创建的对象,更像是 VkInstance 中已存在的事物的句柄。
我们现在有了一种向 gpu 发送命令的方法,但我们仍然需要另一部分,即同步结构,以将 GPU 执行与 CPU 同步。
下一步: 渲染循环