Link

如果你启用了图层运行了三角形,你可能已经看到,当你关闭应用程序时,图层会抱怨管线和其他对象没有被删除。鉴于我们拥有的 Vulkan 对象数量增长到如此程度,现在是时候进行小的重构,并实施一个更好的系统来处理这些删除。

删除队列

在 Vulkan 中,我们不能删除任何对象,除非我们确定 GPU 没有在使用它。乱序删除对象也是一个大问题,因为它会让图层抱怨,并可能导致驱动程序崩溃。由于这些原因,使用普通的 Cpp 析构函数是不可能的,也是不可行的。我们需要一个更好的系统来删除对象。

一个非常常见的实现是创建一个删除队列。在创建对象时,我们也会将对象添加到队列中,然后在应用程序的某个时刻,一旦我们确定 GPU 已经完成,我们就会遍历队列删除所有内容。

实现所述删除队列的一种方法是使用包含 Vulkan 对象的数组,然后按顺序删除这些对象。但现在我们需要更简单的东西。

我们将要实现它的方式是拥有一个 std::function lambda 队列,这些 lambda 将按 FIFO(先进先出)的顺序被调用。这种方式不是最有效的,但对于小型引擎来说已经足够好了。

vulkan_engine.h

struct DeletionQueue
{
	std::deque<std::function<void()>> deletors;

	void push_function(std::function<void()>&& function) {
		deletors.push_back(function);
	}

	void flush() {
		// reverse iterate the deletion queue to execute all the functions
		for (auto it = deletors.rbegin(); it != deletors.rend(); it++) {
			(*it)(); //call the function
		}

		deletors.clear();
	}
};

//add the deletion queue as a variable of vulkanengine
class VulkanEngine{
// other code .....
	DeletionQueue _mainDeletionQueue;
}

确保 #include <functional> 以便你可以使用 std::function,并 #include <deque> 以便你可以使用 deque。你可以将它们包含在 vk_engine.h 或 vk_types.h 中。

这是我们从现在开始将使用的删除队列的实现。用法如下

VkSomething something;
VkCreateSomething(&something);

_mainDeletionQueue.push_function([=](){
VkDeleteSomething(something);
});

然后,作为我们清理函数的一部分,我们 flush() 删除队列,这将按照 lambda 入队的顺序调用它们。非常重要的是要记住 cpp lambda 如何捕获数据。在这里我们使用 [=] 捕获,这意味着它将创建对象的副本。对此要非常小心,除非你知道自己在做什么,否则永远不要通过引用捕获任何内容。在上面的示例中,它将首先调用 DestroyFence,然后调用 DestroySemaphore,因为它保持了顺序。

现在我们有了这个,让我们在当前代码库中实现它,以便我们可以回到验证层不会抱怨的状态。如果你想跳过这一步,你可以查看教程代码,其中已经使用了它。

重构代码

我们将首先更改 VulkanEngine::Cleanup() 以使用删除队列。

void VulkanEngine::cleanup()
{
	if (_isInitialized) {

		//make sure the GPU has stopped doing its things
		vkWaitForFences(_device, 1, &_renderFence, true, 1000000000);

		_mainDeletionQueue.flush();

		vkDestroyDevice(_device, nullptr);
		vkDestroySurfaceKHR(_instance, _surface, nullptr);
		vkb::destroy_debug_utils_messenger(_instance, _debug_messenger);
		vkDestroyInstance(_instance, nullptr);
		SDL_DestroyWindow(_window);
	}
}

我们不会将所有内容都放入删除队列中,因此我们将保留 Surface、Device、Instance 和 SDL 窗口。所有其他的都将转换为使用删除队列。

让我们将删除队列代码添加到交换链代码中

void VulkanEngine::init_swapchain()
{
	//other code ....

	//store swapchain and its related images
	_swapchain = vkbSwapchain.swapchain;
	_swapchainImages = vkbSwapchain.get_images().value();
	_swapchainImageViews = vkbSwapchain.get_image_views().value();

	_swapchainImageFormat = vkbSwapchain.image_format;

	_mainDeletionQueue.push_function([=]() {
		vkDestroySwapchainKHR(_device, _swapchain, nullptr);
	});
}

现在正确删除渲染通道

void VulkanEngine::init_default_renderpass()
{
	// other code.....

	VK_CHECK(vkCreateRenderPass(_device, &render_pass_info, nullptr, &_renderPass));

	_mainDeletionQueue.push_function([=]() {
		vkDestroyRenderPass(_device, _renderPass, nullptr);
    });
}

接下来是删除帧缓冲区

void VulkanEngine::init_framebuffers()
{
	//create the framebuffers for the swapchain images. This will connect the render-pass to the images for rendering
	VkFramebufferCreateInfo fb_info = vkinit::framebuffer_create_info(_renderPass, _windowExtent);

	const uint32_t swapchain_imagecount = _swapchainImages.size();
	_framebuffers = std::vector<VkFramebuffer>(swapchain_imagecount);

	for (int i = 0; i < swapchain_imagecount; i++) {

		fb_info.pAttachments = &_swapchainImageViews[i];
		VK_CHECK(vkCreateFramebuffer(_device, &fb_info, nullptr, &_framebuffers[i]));

		_mainDeletionQueue.push_function([=]() {
			vkDestroyFramebuffer(_device, _framebuffers[i], nullptr);
			vkDestroyImageView(_device, _swapchainImageViews[i], nullptr);
    	});
	}
}

接下来是命令池

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));

	_mainDeletionQueue.push_function([=]() {
		vkDestroyCommandPool(_device, _commandPool, nullptr);
	});
}

还有同步结构

void VulkanEngine::init_sync_structures()
{
	VkFenceCreateInfo fenceCreateInfo = vkinit::fence_create_info(VK_FENCE_CREATE_SIGNALED_BIT);

	VK_CHECK(vkCreateFence(_device, &fenceCreateInfo, nullptr, &_renderFence));

    //enqueue the destruction of the fence
    _mainDeletionQueue.push_function([=]() {
        vkDestroyFence(_device, _renderFence, nullptr);
    });

	VkSemaphoreCreateInfo semaphoreCreateInfo = vkinit::semaphore_create_info();

	VK_CHECK(vkCreateSemaphore(_device, &semaphoreCreateInfo, nullptr, &_presentSemaphore));
	VK_CHECK(vkCreateSemaphore(_device, &semaphoreCreateInfo, nullptr, &_renderSemaphore));

    //enqueue the destruction of semaphores
    _mainDeletionQueue.push_function([=]() {
        vkDestroySemaphore(_device, _presentSemaphore, nullptr);
        vkDestroySemaphore(_device, _renderSemaphore, nullptr);
    });
}

我们在 vk_initializers 中添加新的初始化器,用于栅栏和信号量创建

//header
VkFenceCreateInfo fence_create_info(VkFenceCreateFlags flags = 0);
VkSemaphoreCreateInfo semaphore_create_info(VkSemaphoreCreateFlags flags = 0);

//implementation
VkFenceCreateInfo vkinit::fence_create_info(VkFenceCreateFlags flags)
{
    VkFenceCreateInfo fenceCreateInfo = {};
    fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    fenceCreateInfo.pNext = nullptr;
    fenceCreateInfo.flags = flags;
    return fenceCreateInfo;
}

VkSemaphoreCreateInfo vkinit::semaphore_create_info(VkSemaphoreCreateFlags flags)
{
    VkSemaphoreCreateInfo semCreateInfo = {};
    semCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    semCreateInfo.pNext = nullptr;
    semCreateInfo.flags = flags;
    return semCreateInfo;
}

最后,我们为本章中的新对象添加删除。对于着色器模块,我们不会使用队列。当从着色器模块创建管线时,你可以删除着色器模块,这将没问题。


void VulkanEngine::init_pipelines()
{
// other code .....

	//destroy all shader modules, outside of the queue
	vkDestroyShaderModule(_device, redTriangleVertShader, nullptr);
	vkDestroyShaderModule(_device, redTriangleFragShader, nullptr);
	vkDestroyShaderModule(_device, triangleFragShader, nullptr);
	vkDestroyShaderModule(_device, triangleVertexShader, nullptr);

 	_mainDeletionQueue.push_function([=]() {
		//destroy the 2 pipelines we have created
		vkDestroyPipeline(_device, _redTrianglePipeline, nullptr);
        vkDestroyPipeline(_device, _trianglePipeline, nullptr);

		//destroy the pipeline layout that they use
		vkDestroyPipelineLayout(_device, _trianglePipelineLayout, nullptr);
    });
}

此时,如果你运行引擎并关闭它,调试层应该只会抱怨缺少 VK_OBJECT_TYPE_DEBUG_UTILS_MESSENGER_EXT; 由于此对象是 vk-bootstrap 库隐式创建的,我们不必关心它。

这样,第 2 章就完成了。下一步是开始在第 3 章中渲染网格。

下一步: 顶点缓冲区