如果你启用了图层运行了三角形,你可能已经看到,当你关闭应用程序时,图层会抱怨管线和其他对象没有被删除。鉴于我们拥有的 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 章中渲染网格。
下一步: 顶点缓冲区