Link

我们将首先开始向 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 渲染通道