初始化核心 Vulkan 结构
这些解释假设您从 chapter-0 的代码开始。如果您没有项目设置,请获取 chapter-0 的代码并编译它。
首先要做的是 `#include` vkBootstrap 库,我们将使用它来简化初始化代码。为此,请转到 vk_engine.cpp 的顶部,并包含 `"VkBootstrap.h"` 头文件
// --- other includes ---
#include <vk_types.h>
#include <vk_initializers.h>
//bootstrap library
#include "VkBootstrap.h"
VkBootstrap 将从我们的引擎中删除数百行代码,从而大大简化启动代码。如果您想学习如何在没有 vkbootstrap 的情况下自己完成此操作,可以尝试阅读 vulkan-tutorial 的第一章:here
我们需要初始化的第一件事是 Vulkan 实例。为此,让我们首先向 VulkanEngine 类添加一个新函数和存储的句柄。我们还在引擎类中为初始化的不同阶段添加一些额外的函数。
vk_engine.h
class VulkanEngine {
public:
// --- omitted ---
VkInstance _instance;// Vulkan library handle
VkDebugUtilsMessengerEXT _debug_messenger;// Vulkan debug output handle
VkPhysicalDevice _chosenGPU;// GPU chosen as the default device
VkDevice _device; // Vulkan device for commands
VkSurfaceKHR _surface;// Vulkan window surface
private:
void init_vulkan();
void init_swapchain();
void init_commands();
void init_sync_structures();
现在我们将从引擎初始化函数中按顺序调用这些初始化函数
vk_engine.cpp
constexpr bool bUseValidationLayers = false;
void VulkanEngine::init()
{
// We initialize SDL and create a window with it.
SDL_Init(SDL_INIT_VIDEO);
SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_VULKAN);
_window = SDL_CreateWindow(
"Vulkan Engine",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
_windowExtent.width,
_windowExtent.height,
window_flags
);
init_vulkan();
init_swapchain();
init_commands();
init_sync_structures();
//everything went fine
_isInitialized = true;
}
void VulkanEngine::init_vulkan()
{
//nothing yet
}
void VulkanEngine::init_swapchain()
{
//nothing yet
}
void VulkanEngine::init_commands()
{
//nothing yet
}
void VulkanEngine::init_sync_structures()
{
//nothing yet
}
我们已向主类添加了 4 个句柄:`VkDevice`、`VkPhysicalDevice`、`VkInstance`、`VkDebugUtilsMessengerEXT`。
实例
现在我们已经添加了新的 init_Vulkan 函数,我们可以开始用创建实例所需的代码填充它。
void VulkanEngine::init_vulkan()
{
vkb::InstanceBuilder builder;
//make the vulkan instance, with basic debug features
auto inst_ret = builder.set_app_name("Example Vulkan Application")
.request_validation_layers(bUseValidationLayers)
.use_default_debug_messenger()
.require_api_version(1, 3, 0)
.build();
vkb::Instance vkb_inst = inst_ret.value();
//grab the instance
_instance = vkb_inst.instance;
_debug_messenger = vkb_inst.debug_messenger;
}
我们将创建一个 `vkb::InstanceBuilder`,它来自 VkBootstrap 库,并抽象了 Vulkan `VkInstance` 的创建。
为了创建实例,我们希望它具有名称“Example Vulkan Application”,启用验证层,并使用默认调试记录器。“Example Vulkan Application”名称无关紧要。您可以将名称设置为您想要的任何名称。初始化 `VkInstance` 时,会提供应用程序和引擎的名称。这样做是为了让驱动程序供应商更容易找到游戏/引擎的名称,以便他们可以单独为它们调整内部驱动程序逻辑。对我们来说,这并不重要。
我们希望默认启用验证层。根据我们在指南中要做的事情,没有必要关闭它们,因为它们会很好地捕获我们的错误。在更高级的引擎上,您只会以调试模式或使用特定的配置参数启用这些层。Vulkan 验证层会显着降低 Vulkan 调用的性能,因此一旦我们开始加载包含大量数据的复杂场景,我们将需要禁用它们以查看代码的真实性能。
我们还需要 Vulkan API 版本 1.3。这应该在相对现代的 GPU 上得到支持。我们将利用该 Vulkan 版本提供的功能。如果您使用的是不支持这些功能的旧 PC/GPU,则您将必须遵循本指南的旧版本,该版本的目标是 1.1。
最后,我们告诉库我们需要调试信使。这是捕获验证层将输出的日志消息的内容。因为我们不需要专用的调试信使,所以我们只需让库使用默认的调试信使,它会输出到控制台窗口。
然后我们只需从 `vkb::Instance` 对象中获取实际的 VkInstance 句柄和 `VkDebugUtilsMessengerEXT` 句柄。我们存储 `VkDebugUtilsMessengerEXT` 是为了在程序退出时销毁它,否则我们会泄漏它。
设备
void VulkanEngine::init_vulkan()
{
// other code ------
SDL_Vulkan_CreateSurface(_window, _instance, &_surface);
//vulkan 1.3 features
VkPhysicalDeviceVulkan13Features features{ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES };
features.dynamicRendering = true;
features.synchronization2 = true;
//vulkan 1.2 features
VkPhysicalDeviceVulkan12Features features12{ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES };
features12.bufferDeviceAddress = true;
features12.descriptorIndexing = true;
//use vkbootstrap to select a gpu.
//We want a gpu that can write to the SDL surface and supports vulkan 1.3 with the correct features
vkb::PhysicalDeviceSelector selector{ vkb_inst };
vkb::PhysicalDevice physicalDevice = selector
.set_minimum_version(1, 3)
.set_required_features_13(features)
.set_required_features_12(features12)
.set_surface(_surface)
.select()
.value();
//create the final vulkan device
vkb::DeviceBuilder deviceBuilder{ physicalDevice };
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;
}
为了选择要使用的 GPU,我们将使用 `vkb::PhysicalDeviceSelector`。
首先,我们需要从 SDL 窗口创建一个 `VkSurfaceKHR` 对象。这是我们将要渲染到的实际窗口,因此我们需要告诉物理设备选择器获取一个可以渲染到该窗口的 GPU。
我们需要启用一些功能。首先是一些 Vulkan 1.3 功能,即动态渲染和同步 2。这些是 Vulkan 1.3 中提供的可选功能,它们改变了一些东西。动态渲染允许我们完全跳过渲染通道/帧缓冲区(如果您想了解它们,旧版本的 vkguide 中对此进行了说明),并且还可以使用新升级版本的同步功能。我们还将使用 Vulkan 1.2 功能 `bufferDeviceAddress` 和 `descriptorIndexing`。缓冲区设备地址将允许我们使用 GPU 指针而无需绑定缓冲区,而 descriptorIndexing 为我们提供无绑定纹理。
通过为 `vkb::PhysicalDeviceSelector` 提供 `VkPhysicalDeviceVulkan13Features` 结构,我们可以告诉 vkbootstrap 找到具有这些功能的 GPU。
根据您的 Vulkan 版本,您可以使用多个级别的功能结构,您可以在此处找到它们的信息
Vulkan 规范:1.0 物理设备功能 Vulkan 规范:1.1 物理设备功能 Vulkan 规范:1.2 物理设备功能 Vulkan 规范:1.3 物理设备功能
一旦我们有了 `VkPhysicalDevice`,我们就可以直接从中构建 VkDevice。
最后,我们将句柄存储在类中。
这样,我们就初始化了 Vulkan。我们现在可以开始调用 Vulkan 命令了。
如果您现在运行该项目,如果您没有具有所需功能的 GPU 或不支持它们的 Vulkan 驱动程序,它将崩溃。如果发生这种情况,请确保您的驱动程序已更新。如果只是您的 GPU 不支持某些功能,请使用旧版本的 vkguide,因为您将无法学习本教程。
设置交换链
核心初始化的最后一件事是初始化交换链,以便我们可以将内容渲染到其中。
首先向 VulkanEngine 添加新成员和函数
class VulkanEngine {
public:
// --- other code ---
VkSwapchainKHR _swapchain;
VkFormat _swapchainImageFormat;
std::vector<VkImage> _swapchainImages;
std::vector<VkImageView> _swapchainImageViews;
VkExtent2D _swapchainExtent;
private:
void create_swapchain(uint32_t width, uint32_t height);
void destroy_swapchain();
}
我们正在存储 `VkSwapchainKHR` 本身,以及交换链图像在渲染到它们时使用的格式。
我们还存储 2 个数组,一个用于图像,另一个用于图像视图。
`VkImage` 是实际图像对象的句柄,可用作纹理或渲染到其中。`VkImageView` 是该图像的包装器。它允许执行诸如交换颜色之类的操作。我们稍后将详细介绍它。
我们还为交换链添加了创建和销毁函数。
与其他初始化函数一样,我们将使用 vkb 库来创建交换链。它使用类似于我们用于实例和设备的构建器。
void VulkanEngine::create_swapchain(uint32_t width, uint32_t height)
{
vkb::SwapchainBuilder swapchainBuilder{ _chosenGPU,_device,_surface };
_swapchainImageFormat = VK_FORMAT_B8G8R8A8_UNORM;
vkb::Swapchain vkbSwapchain = swapchainBuilder
//.use_default_format_selection()
.set_desired_format(VkSurfaceFormatKHR{ .format = _swapchainImageFormat, .colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR })
//use vsync present mode
.set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR)
.set_desired_extent(width, height)
.add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT)
.build()
.value();
_swapchainExtent = vkbSwapchain.extent;
//store swapchain and its related images
_swapchain = vkbSwapchain.swapchain;
_swapchainImages = vkbSwapchain.get_images().value();
_swapchainImageViews = vkbSwapchain.get_image_views().value();
}
void VulkanEngine::init_swapchain()
{
create_swapchain(_windowExtent.width, _windowExtent.height);
}
从 create_swapchain 中,我们创建交换链结构,然后我们从 `init_swapchain()` 中调用该函数
这里最重要的细节是呈现模式,我们已将其设置为 `VK_PRESENT_MODE_FIFO_KHR`。这样,我们就进行了硬 VSync,这将整个引擎的 FPS 限制为显示器的速度。
我们还将窗口大小发送到交换链。这很重要,因为创建交换链也会为其创建图像,因此大小被锁定。在本教程的后面部分,当窗口大小调整时,我们将需要重建交换链,因此我们将它们与初始化流程分开,但在初始化流程中,我们将该大小默认为窗口大小。
构建交换链后,我们只需将其所有内容存储到 VulkanEngine 类的成员中。
让我们也编写 `destroy_swapchain()` 函数。
void VulkanEngine::destroy_swapchain()
{
vkDestroySwapchainKHR(_device, _swapchain, nullptr);
// destroy swapchain resources
for (int i = 0; i < _swapchainImageViews.size(); i++) {
vkDestroyImageView(_device, _swapchainImageViews[i], nullptr);
}
}
我们首先删除交换链对象,这将删除它在内部保存的图像。然后我们必须销毁这些图像的图像视图。
清理资源
我们需要确保在我们创建的应用程序存在时,所有 Vulkan 资源都已正确删除。
为此,请转到 `VulkanEngine::cleanup()` 函数
void VulkanEngine::cleanup()
{
if (_isInitialized) {
destroy_swapchain();
vkDestroySurfaceKHR(_instance, _surface, nullptr);
vkDestroyDevice(_device, nullptr);
vkb::destroy_debug_utils_messenger(_instance, _debug_messenger);
vkDestroyInstance(_instance, nullptr);
SDL_DestroyWindow(_window);
}
}
对象彼此之间具有依赖关系,我们需要按正确的顺序删除它们。以与创建顺序相反的顺序删除它们是一个好方法。在某些情况下,如果您知道自己在做什么,则可以稍微更改顺序,这也可以。
`VkPhysicalDevice` 无法销毁,因为它本身不是 Vulkan 资源,它更像是系统中 GPU 的句柄。
因为我们的初始化顺序是 SDL 窗口 -> 实例 -> 表面 -> 设备 -> 交换链,所以我们销毁的顺序正好相反。
如果您现在尝试运行该程序,它应该什么也不做,但什么也不做也包括不发出错误。
在这种特定情况下,无需销毁图像,因为图像由交换链拥有并随交换链销毁。
验证层错误
为了检查我们的验证层是否正常工作,让我们尝试以错误的顺序调用销毁函数
void VulkanEngine::cleanup()
{
if (_isInitialized) {
//ERROR - Instance destroyed before others
vkDestroyInstance(_instance, nullptr);
destroy_swapchain();
vkDestroyDevice(_device, nullptr);
vkDestroySurfaceKHR(_instance, _surface, nullptr);
vkb::destroy_debug_utils_messenger(_instance, _debug_messenger);
SDL_DestroyWindow(_window);
}
}
我们现在在销毁设备和表面(从实例创建)之前销毁实例。验证层应该会报错,如下所示。
[ERROR: Validation]
Validation Error: [ VUID-vkDestroyInstance-instance-00629 ] Object 0: handle = 0x24ff02340c0, type = VK_OBJECT_TYPE_INSTANCE; Object 1: handle = 0xf8ce070000000002, type = VK_OBJECT_TYPE_SURFACE_KHR; | MessageID = 0x8b3d8e18 | OBJ ERROR : For VkInstance 0x24ff02340c0[], VkSurfaceKHR 0xf8ce070000000002[] has not been destroyed. The Vulkan spec states: All child objects created using instance must have been destroyed prior to destroying instance (https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VUID-vkDestroyInstance-instance-00629)
随着 Vulkan 初始化完成且图层正常工作,我们可以开始准备命令结构,以便我们可以让 GPU 做一些事情。
下一步: 执行 Vulkan 命令