从 OpenGL 到 Vulkan

Vulkan 被视作是 OpenGL 的后续产品,由 Khronos Group 维护。Vulkan 基于 Mantle(第一个全新的低层次图形 API)。Mantle 由 AMD 开发而成,专为 Radeon 卡架构而设计。尽管是第一个公开发布的 API,但使用 Mantle 的游戏在基准测试中均显著提升了性能。后来其它产商陆续发布了其他低层次的图形 API,比如 Microsoft 的 DirectX 12、Apple 的 Metal,以及现在的 Vulkan。

OpenGL 等高层次 API 使用起来非常简单。开发人员只需声明操作内容和操作方式,剩下的都由驱动程序来完成 。驱动程序检查开发人员是否正确使用 API 调用、是否传递了正确的参数,以及是否充分准备了状态。如果出现问题,将提供反馈。为实现其易用性,许多任务必须由驱动程序在「后台」执行。

在低层次图形 API 中,开发人员需要负责完成大部分任务 。他们需要符合严格的编程和使用规则,还必须编写大量代码。但这种做法是合理的。开发人员知道他们的操作内容和希望实现的目的,但驱动程序不知道。因此使用传统 API 时,驱动程序必须完成更多工作,以便程序正常运行。采用 Vulkan 等 API 可避免这些额外的工作。因此 DirectX 12、Metal 或 Vulkan 也被称为精简驱动程序/精简 API。大部分时候它们仅将用户请求传输至硬件,仅提供硬件的精简抽象层。为显著提升性能,驱动程序几乎不执行任何操作。

Vulkan 的主要设计目标是提升性能,其提升性能的方式之一是减少显卡驱动程序执行状态和错误检查操作的次数。 为了能获得必要的提示,一般会在调试模式中加入 验证层 提前验证程序是否正确。Vulkan 的验证层含有一系列软件库,这些软件库能够帮助用户在开发过程中发现应用程序的潜在问题。它们的调试功能包括:验证传输给Vulkan 函数的参数、验证纹理和渲染目标格式、跟踪 Vulkan 对象并监视它们的生命周期和使用情况、监视潜在的内存泄露和通过调用Vulkan函数输出(显示/打印)数据的情况等。

SDK

Vulkan 可以在 Windows 7(以及更新的版本)和 Linux(Ubuntu 16.04或更新的版本)操作系统中使用。

开发使用 Vulkan 的应用程序前,需要安装 SDK

安装 Vulkan SDK

从 Vulkan 模板开始

安装完 Vulkan 目录后,有 Visual Studio 的模板,将模板拷贝到 Visual Studio 的模板目录下(C:\Users\username\Documents\Visual Studio YYYY\Templates\ProjectTemplates\Visual C++ Project)。

Vulkan 模板

拷贝到 Visual Studio 下面:

Visual Studio 模板

可以看到有 4 个项目模板,我们创建一个基于 VulkanCppWindowedProgram 的模板。它基于 SDL 库实现窗口。由于项目模板没有拷贝 SDL2.dll 到程序下,所以直接运行项目会出现 SDL2.dll 缺失的问题。这个问题比较简单,只需要去 Vulkan SDL 的 Bin 目录将 SDL2.dll 拷贝到程序目录下即可。

这样运行的时候就不会出现 .dll 缺失,可以看到一个黑色的背景窗口。

Vulkan 模板运行效果

目前是 Debug 版本,对于 Release 同样需要拷贝 dll。如果嫌比较麻烦,可以把 .dll 放到代码目录,然后配置生成后事件进行复制。

配置生成后事件

重新调整代码结构

模板里基于 SDL 代码都放到 main 中,不妨按照之前的模式重新调整一下。创建一个 Game.hGame.cpp 重新调整一下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// Game.h
#pragma once

// Enable the WSI extensions
#if defined(__ANDROID__)
#define VK_USE_PLATFORM_ANDROID_KHR
#elif defined(__linux__)
#define VK_USE_PLATFORM_XLIB_KHR
#elif defined(_WIN32)
#define VK_USE_PLATFORM_WIN32_KHR
#endif

#include <glm/glm.hpp>
#include <SDL2/SDL.h>
#include <SDL2/SDL_syswm.h>
#include <SDL2/SDL_vulkan.h>
#include <vulkan/vulkan.hpp>

#include <iostream>
#include <vector>

// Game class
class Game
{
public:
Game();
// 初始化游戏
bool Initialize();
// 运行游戏循环直到游戏结束
void RunLoop();
// 关闭游戏
void Shutdown();
private:
// 处理进程输入
void ProcessInput();
// 更新游戏
void UpdateGame();
// 生成输出
void GenerateOutput();

// 通过 SDL 创建窗体
SDL_Window* mWindow{ nullptr };

// 继续运行
bool mIsRunning{ true };

private:
// Use validation layers if this is a debug build
std::vector<const char*> layers;
// Create the Vulkan instance.
vk::Instance instance;
// Create a Vulkan surface for rendering
VkSurfaceKHR c_surface;
vk::SurfaceKHR surface{ c_surface };
};

调整代码后的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include "Game.h"

Game::Game()
{
#if defined(_DEBUG)
layers.push_back("VK_LAYER_KHRONOS_validation");
#endif
}

bool Game::Initialize()
{
// Create an SDL window that supports Vulkan rendering.
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("Could not initialize SDL.");
return false;
}

mWindow = SDL_CreateWindow("Vulkan Window", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, 1280, 720, SDL_WINDOW_VULKAN);
if (mWindow == nullptr) {
SDL_Log("Could not create SDL window.");
return false;
}

// Get WSI extensions from SDL (we can add more if we like - we just can't remove these)
unsigned extension_count;
if (!SDL_Vulkan_GetInstanceExtensions(mWindow, &extension_count, nullptr)) {
SDL_Log("Could not get the number of required instance extensions from SDL.");
return false;
}

std::vector<const char*> extensions(extension_count);
if (!SDL_Vulkan_GetInstanceExtensions(mWindow, &extension_count, extensions.data())) {
SDL_Log("Could not get the names of required instance extensions from SDL.");
return false;
}

// vk::ApplicationInfo allows the programmer to specifiy some basic information about the
// program, which can be useful for layers and tools to provide more debug information.
vk::ApplicationInfo appInfo = vk::ApplicationInfo()
.setPApplicationName("Vulkan C++ Windowed Program Template")
.setApplicationVersion(1)
.setPEngineName("LunarG SDK")
.setEngineVersion(1)
.setApiVersion(VK_API_VERSION_1_0);

// vk::InstanceCreateInfo is where the programmer specifies the layers and/or extensions that
// are needed.
vk::InstanceCreateInfo instInfo = vk::InstanceCreateInfo()
.setFlags(vk::InstanceCreateFlags())
.setPApplicationInfo(&appInfo)
.setEnabledExtensionCount(static_cast<uint32_t>(extensions.size()))
.setPpEnabledExtensionNames(extensions.data())
.setEnabledLayerCount(static_cast<uint32_t>(layers.size()))
.setPpEnabledLayerNames(layers.data());

try {
instance = vk::createInstance(instInfo);
}
catch (const std::exception& e) {
SDL_Log("Could not create a Vulkan instance: %s", e.what());
return false;
}

if (!SDL_Vulkan_CreateSurface(mWindow, static_cast<VkInstance>(instance), &c_surface)) {
SDL_Log("Could not create a Vulkan surface.");
return false;
}

return true;
}

void Game::RunLoop()
{
while (mIsRunning)
{
ProcessInput();
UpdateGame();
GenerateOutput();
}
}

void Game::Shutdown()
{
// Clean up.
instance.destroySurfaceKHR(surface);
SDL_DestroyWindow(mWindow);
SDL_Quit();
instance.destroy();
}

void Game::ProcessInput()
{
SDL_Event event;

// 有 event 在队列就一直循环
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
mIsRunning = false;
break;
default:
break;
}
}

// 获取键盘的状态
const Uint8* state = SDL_GetKeyboardState(nullptr);
// 如果按了 Esc,结束循环
if (state[SDL_SCANCODE_ESCAPE])
{
mIsRunning = false;
}
}

void Game::UpdateGame()
{
}

void Game::GenerateOutput()
{
}

main.cpp 就比较简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Tell SDL not to mess with main()
#define SDL_MAIN_HANDLED

#include "Game.h"

int main()
{
Game game;
if (game.Initialize())
{
game.RunLoop();
}
game.Shutdown();
return 0;
}

小结

由于后续内容概念众多,而且有一定难度,使用模板直接启动,可能少一点痛苦。当然,不使用模板直接手动配置头文件目录和库目录也是可以的(其实也不是很难,但有一点繁琐)。下一篇概念非常多,代码量也相当大,而且最后效果仅仅是更换背景颜色,慎入。

参考资料

  1. 没有任何秘密的 API:第 1 部分
  2. [波兰]帕维尔·利平斯基著,苏连印,苏宝龙译.Vulkan实战.电子工业出版社.2022