基础代码
总体结构
在前一章中,您创建了一个具有所有正确配置的 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
调用的新函数,以及一个或多个新的 Vulkan 对象到私有类成员中,这些对象需要在 cleanup
末尾释放。
资源管理
就像每个使用 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 对象吧!