Drawing a triangle -- setup -- Base code

Vulkan-hpp 与指定初始化器

我们将使用 C++ 20 引入的指定初始化器 。默认情况下,Vulkan-hpp 采用不同的初始化方式,因此需要通过定义 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS 宏来显式启用该特性。

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

若你使用其他构建配置或希望从零编写代码,需在包含 Vulkan-hpp 头文件前手动定义该宏,如下所示:

cpp 复制代码
#define VULKAN_HPP_NO_STRUCT_CONSTRUCTORS
#include <vulkan/vulkan.hpp>
// 或
#include <vulkan/vulkan_raii.hpp>

总体结构

在上一章中,你已创建了配置齐全的 Vulkan 项目,并通过示例代码完成了测试。本章将从零开始,基础代码如下:

cpp 复制代码
#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() {
    HelloTriangleApplication app;

    try {
        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 操作。例如,以下代码:

cpp 复制代码
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;

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

vkDestroyInstance(instance, nullptr);

可直接替换为:

cpp 复制代码
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 Surface(表面),但直接使用 C 语言版本的 Surface。除此之外,你也可以使用 GLFW_INCLUDE_NONE 或不指定该宏,其他功能均能正常工作。

cpp 复制代码
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

这样,GLFW 会包含自身的定义,并自动加载 Vulkan C 语言头文件。添加 initWindow 函数,并在 run 函数中其他调用之前添加对它的调用 ------ 我们将通过该函数初始化 GLFW 并创建窗口。

cpp 复制代码
void run() {
    initWindow();
    initVulkan();
    mainLoop();
    cleanup();
}

private:
    void initWindow() {
    }

initWindow 中的第一个调用应为 glfwInit(),用于初始化 GLFW 库。由于 GLFW 最初是为创建 OpenGL 上下文设计的,我们需要通过后续调用告知它不要创建 OpenGL 上下文:

cpp 复制代码
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

窗口大小调整需要特殊处理(后续会详细说明),因此现在通过另一个窗口提示调用禁用该功能:

cpp 复制代码
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

接下来只需创建实际窗口即可。添加私有类成员 GLFWwindow* window; 用于存储窗口引用,并通过以下代码初始化窗口:

cpp 复制代码
window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);

前三个参数分别指定窗口的宽度、高度和标题。第四个参数允许你可选地指定窗口要显示的显示器,最后一个参数仅与 OpenGL 相关。

建议使用常量代替硬编码的宽度和高度数值,因为后续会多次引用这些值。在 HelloTriangleApplication 类定义之前添加以下代码:

cpp 复制代码
constexpr uint32_t WIDTH = 800;
constexpr uint32_t HEIGHT = 600;

并将窗口创建调用替换为:

cpp 复制代码
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);

此时,你的 initWindow 函数应如下所示:

cpp 复制代码
void initWindow() {
    glfwInit();

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}

为了让应用程序持续运行直至发生错误或窗口关闭,需在 mainLoop 函数中添加事件循环:

cpp 复制代码
void mainLoop() {
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
    }
}

这段代码的逻辑非常直观:循环检查事件(如点击关闭按钮),直至用户关闭窗口。后续我们将在这个循环中调用渲染单帧的函数。

窗口关闭后,需要清理资源:销毁窗口并终止 GLFW。这是我们的第一段 cleanup 代码:

cpp 复制代码
void cleanup() {
    glfwDestroyWindow(window);

    glfwTerminate();
}

注意:本教程中,这是最后一次需要修改 cleanup() 函数的内容 ------ 这段代码此后无需再更改。

现在运行程序,你将看到一个标题为 Vulkan 的窗口,关闭窗口即可终止应用程序。既然我们已经搭建好 Vulkan 应用程序的框架,接下来就让我们创建第一个 Vulkan 对象吧!

相关推荐
txinyu的博客2 小时前
unique_ptr shared_ptr weak_ptr的线程安全问题
c++·安全
Howrun7772 小时前
虚幻引擎_用户小控件_准星
c++·游戏引擎·虚幻
CoderCodingNo2 小时前
【GESP】C++六级考试大纲知识点梳理, (1) 树的概念与遍历
开发语言·c++
星火开发设计2 小时前
C++ multimap 全面解析与实战指南
java·开发语言·数据结构·c++·学习·知识
李日灐2 小时前
C++STL:deque、priority_queue详解!!:详解原理和底层
开发语言·数据结构·c++·后端·stl
羑悻的小杀马特3 小时前
etcd实战指南:从安装集群到C++封装,解锁分布式服务治理的“钥匙”
c++·分布式·etcd·集群
星火开发设计3 小时前
C++ deque 全面解析与实战指南
java·开发语言·数据结构·c++·学习·知识
Yu_Lijing3 小时前
基于C++的《Head First设计模式》笔记——单件模式
c++·笔记·设计模式
点云SLAM3 小时前
C++设计模式之单例模式(Singleton)以及相关面试问题
c++·设计模式·面试·c++11·单例模式(singleton)