Link

我们将首先在 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 同步。

下一步: 渲染循环