起始点
这些解释假设您从 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 命令