Link

初始化核心 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 命令