狸村 Mystic Web · 狸村
目录

Vulkan-hpp 与指定初始化器

注意:我们将使用 C++20 引入的指定初始化器。 默认情况下,Vulkan-hpp 使用不同的初始化方式,我们需要通过定义 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS 来显式启用此功能。

这能让我们更清晰地理解每个选项在所依赖结构体中的含义。 对于本教程,该定义已在 CMake 构建配置 中声明。

如果你使用不同的构建配置,或者想从头开始编写代码,你需要在包含 Vulkan-hpp 头文件之前手动定义此宏,如下所示:

#define VULKAN_HPP_NO_STRUCT_CONSTRUCTORS
#include <vulkan/vulkan.hpp>
// 或
#include <vulkan/vulkan_raii.hpp>

整体结构

在上一章中,你已经创建了一个配置正确的 Vulkan 项目,并用示例代码进行了测试。 在本章中,我们将从以下代码开始,从头构建:

#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()
{
    try
    {
        HelloTriangleApplication app;
        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; 而非包含头文件。引入 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 编程实践。本教程不仅会使用 RAII,还会努力展示最新的方法和扩展,希望能让 Vulkan 的使用成为一种享受。仅仅因为我们喜欢使用底层图形 API,并不意味着我们应该把学习门槛设得太高。在适当的地方,我们会讨论资源管理(释放资源)的注意事项。不过,在本教程中,我们将展示使用一个基本的析构函数来完成清理工作就能解决大部分问题。

Vulkan 对象要么通过 vkCreateXXX 等函数直接创建,要么通过 vkAllocateXXX 等函数从另一个对象中分配。在确保对象不再被任何地方使用后,你需要使用对应的 vkDestroyXXXvkFreeXXX 来销毁它。这些函数的参数通常因对象类型而异,但它们都有一个共同的参数:pAllocator。这是一个可选参数,允许你为自定义内存分配器指定回调。在本教程中我们将忽略这个参数,始终传递 nullptr

使用 Vulkan_hpp RAII 模块,我们可以依赖库来负责 vkCreateXXXvkAllocateXXXvkDestroyXXXvkFreeXXX 的调用,因此类似下面这样的代码块:

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;
createInfo.ppEnabledLayerNames = nullptr;

if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
    throw std::runtime_error("failed to create instance!");
}

vkDestroyInstance(instance, nullptr);

可以直接替换为:

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 表面,但它直接使用 C 语言表面接口。除此之外,我们可以使用 GLFW_INCLUDE_NONE 或不进行该项指定,其他一切都能正常工作。

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

这样,GLFW 将包含其自己的定义,并自动加载 Vulkan C 头文件。添加一个 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; 私有类成员来存储对它的引用:

private:
    GLFWwindow *window = nullptr;

并使用以下代码初始化窗口:

window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);

前三个参数指定窗口的宽度、高度和标题。第四个参数允许你指定一个监视器来打开窗口,最后一个参数仅与 OpenGL 相关。

最好使用常量而不是硬编码的宽度和高度数值,因为将来我们会多次引用这些值。我在 HelloTriangleApplication 类定义之前添加了以下行:

constexpr uint32_t WIDTH = 800;
constexpr 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();
}

请注意,在本教程中,这是我们最后一次需要在 cleanup() 函数中手动执行任何操作。这段代码以后再也不需要更改了。

现在运行程序,你应该会看到一个标题为 Vulkan 的窗口出现,直到你关闭窗口程序才会终止。现在我们有了 Vulkan 应用程序的框架,让我们 创建第一个 Vulkan 对象

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。