基础代码
总体结构
在上一章中,您已经创建了一个 Vulkan 项目,其中包含所有正确的配置,并使用示例代码进行了测试。在本章中,我们将从以下代码从头开始
#include <vulkan/vulkan.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;
}
我们首先包含来自 LunarG SDK 的 Vulkan 头文件,它提供了函数、结构体和枚举。包含 stdexcept
和 iostream
头文件是为了报告和传播错误。cstdlib
头文件提供了 EXIT_SUCCESS
和 EXIT_FAILURE
宏。
程序本身被包装在一个类中,我们将在其中存储 Vulkan 对象作为私有类成员,并添加函数来初始化它们中的每一个,这些函数将从 initVulkan
函数中调用。一旦一切准备就绪,我们就进入主循环以开始渲染帧。我们将填充 mainLoop
函数,使其包含一个循环,该循环将迭代直到窗口在稍后关闭。一旦窗口关闭并且 mainLoop
返回,我们将确保在 cleanup
函数中释放我们使用的资源。
如果在执行期间发生任何类型的致命错误,我们将抛出一个带有描述性消息的 std::runtime_error
异常,该异常将传播回 main
函数并打印到命令提示符。为了处理各种标准异常类型,我们还捕获更通用的 std::exception
。我们将很快处理的一个错误示例是发现不支持某个必需的扩展。
大约在接下来的每一章中,都将添加一个新函数,该函数将从 initVulkan
调用,并且会向需要在最后在 cleanup
中释放的私有类成员添加一个或多个新的 Vulkan 对象。
资源管理
正如使用 malloc
分配的每块内存都需要调用 free
一样,我们创建的每个 Vulkan 对象在不再需要时都需要显式销毁。在 C++ 中,可以使用 RAII 或 <memory>
头文件中提供的智能指针来执行自动资源管理。但是,在本教程中,我选择显式说明 Vulkan 对象的分配和释放。毕竟,Vulkan 的特点是对每个操作都显式说明以避免错误,因此显式说明对象的生命周期以了解 API 的工作原理是很好的。
在学习完本教程后,您可以通过编写 C++ 类来实现自动资源管理,这些类在其构造函数中获取 Vulkan 对象并在其析构函数中释放它们,或者通过为 std::unique_ptr
或 std::shared_ptr
提供自定义删除器,具体取决于您的所有权要求。RAII 是大型 Vulkan 程序的推荐模型,但出于学习目的,了解幕后发生的事情总是好的。
Vulkan 对象可以直接使用诸如 vkCreateXXX
之类的函数创建,或者通过另一个对象使用诸如 vkAllocateXXX
之类的函数分配。在确保对象在任何地方都不再使用后,您需要使用对应的 vkDestroyXXX
和 vkFreeXXX
销毁它。这些函数的参数通常因对象类型的不同而异,但它们都共享一个参数:pAllocator
。这是一个可选参数,允许您为自定义内存分配器指定回调。在本教程中,我们将忽略此参数,并始终传递 nullptr
作为参数。
集成 GLFW
如果您想将 Vulkan 用于离屏渲染,即使不创建窗口也可以完美运行,但实际显示一些东西会令人兴奋得多!首先将 #include <vulkan/vulkan.h>
行替换为
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
这样 GLFW 将包含自己的定义并自动加载 Vulkan 头文件。添加一个 initWindow
函数,并在其他调用之前从 run
函数中添加对其的调用。我们将使用该函数来初始化 GLFW 并创建一个窗口。
void run() {
initWindow();
initVulkan();
mainLoop();
cleanup();
}
private:
void initWindow() {
}
initWindow
中的第一个调用应该是 glfwInit()
,它初始化 GLFW 库。由于 GLFW 最初设计用于创建 OpenGL 上下文,我们需要通过后续调用告诉它不要创建 OpenGL 上下文
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
因为处理调整大小的窗口需要特别注意,我们将在稍后研究,所以现在使用另一个窗口提示调用禁用它
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
现在剩下要做的就是创建实际的窗口。添加一个 GLFWwindow* window;
私有类成员来存储对其的引用,并使用以下代码初始化窗口
window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);
前三个参数指定窗口的宽度、高度和标题。第四个参数允许您选择性地指定要在其上打开窗口的监视器,最后一个参数仅与 OpenGL 相关。
使用常量而不是硬编码的宽度和高度数字是一个好主意,因为将来我们将多次引用这些值。我在 HelloTriangleApplication
类定义上方添加了以下行
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
并将窗口创建调用替换为
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
您现在应该有一个看起来像这样的 initWindow
函数
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
为了使应用程序运行直到发生错误或窗口关闭,我们需要按如下方式向 mainLoop
函数添加一个事件循环
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
此代码应该是不言自明的。它循环并检查事件,例如按下 X 按钮,直到窗口被用户关闭。这也是我们稍后将调用函数来渲染单帧的循环。
一旦窗口关闭,我们需要通过销毁它并终止 GLFW 本身来清理资源。这将是我们的第一个 cleanup
代码
void cleanup() {
glfwDestroyWindow(window);
glfwTerminate();
}
现在运行程序,您应该看到一个标题为 Vulkan
的窗口出现,直到应用程序通过关闭窗口终止。现在我们有了 Vulkan 应用程序的框架,让我们创建第一个 Vulkan 对象!