总体结构

在上一章中,您已经创建了一个 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 头文件,它提供了函数、结构体和枚举。包含 stdexceptiostream 头文件是为了报告和传播错误。cstdlib 头文件提供了 EXIT_SUCCESSEXIT_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_ptrstd::shared_ptr 提供自定义删除器,具体取决于您的所有权要求。RAII 是大型 Vulkan 程序的推荐模型,但出于学习目的,了解幕后发生的事情总是好的。

Vulkan 对象可以直接使用诸如 vkCreateXXX 之类的函数创建,或者通过另一个对象使用诸如 vkAllocateXXX 之类的函数分配。在确保对象在任何地方都不再使用后,您需要使用对应的 vkDestroyXXXvkFreeXXX 销毁它。这些函数的参数通常因对象类型的不同而异,但它们都共享一个参数: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 对象

C++ 代码