基础代码

总体结构

在前一章中,您创建了一个具有所有正确配置的 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 调用的新函数,以及一个或多个新的 Vulkan 对象到私有类成员中,这些对象需要在 cleanup 末尾释放。

资源管理

就像每个使用 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 对象吧!