Vulkan-hpp 与指定初始化器
我们将使用 C++ 20 引入的指定初始化器 。默认情况下,Vulkan-hpp 采用不同的初始化方式,因此需要通过定义 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS 宏来显式启用该特性。
这能让我们更清晰地了解所依赖结构体中每个选项的对应含义。本教程中,该宏已在 CMake 构建配置中声明。
若你使用其他构建配置或希望从零编写代码,需在包含 Vulkan-hpp 头文件前手动定义该宏,如下所示:
cpp
#define VULKAN_HPP_NO_STRUCT_CONSTRUCTORS
#include <vulkan/vulkan.hpp>
// 或
#include <vulkan/vulkan_raii.hpp>
总体结构
在上一章中,你已创建了配置齐全的 Vulkan 项目,并通过示例代码完成了测试。本章将从零开始,基础代码如下:
cpp
#if defined(__INTELLISENSE__) || !defined(USE_CPP20_MODULES)
#include <vulkan/vulkan_raii.hpp>
#else
import vulkan_hpp;
#endif
#include <GLFW/glfw3.h>
#include <iostream>
#include <stdexcept>
#include <cstdlib>
class HelloTriangleApplication {
public:
void run() {
initVulkan();
mainLoop();
cleanup();
}
private:
void initVulkan() {
}
void mainLoop() {
}
void cleanup() {
}
};
int main() {
HelloTriangleApplication app;
try {
app.run();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
默认情况下,我们首先包含 Vulkan-Hpp RAII 头文件,该文件提供了相关函数、结构体和枚举类型。若启用 C++20 模块(通过 -DENABLE_CPP20_MODULE=ON 编译选项),代码将通过 CMake 定义的 USE_CPP20_MODULES 宏,改用 import vulkan_hpp; 导入模块。stdexcept 和 iostream 头文件用于错误报告和传播,cstdlib 头文件提供了 EXIT_SUCCESS 和 EXIT_FAILURE 宏。
程序本身被封装在一个类中,我们将 Vulkan 对象存储为私有类成员,并添加初始化函数(将在 initVulkan 中调用)。所有准备工作完成后,程序进入主循环开始渲染帧。稍后我们将完善 mainLoop 函数,添加一个循环直至窗口关闭。窗口关闭且 mainLoop 返回后,我们会在 cleanup 函数中释放所使用的资源。
若执行过程中发生任何致命错误,将抛出带有描述信息的 std::runtime_error 异常,该异常会传播回 main 函数并打印到命令行。为了处理多种标准异常类型,我们捕获更通用的 std::exception。例如,后续会遇到的 "所需扩展不受支持" 就属于此类错误。
本章之后的几乎每一章,都会新增一个从 initVulkan 调用的函数,并在私有类成员中添加一个或多个新的 Vulkan 对象,这些对象最终需要在 cleanup 中释放。
资源管理
与 malloc 分配的内存需要通过 free 释放类似,我们创建的每个 Vulkan 对象在不再需要时都必须显式销毁。在 C++ 中,可以使用 RAII(资源获取即初始化)或 <memory> 头文件提供的智能指针实现自动资源管理。本教程旨在简化 Vulkan 的使用流程,展示现代 Vulkan 编程方式 ------ 不仅会使用 RAII 与智能指针,还会尝试演示最新的方法和扩展,让 Vulkan 变得易于使用。尽管我们热衷于底层图形 API,但不应为其学习设置过高门槛。在适当的场景下,我们会讨论资源释放相关的注意事项,但本教程将首先通过一个基础析构函数来完成资源清理工作,展示其可行性。
Vulkan 对象的创建方式有两种:一是通过 vkCreateXXX 类函数直接创建,二是通过 vkAllocateXXX 类函数从其他对象中分配。确保对象不再被任何地方使用后,需通过对应的 vkDestroyXXX 和 vkFreeXXX 函数销毁。这些函数的参数因对象类型而异,但均包含一个共同参数 pAllocator------ 这是一个可选参数,允许你指定自定义内存分配器的回调函数。本教程中我们将忽略该参数,始终传入 nullptr 作为实参。
通过 Vulkan_hpp RAII 模块,我们可以依赖库自动处理 vkCreateXXX、vkAllocateXXX、vkDestroyXXX 和 vkFreeXXX 操作。例如,以下代码:
cpp
vkInstance instance;
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = 0;
createInfo.ppEnabledExtensionNames = nullptr;
createInfo.enabledLayerCount = 0;
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
vkDestroyInstance(instance, nullptr);
可直接替换为:
cpp
constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle",
.applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.pEngineName = "No Engine",
.engineVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.apiVersion = vk::ApiVersion14 };
vk::InstanceCreateInfo createInfo{
.pApplicationInfo = &appInfo
};
instance = vk::raii::Instance(context, createInfo);
集成 GLFW
Vulkan 无需创建窗口即可用于离屏渲染,但实际显示内容会更具趣味性!首先,我们添加 GLFW:
注意:我们将继续使用
GLFW_INCLUDE_VULKAN,因为 GLFW 设计用于获取 Vulkan Surface(表面),但直接使用 C 语言版本的 Surface。除此之外,你也可以使用GLFW_INCLUDE_NONE或不指定该宏,其他功能均能正常工作。
cpp
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
这样,GLFW 会包含自身的定义,并自动加载 Vulkan C 语言头文件。添加 initWindow 函数,并在 run 函数中其他调用之前添加对它的调用 ------ 我们将通过该函数初始化 GLFW 并创建窗口。
cpp
void run() {
initWindow();
initVulkan();
mainLoop();
cleanup();
}
private:
void initWindow() {
}
initWindow 中的第一个调用应为 glfwInit(),用于初始化 GLFW 库。由于 GLFW 最初是为创建 OpenGL 上下文设计的,我们需要通过后续调用告知它不要创建 OpenGL 上下文:
cpp
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
窗口大小调整需要特殊处理(后续会详细说明),因此现在通过另一个窗口提示调用禁用该功能:
cpp
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
接下来只需创建实际窗口即可。添加私有类成员 GLFWwindow* window; 用于存储窗口引用,并通过以下代码初始化窗口:
cpp
window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);
前三个参数分别指定窗口的宽度、高度和标题。第四个参数允许你可选地指定窗口要显示的显示器,最后一个参数仅与 OpenGL 相关。
建议使用常量代替硬编码的宽度和高度数值,因为后续会多次引用这些值。在 HelloTriangleApplication 类定义之前添加以下代码:
cpp
constexpr uint32_t WIDTH = 800;
constexpr uint32_t HEIGHT = 600;
并将窗口创建调用替换为:
cpp
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
此时,你的 initWindow 函数应如下所示:
cpp
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
为了让应用程序持续运行直至发生错误或窗口关闭,需在 mainLoop 函数中添加事件循环:
cpp
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
这段代码的逻辑非常直观:循环检查事件(如点击关闭按钮),直至用户关闭窗口。后续我们将在这个循环中调用渲染单帧的函数。
窗口关闭后,需要清理资源:销毁窗口并终止 GLFW。这是我们的第一段 cleanup 代码:
cpp
void cleanup() {
glfwDestroyWindow(window);
glfwTerminate();
}
注意:本教程中,这是最后一次需要修改
cleanup()函数的内容 ------ 这段代码此后无需再更改。
现在运行程序,你将看到一个标题为 Vulkan 的窗口,关闭窗口即可终止应用程序。既然我们已经搭建好 Vulkan 应用程序的框架,接下来就让我们创建第一个 Vulkan 对象吧!