Link

起始点

这些解释假设您从 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"

接下来我们要做的是在 vk_engine.cpp 的顶部添加一个 VK_CHECK 宏。只要 Vulkan 错误未处理,这将立即崩溃,因为错误很可能发生在这里,所以很有用。

#include <iostream>

//we want to immediately abort when there is an error. In normal engines this would give an error message to the user, or perform a dump of state.
using namespace std;
#define VK_CHECK(x)                                                 \
	do                                                              \
	{                                                               \
		VkResult err = x;                                           \
		if (err)                                                    \
		{                                                           \
			std::cout <<"Detected Vulkan error: " << err << std::endl; \
			abort();                                                \
		}                                                           \
	} while (0)

所有可能出错的 Vulkan 函数都将返回 VkResult。这实际上只是一个整数错误代码。如果错误代码不是 0,则表示情况不妙,我们只需中止并显示错误消息。

完成这两项操作后,我们可以继续初始化 Vulkan 的基本结构。

初始化核心 Vulkan 结构

我们要初始化的第一件事是 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();

vk_engine.cpp

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

	//load the core Vulkan structures
	init_vulkan();

	//everything went fine
	_isInitialized = true;
}

void VulkanEngine::init_vulkan()
{
    //nothing yet
}

我们已向主类添加了 4 个句柄,VkDevice、VkPhysicalDevice、VkInstance、VkDebugUtilsMessengerEXT。

实例

现在我们添加了新的 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(true)
		.require_api_version(1, 1, 0)
		.use_default_debug_messenger()
		.build();

	vkb::Instance vkb_inst = inst_ret.value();

	//store the instance
	_instance = vkb_inst.instance;
	//store the debug messenger
	_debug_messenger = vkb_inst.debug_messenger;

我们将创建一个 vkb::InstanceBuilder,它来自 VkBootstrap 库,并简化了 Vulkan VkInstance 的创建。

对于实例的创建,我们希望它具有名称“Example Vulkan Application”,启用验证层,并使用默认调试记录器。“Example Vulkan Application”名称完全没有意义。如果您想将其更改为任何名称,都不会有问题。初始化 VkInstance 时,会提供应用程序和引擎的名称。这是为了让驱动程序供应商可以检测 AAA 游戏的名称,以便他们可以专门为这些游戏调整内部驱动程序逻辑。对于普通人来说,这真的不重要。

我们希望默认情况下硬编码启用验证层。根据我们在指南中要做的事情,永远不需要关闭它们,因为它们会很好地捕获我们的错误。在更高级的引擎上,您只会以调试模式或使用特定配置参数启用这些层。

我们还需要 Vulkan api 版本 1.1。

最后,我们告诉库我们需要调试 messenger。这就是捕获验证层将输出的日志消息的内容。因为我们不需要专用的 messenger,我们将让库使用一个直接输出到控制台的 messenger。

然后,我们只需从 vkb::Instance 对象中获取实际的 VkInstance 句柄和 VkDebugUtilsMessengerEXT 句柄。我们存储 VkDebugUtilsMessengerEXT,以便在程序退出时销毁它,否则我们会泄漏它。

设备

	// get the surface of the window we opened with SDL
	SDL_Vulkan_CreateSurface(_window, _instance, &_surface);

	//use vkbootstrap to select a GPU.
	//We want a GPU that can write to the SDL surface and supports Vulkan 1.1
	vkb::PhysicalDeviceSelector selector{ vkb_inst };
	vkb::PhysicalDevice physicalDevice = selector
		.set_minimum_version(1, 1)
		.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。

对于 GPU 选择器,我们只需要 Vulkan 1.1 支持和窗口表面,所以没有太多要查找的。该库将确保选择系统中的专用 GPU。

一旦我们有了 VkPhysicalDevice,我们就可以直接从中构建 VkDevice。

最后,我们将句柄存储在类中。

就这样,我们已经初始化了 Vulkan。我们现在可以开始调用 Vulkan 命令了。

但在我们开始执行命令之前,还有最后一件事要做。

设置交换链

核心初始化的最后一件事是初始化交换链,以便我们可以拥有一些东西来渲染到其中。

首先向 VulkanEngine 添加新成员和函数

class VulkanEngine {
public:
	// --- other code ---
	VkSwapchainKHR _swapchain; // from other articles

	// image format expected by the windowing system
	VkFormat _swapchainImageFormat;

	//array of images from the swapchain
	std::vector<VkImage> _swapchainImages;

	//array of image-views from the swapchain
	std::vector<VkImageView> _swapchainImageViews;

	// --- other code ---
private:
	// --- other code ---
	void init_swapchain();
}

我们正在存储 VkSwapchainKHR 本身,以及交换链图像在渲染到它们时使用的格式。

我们还存储了 2 个数组,一个是 Images 数组,另一个是 ImageViews 数组。

VkImage 是实际图像对象的句柄,用作纹理或渲染到其中。VkImageView 是该图像的包装器。它允许执行诸如交换颜色之类的操作。我们将在第 5 章解释纹理时详细介绍它。

我们在主 init 函数中调用 init_swapchain(),紧跟在调用 init_vulkan() 之后

vk_engine.cpp

void VulkanEngine::init()
{
	// --- SDL stuff ----

	//load the core Vulkan structures
	init_vulkan();

	//create the swapchain
	init_swapchain();

	//everything went fine
	_isInitialized = true;
}

void VulkanEngine::init_swapchain()
{
    //nothing yet
}

与其他初始化函数一样,我们将使用 vkb 库来创建交换链。

void VulkanEngine::init_swapchain()
{
	vkb::SwapchainBuilder swapchainBuilder{_chosenGPU,_device,_surface };

	vkb::Swapchain vkbSwapchain = swapchainBuilder
		.use_default_format_selection()
		//use vsync present mode
		.set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR)
		.set_desired_extent(_windowExtent.width, _windowExtent.height)
		.build()
		.value();

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

	_swapchainImageFormat = vkbSwapchain.image_format;

}

这里最重要的细节是 present mode,我们已将其设置为 VK_PRESENT_MODE_FIFO_KHR。这样我们就进行了硬 VSync,这将把整个引擎的 FPS 限制为显示器的速度。现在这是一个限制 FPS 的好方法。

我们还将窗口大小发送到交换链。这很重要,因为创建交换链也会为其创建图像,因此大小是锁定的。如果您需要调整窗口大小,则需要重建交换链。

构建交换链后,我们只需将其所有内容存储到 VulkanEngine 类的成员中。

清理资源

我们需要确保在我们退出应用程序时,正确删除我们创建的所有 Vulkan 资源。

为此,请转到 VulkanEngine::cleanup() 函数

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

		vkDestroySwapchainKHR(_device, _swapchain, nullptr);

		//destroy swapchain resources
		for (int i = 0; i < _swapchainImageViews.size(); i++) {

			vkDestroyImageView(_device, _swapchainImageViews[i], nullptr);
		}

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

必须以与创建对象相反的顺序销毁对象。在某些情况下,如果您知道自己在做什么,则可以稍微更改顺序,这也可以,但是以相反的顺序销毁对象是一种使其工作的简单方法。

VkPhysicalDevice 无法销毁,因为它本身不是 Vulkan 资源,它更像是系统中 GPU 的句柄。

由于我们的初始化顺序是 SDL Window -> Instance -> Surface -> Device -> Swapchain,因此我们正在对销毁执行完全相反的顺序。

如果您现在尝试运行该程序,它应该什么也不做,但什么也不做也包括不发出错误。

在这种特定情况下,无需销毁 Images,因为图像由交换链拥有并在交换链销毁时销毁。

验证层错误

只是为了检查我们的验证层是否正常工作,让我们尝试以错误的顺序调用销毁函数

void VulkanEngine::cleanup()
{
	if (_isInitialized) {
		//ERROR - Instance destroyed before others
		vkDestroyInstance(_instance, nullptr);

		vkDestroySwapchainKHR(_device, _swapchain, nullptr);

		//destroy swapchain resources
		for (int i = 0; i < _swapchainImageViews.size(); i++) {

			vkDestroyImageView(_device, _swapchainImageViews[i], nullptr);
		}

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

我们现在在 Device 和 Surface 之前销毁 Instance(Surface 是从 Instance 创建的),Surface 也被删除了。验证层应该会报错,如下所示。

[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 初始化完成并且图层正常工作,我们可以开始准备实际的渲染循环和命令执行。

下一步:Vulkan 命令