0%

Rethinking of "从 Rust 开始的 Vulkan Ray Tracing"(一)

新的一年,也要回望过去,重新思考一些以前的话题。

大约一年前我完成了 “从 Rust 开始的 Vulkan Ray Tracing” 项目,简而言之就是用 Rust 的 Vulkan Wrapper 来实现了一个简单的 Ray Tracing 场景。但是实际上这个项目依然存在很多问题,比如使用的 rust-gpu 库实际上是一个非常老旧的东西,而且和特定的 rust 版本绑定,所以非常的不灵活也令人不爽。

于是在 2026 年的新年之际我又重新拾起了这个项目,这次会在一年前的项目的基础上,去掉使用 rust-gpu 编写的 shader lib,而是改成使用 GLSL 编写所有的 shaders,然后使用 rust 的 shaderc 库来进行编译。我计划使用三篇文章来重新完成从 Vulkan 到 Ray Tracing 的总体流程,现在这是整个系列的第一篇。

你可以在这个 repo 中找到这个项目的源代码。

总览

我们整体的修改计划如下:

  1. 将原先使用 rust-gpu 编写的 shader lib 改成使用 GLSL,分成四个 shader 文件,然后使用 shaderc 库来进行编译。
  2. 将原先的 main.rs 分成多个文件,每个文件负责一部分功能,而 main.rs 负责创建 Vulkan 实例并完成整个流程构建,然后将所有文件组合成一个可执行文件。
  3. 添加一个 glfw 的窗口可以实时显示渲染结果,同时也保留了导出为 PNG 的功能,为此还需要编写两个着色器。

在明确了这几点计划之后,我们最终的项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vulkan-ray-tracing
├── shaders/
│ ├── blit.frag.shader # Blit 片段着色器(窗口显示)
│ ├── blit.vert.shader # Blit 顶点着色器(窗口显示)
│ ├── closesthit.rchit.shader # 最近命中着色器
│ ├── intersection.rint.shader # 相交着色器(球体 AABB)
│ ├── miss.rmiss.shader # 未命中着色器
│ └── raygen.rgen.shader # 光线生成着色器
├── src/
│ ├── acceleration_structure.rs # 加速结构(BLAS/TLAS)构建
│ ├── buffer.rs # Buffer 资源管理
│ ├── image_utils.rs # 图像工具(渲染目标、PNG 导出)
│ ├── lib.rs # 库入口与模块导出
│ ├── main.rs # 主程序与渲染循环
│ ├── material.rs # 材质定义
│ ├── pipeline.rs # RT 管线、Descriptor、SBT
│ ├── scene.rs # 场景构建
│ ├── vulkan_base.rs # Vulkan 基础设施
│ └── windowed.rs # 窗口模式与 Swapchain
├── build.rs # 构建脚本(着色器编译)
└── Cargo.toml

所有的 .shader 文件都将通过 shaderc 库来进行编译成 spirv,然后被加载到 Vulkan 中。当然在这篇文章中我们不需要写这么多文件,而是循序渐进逐渐填充。

前置工作

通读本文需要掌握的前置知识:

和之前的文章相比,前置知识要求少了“懂得 Vulkan 的渲染流程”这一部分,这是因为我打算新的文章直接从这一部分开始讲起。除了这些前置知识之外,还需要一些硬件条件,简而言之就是一台能跑 Vulkan 的电脑,我的运行环境如下:

  • 操作系统:Ubuntu 24.02 LTS
  • GPU:RTX 2070Super Max-Q
  • Vulkan SDK:1.4.355.0

和之前的文章相比除了 Vulkan SDK 更新了一些之外没有任何区别。

有了这些前置条件,我们就可以开始创建项目了。首先先在命令行当中输入:

1
2
cargo new vulkan-raytracing
cd vulkan-raytracing

然后在 Cargo.toml 添加一些这次我们要用到的库:

1
2
3
4
5
[dependencies]
ash = "*"
ash-window = "*"
raw-window-handle = "*"
glfw = "*"

其中 ashash-window 都是和 Vulkan API 的绑定库,raw-window-handle 主要是得到窗口句柄的抽象,glfw 主要负责窗口的创建和事件循环。

如果你不想要将结果渲染到一个窗口中而是只是想要一个离屏渲染的项目,那你可以只添加 ash 作为依赖。

GLFW 创建一个窗口

首先打开 main.rs 然后给 main() 的返回值添加一个 Result<(), Box<dyn std::error::Error>> 用于返回所有遇到的错误。然后我们定义一些常量:

1
2
3
4
5
fn main() -> Result<(), Box<dyn std::error::Error>> {
const HEADLESS_MODE: bool = false;
const WIDTH: u32 = 1200;
const HEIGHT: u32 = 800;
}

一目了然,三个常量分别用于定义是否是无头模式、窗口以及导出的图片的宽度和高度。如果你不想要一个窗口,只想要无头渲染,那么这一节读到这里就可以结束了。如果你想要一个可以输出图像的窗口,那么就需要创建一个 glfw 实例以及一个窗口,最后定义一个事件循环。

创建窗口

这个过程比较简单,只需要调用 glfw::init 即可创建一个实例,

1
let mut glfw = glfw::init(glfw::fail_on_error)?;

这里让实例创建失败时返回错误然后直接退出。然后我们调用 create_window 创建一个窗口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let window = if !HEADLESS_MODE {
glfw.window_hint(glfw::WindowHint::ClientApi(glfw::ClientApiHint::NoApi));
glfw.window_hint(glfw::WindowHint::Resizable(false));
let (win, _events) = glfw
.create_window(
WIDTH,
HEIGHT,
"Vulkan Raytracing",
glfw::WindowMode::Windowed,
)
.expect("Failed to create GLFW window.");
Some(win)
} else {
None
};

这里首先定义了 glfw 的两个 window_hint,第一个 hint 表示我们不需要任何的 API 上下文,由于 glfw 最早设计出来是给 OpenGL 使用的,所以默认使用 OpenGL 的上下文,但是我们在这里使用 Vulkan,因此需要去掉他们。然后第二个 hint 表示我们不希望能够调整窗口大小,因为我们主要是想要展示一张图片即可,而不是添加什么交互。

create_window 的入参也很简明,第一个参数表示宽度、第二个参数表示高度、第三个参数表示窗口标题、第四个参数表示是全屏还是窗口化。

当然也可以在其中添加一些键盘或者鼠标事件,但是这不是我们这篇文章的重点,因此只演示添加一个按 Esc 键退出的事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let window = if !HEADLESS_MODE {
glfw.window_hint(glfw::WindowHint::ClientApi(glfw::ClientApiHint::NoApi));
glfw.window_hint(glfw::WindowHint::Resizable(false));
// 这里将 win 改为 mut,因为要添加键盘回调事件
let (mut win, _events) = glfw
.create_window(
WIDTH,
HEIGHT,
"Vulkan Raytracing",
glfw::WindowMode::Windowed,
)
.expect("Failed to create GLFW window.");

// 设置 ESC 键退出回调
win.set_key_callback(|window, key, _scancode, action, _modifiers| {
if key == glfw::Key::Escape && action == glfw::Action::Press {
window.set_should_close(true);
}
});

Some(win)
} else {
None
};

在这里调用了窗口的 set_key_callback 方法,定义了一个函数,其中入参分别是窗口引用、按下按键的枚举值、系统的扫描码(通常不会用到)、按键动作(Action::Press 表示按下、Action::Release 表示松开、Action::Repeat 表示长按重复)、修饰键状态(表示是否同时按下了 Ctrl/Shift/Alt 等)。

由此我们就成功创建了一个窗口,最后定义一个事件循环即可。

事件循环

1
2
3
4
5
6
7
8
9
10
11
while !HEADLESS_MODE {
glfw.poll_events();

if let Some(win) = window.as_ref() {
if win.should_close() {
break;
}
}

std::thread::sleep(std::time::Duration::from_millis(16));
}

在这里 glfw.poll_events() 表示接收并处理事件,然后下方的条件判断用于判断是否要退出窗口,最后一行就是一个简单的 sleep,之后会被替换成 Vulkan 的一系列操作。

至此,目前的 main.rs 文件如下:

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
fn main() -> Result<(), Box<dyn std::error::Error>> {
const HEADLESS_MODE: bool = false;
const WIDTH: u32 = 1200;
const HEIGHT: u32 = 800;

let mut glfw = glfw::init(glfw::fail_on_errors)?;
let window = if !HEADLESS_MODE {
glfw.window_hint(glfw::WindowHint::ClientApi(glfw::ClientApiHint::NoApi));
glfw.window_hint(glfw::WindowHint::Resizable(false));
let (mut win, _events) = glfw
.create_window(
WIDTH,
HEIGHT,
"Vulkan Raytracing",
glfw::WindowMode::Windowed,
)
.expect("Failed to create GLFW window.");

// 设置 ESC 键退出回调
win.set_key_callback(|window, key, _scancode, action, _modifiers| {
if key == glfw::Key::Escape && action == glfw::Action::Press {
window.set_should_close(true);
}
});

Some(win)
} else {
None
};

while !HEADLESS_MODE {
glfw.poll_events();

if let Some(win) = window.as_ref() {
if win.should_close() {
break;
}
}

std::thread::sleep(std::time::Duration::from_millis(16));
}

Ok(())
}

编译运行后应该可以看到一个全白的窗口,按下 Esc 可以退出。

初始化的流程

一个想要呈现图像的 Vulkan 应用需要进行如下的初始化步骤:

  1. 创建 Vulkan 实例
  2. 创建 Surface (离屏渲染可以不需要)
  3. 选择物理设备并创建逻辑设备,取得队列
  4. 创建交换链(离屏渲染可以不需要)

其中 Vulkan 实例用于告诉系统你需要使用的 Vulkan 的功能,Surface 帮助你和不同平台的窗口进行对接,逻辑设备用于执行分配内存、创建 Vulkan 相关对象,队列用于执行命令,交换链用于展示图像。

创建 Vulkan 实例

我们创建 Vulkan 实例需要经过两个步骤:

  1. 确定需要的层和扩展
  2. 创建实例

得到层和扩展

什么是层和扩展

层是插入在应用程序和 Vulkan 驱动之间的中间件,可以拦截、修改、记录 API 调用。例如 VK_LAYER_KHRONOS_validation 可以用于验证 API 调用。扩展是用于扩展 Vulkan 的功能,提供新的 API、特性或者功能,它分为实例扩展和设备扩展。前者作用于实例,例如 VK_KHR_surface,后者作用于特定设备,例如 VK_KHR_swapchain

于是我们先在 src 文件夹下新建一个 vulkan_base.rs 文件用于定义一些基础函数。

首先我们先获得实例级的扩展,在 vulkan_base.rs 中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use ash::{ext, khr};

pub fn get_instance_extensions(headless_mode: bool) -> Vec<*const i8> {
let mut instance_extensions: Vec<*const i8> = vec![ext::debug_utils::NAME.as_ptr()];
if !headless_mode {
instance_extensions.push(khr::surface::NAME.as_ptr());
#[cfg(target_os = "windows")]
instance_extensions.push(khr::win32_surface::NAME.as_ptr());
#[cfg(target_os = "linux")]
{
instance_extensions.push(khr::xlib_surface::NAME.as_ptr());
instance_extensions.push(khr::wayland_surface::NAME.as_ptr());
}
#[cfg(target_os = "macos")]
instance_extensions.push(ash::mvk::macos_surface::NAME.as_ptr());
}
instance_extensions
}

在这里就是根据不同的操作系统添加对应的 debug 扩展、surface 扩展以及和操作系统相关的 surface 扩展。注意扩展添加了之后不一定都要启用,因此我们在 linux 当中同时添加 X11 和 Wayland 的扩展是没有额外开销的,而现代的系统一般都会同时支持这两者。

如果我们需要 debug 除了要添加扩展之外,还需要一个额外的验证层,由于我们在这里要测试验证层是否可用,因此我们封装一个简单的 ValidationLayerConfig 类:

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
use ash::prelude::VkResult;
use ash::{Entry, ext, khr, vk};
use std::collections::HashSet;
use std::ffi::{CStr, CString, c_void};

pub struct ValidationLayerConfig {
pub layers: Vec<CString>,
pub enabled: bool,
}

impl ValidationLayerConfig {
/// debug 模式启用,release 模式禁用
pub fn new() -> Result<Self, std::ffi::NulError> {
#[cfg(debug_assertions)]
let layers = vec![CString::new("VK_LAYER_KHRONOS_validation")?];
#[cfg(not(debug_assertions))]
let layers = Vec::new();

let enabled = !layers.is_empty();
Ok(Self { layers, enabled })
}

/// 获取层名称指针列表
pub fn as_ptrs(&self) -> Vec<*const i8> {
self.layers.iter().map(|c_str| c_str.as_ptr()).collect()
}

/// 检查验证层是否支持
pub fn check_support(&self, entry: &Entry) -> VkResult<bool> {
if !self.enabled {
return Ok(true);
}
unsafe { check_validation_layer_support(entry, self.layers.iter().map(|c| c.as_c_str())) }
}
}

impl Default for ValidationLayerConfig {
fn default() -> Self {
Self::new().expect("Failed to create default validation layer config")
}
}

pub unsafe fn check_validation_layer_support<'a>(
entry: &Entry,
required_validation_layers: impl IntoIterator<Item = &'a CStr>,
) -> VkResult<bool> {
unsafe {
let supported_layers: HashSet<CString> = entry
.enumerate_instance_layer_properties()?
.into_iter()
.map(|layer_property| CStr::from_ptr(layer_property.layer_name.as_ptr()).to_owned())
.collect();

Ok(required_validation_layers
.into_iter()
.all(|l| supported_layers.contains(l)))
}
}

// 之前的其他代码
// ...

定义的 ValidationLayerConfig 类会创建一个包含验证层的数组,然后 check_validation_layer_support 会先收集所有支持的层然后比对其中是否包含我们需要的验证层。

创建实例

由此,我们的前期准备工作都已经完成,终于可以创建实例了。在 Vulkan 当中创建任何对象都是需要填一张表然后调用一个创建函数,而在创建实例时需要填的表是这样的:

1
2
3
4
5
6
7
8
9
10
11
pub struct InstanceCreateInfo<'a> {
pub s_type: StructureType, // 结构体的类型,这里必须是 InstanceCreateInfo
pub p_next: *const c_void,
pub flags: InstanceCreateFlags,
pub p_application_info: *const ApplicationInfo<'a>, // 描述程序信息的另一张表
pub enabled_layer_count: u32, // 需要开启的层的数量
pub pp_enabled_layer_names: *const *const c_char, // 需要开启的层的名称
pub enabled_extension_count: u32, // 需要开启的扩展的数量
pub pp_enabled_extension_names: *const *const c_char, // 需要开启的扩展的名称
pub _marker: PhantomData<&'a ()>,
}

Vulkan 中要填的表基本都有 p_nextflags 两个成员,前者是为了扩展性的结构体,后者是一些特殊要求的 flag bit,在这里暂时没有用。如果需要使用 debug 调试的话,我们会将 p_next 指向 debug 启用所需的表。

然后我们来看一下要填的另一张表,也就是 ApplicationInfo

1
2
3
4
5
6
7
8
9
10
pub struct ApplicationInfo<'a> {
pub s_type: StructureType, // 结构体的类型,这里必须是 ApplicationInfo
pub p_next: *const c_void,
pub p_application_name: *const c_char, // 应用名称
pub application_version: u32, // 应用版本号
pub p_engine_name: *const c_char, // 引擎名称
pub engine_version: u32, // 引擎版本号
pub api_version: u32, // Vulkan API 版本号
pub _marker: PhantomData<&'a ()>,
}

这个确实是一张比较无聊的表,只需要填写一些自己喜欢的内容,然后注意一下 API 的版本就可以了。填完两张表之后,我们就可以调用 create_instance 来创建实例了,在 vulkan_base.rs 中定义以下函数。

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
use ash::prelude::VkResult;
use ash::{Entry, Instance, ext, khr, vk}; // 有新加的所需要使用的对象 Instance
use std::collections::HashSet;
use std::ffi::{CStr, CString, c_void};

pub fn create_instance(
entry: &Entry,
validation_layers: &[*const i8],
instance_extensions: &[*const i8],
) -> VkResult<Instance> {
let application_name =
CString::new("Vulkan Ray Tracing").expect("Failed to create application name");
let engine_name = CString::new("No Engine").expect("Failed to create engine name");

let application_info = vk::ApplicationInfo::default()
.application_name(application_name.as_c_str())
.application_version(vk::make_api_version(0, 1, 0, 0))
.engine_name(engine_name.as_c_str())
.engine_version(vk::make_api_version(0, 1, 0, 0))
.api_version(vk::API_VERSION_1_3);

let instance_create_info = vk::InstanceCreateInfo::default()
.application_info(&application_info)
.enabled_layer_names(validation_layers)
.enabled_extension_names(instance_extensions);

unsafe { entry.create_instance(&instance_create_info, None) }
}

在 rust 的封装当中可以使用 default() 来创建一个包含默认值的对象,然后调用对应的参数名称来给他们赋值。在这里使用了 vk::make_api_version 用于创建版本号,当然也可以用你喜欢的版本号格式。对于 Vulkan 的 API 可以直接使用宏来输入。

但是如前面所说,如果需要启用 debug,还需要再填一张和 debug 相关的表

1
2
3
4
5
6
7
8
9
10
pub struct DebugUtilsMessengerCreateInfoEXT<'a> {
pub s_type: StructureType, // 结构体的类型,这里必须是 DebugUtilsMessengerCreateInfoEXT
pub p_next: *const c_void,
pub flags: DebugUtilsMessengerCreateFlagsEXT,
pub message_severity: DebugUtilsMessageSeverityFlagsEXT, // 表明获取的 debug 的严重性
pub message_type: DebugUtilsMessageTypeFlagsEXT, // 表明获取的 debug 信息类型
pub pfn_user_callback: PFN_vkDebugUtilsMessengerCallbackEXT, // 产生 debug 信息后的回调函数
pub p_user_data: *mut c_void, // 之后如有需要,可以让自定义回调函数将数据存入该地址
pub _marker: PhantomData<&'a ()>,
}

我们先简单定义一个回调函数,它接受 debug 的信息然后返回一个 Vk_Bool

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
pub unsafe extern "system" fn default_vulkan_debug_utils_callback(
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
message_type: vk::DebugUtilsMessageTypeFlagsEXT,
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT,
_p_user_data: *mut c_void,
) -> vk::Bool32 {
unsafe {
let severity = match message_severity {
vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => "[Verbose]",
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => "[Warning]",
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => "[Error]",
vk::DebugUtilsMessageSeverityFlagsEXT::INFO => "[Info]",
_ => "[Unknown]",
};
let types = match message_type {
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL => "[General]",
vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE => "[Performance]",
vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION => "[Validation]",
_ => "[Unknown]",
};
let message = CStr::from_ptr((*p_callback_data).p_message);
println!("[Debug]{}{}{:?}", severity, types, message);

vk::FALSE
}
}

在这里就是一个简单的把枚举类型对应成字符串然后输出。

最后我们就可以填上 debug 的表然后插入到 InstanceCreateInfo 的后面完成整个流程创建。

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
pub fn create_instance(
entry: &Entry,
validation_layers: &[*const i8],
instance_extensions: &[*const i8],
// 新增的部分,表示是否开启验证
enable_validation: bool,
) -> VkResult<Instance> {
let application_name = CString::new("Vulkan Ray Tracing").expect("Failed to create application name");
let engine_name = CString::new("No Engine").expect("Failed to create engine name");

// 新增的部分,填一张表
let mut debug_utils_create_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
.message_severity(
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
| vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
)
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION,
)
.pfn_user_callback(Some(default_vulkan_debug_utils_callback));

let application_info = vk::ApplicationInfo::default()
.application_name(application_name.as_c_str())
.application_version(vk::make_api_version(0, 1, 0, 0))
.engine_name(engine_name.as_c_str())
.engine_version(vk::make_api_version(0, 1, 0, 0))
.api_version(vk::API_VERSION_1_3);

let instance_create_info = vk::InstanceCreateInfo::default()
.application_info(&application_info)
.enabled_layer_names(validation_layers)
.enabled_extension_names(instance_extensions);

// 新增的部分,将新填的表插入到 instance_create_info 后面
let instance_create_info: vk::InstanceCreateInfo<'_> = if enable_validation {
instance_create_info.push_next(&mut debug_utils_create_info)
} else {
instance_create_info
};

unsafe { entry.create_instance(&instance_create_info, None) }
}

由此,目前的 vulkan_base.rs 文件代码如下:

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
use ash::prelude::VkResult;
use ash::{Entry, Instance, ext, khr, vk};
use std::collections::HashSet;
use std::ffi::{CStr, CString, c_void};

pub struct ValidationLayerConfig {
pub layers: Vec<CString>,
pub enabled: bool,
}

impl ValidationLayerConfig {
/// 创建验证层配置(debug 模式启用,release 模式禁用)
pub fn new() -> Self {
#[cfg(debug_assertions)]
let layers = vec![CString::new("VK_LAYER_KHRONOS_validation").unwrap()];
#[cfg(not(debug_assertions))]
let layers = Vec::new();

let enabled = !layers.is_empty();
Self { layers, enabled }
}

/// 获取层名称指针列表
pub fn as_ptrs(&self) -> Vec<*const i8> {
self.layers.iter().map(|c_str| c_str.as_ptr()).collect()
}

/// 检查验证层是否支持
pub fn check_support(&self, entry: &Entry) -> VkResult<bool> {
if !self.enabled {
return Ok(true);
}
unsafe { check_validation_layer_support(entry, self.layers.iter().map(|c| c.as_c_str())) }
}
}

impl Default for ValidationLayerConfig {
fn default() -> Self {
Self::new()
}
}

pub unsafe extern "system" fn default_vulkan_debug_utils_callback(
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
message_type: vk::DebugUtilsMessageTypeFlagsEXT,
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT,
_p_user_data: *mut c_void,
) -> vk::Bool32 {
unsafe {
let severity = match message_severity {
vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => "[Verbose]",
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => "[Warning]",
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => "[Error]",
vk::DebugUtilsMessageSeverityFlagsEXT::INFO => "[Info]",
_ => "[Unknown]",
};
let types = match message_type {
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL => "[General]",
vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE => "[Performance]",
vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION => "[Validation]",
_ => "[Unknown]",
};
let message = CStr::from_ptr((*p_callback_data).p_message);
println!("[Debug]{}{}{:?}", severity, types, message);

vk::FALSE
}
}

pub unsafe fn check_validation_layer_support<'a>(
entry: &Entry,
required_validation_layers: impl IntoIterator<Item = &'a CStr>,
) -> VkResult<bool> {
unsafe {
let supported_layers: HashSet<CString> = entry
.enumerate_instance_layer_properties()?
.into_iter()
.map(|layer_property| CStr::from_ptr(layer_property.layer_name.as_ptr()).to_owned())
.collect();

Ok(required_validation_layers
.into_iter()
.all(|l| supported_layers.contains(l)))
}
}

pub fn get_instance_extensions(headless_mode: bool) -> Vec<*const i8> {
let mut instance_extensions: Vec<*const i8> = vec![ext::debug_utils::NAME.as_ptr()];
if !headless_mode {
instance_extensions.push(khr::surface::NAME.as_ptr());
#[cfg(target_os = "windows")]
instance_extensions.push(khr::win32_surface::NAME.as_ptr());
#[cfg(target_os = "linux")]
{
instance_extensions.push(khr::xlib_surface::NAME.as_ptr());
instance_extensions.push(khr::wayland_surface::NAME.as_ptr());
}
#[cfg(target_os = "macos")]
instance_extensions.push(ash::mvk::macos_surface::NAME.as_ptr());
}
instance_extensions
}

pub fn create_instance(
entry: &Entry,
validation_layers: &[*const i8],
instance_extensions: &[*const i8],
enable_validation: bool,
) -> VkResult<Instance> {
let application_name =
CString::new("Vulkan Ray Tracing").expect("Failed to create application name");
let engine_name = CString::new("No Engine").expect("Failed to create engine name");

let mut debug_utils_create_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
.message_severity(
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
| vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
)
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION,
)
.pfn_user_callback(Some(default_vulkan_debug_utils_callback));

let application_info = vk::ApplicationInfo::default()
.application_name(application_name.as_c_str())
.application_version(vk::make_api_version(0, 1, 0, 0))
.engine_name(engine_name.as_c_str())
.engine_version(vk::make_api_version(0, 1, 0, 0))
.api_version(vk::API_VERSION_1_3);

let instance_create_info = vk::InstanceCreateInfo::default()
.application_info(&application_info)
.enabled_layer_names(validation_layers)
.enabled_extension_names(instance_extensions);

let instance_create_info: vk::InstanceCreateInfo<'_> = if enable_validation {
instance_create_info.push_next(&mut debug_utils_create_info)
} else {
instance_create_info
};

unsafe { entry.create_instance(&instance_create_info, None) }
}

物理设备与队列族

选择物理设备

我们可以通过 enumerate_physical_devices 方法获取物理设备列表,它返回的是一个 Vec,每个元素是一个 vk::PhysicalDevice 对象。在得到所有的物理设备后,我们需要选择一个支持所有需要的扩展的物理设备。我们可以通过 get_physical_device_properties 方法获取物理设备的属性,然后根据属性选择物理设备。

vulkan_base.rs 中加入如下代码。

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
pub fn pick_physical_device_and_queue_family_indices(
instance: &Instance,
extensions: &[&CStr],
) -> VkResult<Option<vk::PhysicalDevice>> {
Ok(unsafe { instance.enumerate_physical_devices() }?
.into_iter()
.find_map(|physical_device| {
// 检查设备扩展支持
if unsafe { instance.enumerate_device_extension_properties(physical_device) }.map(
|exts| {
let set: HashSet<&CStr> = exts
.iter()
.map(|ext| unsafe { CStr::from_ptr(&ext.extension_name as *const c_char) })
.collect();

extensions.iter().all(|ext| set.contains(ext))
},
)
{
Some(physical_device)
} else {
None
}
}))
}

遍历所有的物理设备,找到一个支持所有需要的扩展的物理设备。如果找不到,返回 None。非常清晰的逻辑。

检查并获得队列族索引

什么是队列?

队列是 Vulkan 中用于提交命令的机制。每个队列都关联到一个队列族,每个队列族都关联到一个物理设备。Vulkan 规定队列支持的操作类型包括图形、计算、数据传输和稀疏绑定,图形和计算队列必定支持数据传输。而任何支持 Vulkan 的设备确保你必定能找到至少一个同时支持图形和计算操作的队列族。

我们在这里由于需要进行光追并进行渲染,因此需要图形和计算队列族,如果需要把结果呈现在屏幕上则需要呈现队列族。因此我们需要在选择物理设备的同时,指定队列族的索引。我们可以通过 get_physical_device_queue_family_properties 来得到所有的队列族,然后经过条件判断来得到结果。

我们首先定义几个简单的数据结构来储存所有的队列族索引

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
#[derive(Default, Clone, Copy, Debug)]
pub struct QueueFamilyIndices {
pub graphics_family: Option<u32>,
pub compute_family: Option<u32>,
pub present_family: Option<u32>,
}

impl QueueFamilyIndices {
/// 检查是否满足要求
/// - need_compute: 是否需要 compute 队列
/// - need_present: 是否需要 present 队列
pub fn is_complete(&self, need_compute: bool, need_present: bool) -> bool {
let has_graphics = self.graphics_family.is_some();
let has_compute = !need_compute || self.compute_family.is_some();
let has_present = !need_present || self.present_family.is_some();
has_graphics && has_compute && has_present
}

/// 获取唯一的队列族索引列表,用于创建设备时避免重复
pub fn unique_families(&self) -> Vec<u32> {
let mut families = Vec::new();
if let Some(g) = self.graphics_family {
families.push(g);
}
if let Some(c) = self.compute_family {
if !families.contains(&c) {
families.push(c);
}
}
if let Some(p) = self.present_family {
if !families.contains(&p) {
families.push(p);
}
}
families
}
}

由于队列族是有可能重复的,因此我们需要设计一个方法来返回没有重复的队列族(对于现代 GPU 来说,一般主队列族都会同时支持三种操作的队列族,因此很大可能是重复的)。在定义了这些之后,我们就可以在 vulkan_base.rs 中加入如下代码。

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
pub fn pick_physical_device_and_queue_family_indices(
instance: &Instance,
extensions: &[&CStr],
// 新增的参数
surface_loader: Option<&khr::surface::Instance>,
surface: Option<vk::SurfaceKHR>,
need_compute: bool,
) -> VkResult<Option<(vk::PhysicalDevice, QueueFamilyIndices)>> {
let need_present = surface.is_some();

Ok(unsafe { instance.enumerate_physical_devices() }?
.into_iter()
.find_map(|physical_device| {
// 检查设备扩展支持
if unsafe { instance.enumerate_device_extension_properties(physical_device) }.map(
|exts| {
let set: HashSet<&CStr> = exts
.iter()
.map(|ext| unsafe { CStr::from_ptr(&ext.extension_name as *const c_char) })
.collect();

extensions.iter().all(|ext| set.contains(ext))
},
) != Ok(true)
{
return None;
}

// 新增的逻辑
let queue_families =
unsafe { instance.get_physical_device_queue_family_properties(physical_device) };

let mut indices = QueueFamilyIndices::default();

// 查找图形队列族
if let Some(graphics_index) = queue_families
.iter()
.enumerate()
.find(|(_, properties)| {
properties.queue_count > 0
&& properties.queue_flags.contains(vk::QueueFlags::GRAPHICS)
})
.map(|(i, _)| i as u32)
{
indices.graphics_family = Some(graphics_index);
}

// 查找计算队列族
if need_compute {
if let Some(compute_index) = queue_families
.iter()
.enumerate()
.find(|(_, properties)| {
properties.queue_count > 0
&& properties.queue_flags.contains(vk::QueueFlags::COMPUTE)
})
.map(|(i, _)| i as u32)
{
indices.compute_family = Some(compute_index);
}
}

// 查找呈现队列族,如果没有 surface,则不查找
if let (Some(loader), Some(surf)) = (surface_loader, surface) {
if let Some(present_index) = queue_families
.iter()
.enumerate()
.find(|(i, _)| {
unsafe {
loader
.get_physical_device_surface_support(physical_device, *i as u32, surf)
.unwrap_or(false)
}
})
.map(|(i, _)| i as u32)
{
indices.present_family = Some(present_index);
}
}

// 检查是否满足要求
if indices.is_complete(need_compute, need_present) {
Some((physical_device, indices))
} else {
None
}
}))
}

由此,我们通过判断队列族的参数中是否包含对应的 QueueFlags 来检查是否满足要求,并进行选择。而对于 surface,我们同时还要检查物理设备是否支持当前的 surface。

逻辑设备创建

在找到了一个合适的物理设备以及对应的队列族之后,我们就可以创建逻辑设备了。只需要填一张表 DeviceCreateInfo 然后调用 create_device 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub struct DeviceCreateInfo<'a> {
pub s_type: StructureType, // 结构体的类型,必须为 `DeviceCreateInfo`
pub p_next: *const c_void,
pub flags: DeviceCreateFlags,
pub queue_create_info_count: u32, // 队列创建信息数量
pub p_queue_create_infos: *const DeviceQueueCreateInfo<'a>, // 队列创建信息数组
#[deprecated = "functionality described by this member no longer operates"]
pub enabled_layer_count: u32,
#[deprecated = "functionality described by this member no longer operates"]
pub pp_enabled_layer_names: *const *const c_char,
pub enabled_extension_count: u32, // 扩展数量
pub pp_enabled_extension_names: *const *const c_char, // 扩展名称数组
pub p_enabled_features: *const PhysicalDeviceFeatures, // 指向一个 `PhysicalDeviceFeatures` 结构体,说明开启的特性
pub _marker: PhantomData<&'a ()>,
}

对于队列的创建信息有

1
2
3
4
5
6
7
8
9
pub struct DeviceQueueCreateInfo<'a> {
pub s_type: StructureType, // 结构体的类型,必须为 `DeviceQueueCreateInfo`
pub p_next: *const c_void,
pub flags: DeviceQueueCreateFlags,
pub queue_family_index: u32, // 队列族索引
pub queue_count: u32, // 在该队列族索引下创建的队列数量
pub p_queue_priorities: *const f32, // 队列族中各个队列的优先级(0-1)
pub _marker: PhantomData<&'a ()>,
}

于是乎,我们就只需要填一下表,然后启用一些扩展,就可以创建逻辑设备了。

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
pub fn create_device(
instance: &Instance,
physical_device: vk::PhysicalDevice,
queue_indices: &QueueFamilyIndices,
headless_mode: bool,
) -> VkResult<Device> {
let priorities = [1.0];

// 为每个唯一的队列族创建 QueueCreateInfo
let queue_create_infos: Vec<vk::DeviceQueueCreateInfo> = queue_indices
.unique_families()
.iter()
.map(|&index| {
vk::DeviceQueueCreateInfo::default()
.queue_family_index(index)
.queue_priorities(&priorities)
})
.collect();

// 默认的 1.0 特性,全部默认
let mut features2 = vk::PhysicalDeviceFeatures2::default();

// 1.2 的特性,开启光追加速结构需要的 `buffer_device_address` 和简化 shader buffer 布局的 `scalar_block_layout`
let mut features12 = vk::PhysicalDeviceVulkan12Features::default()
.buffer_device_address(true)
.scalar_block_layout(true);

// 光追的加速结构特性
let mut as_feature = vk::PhysicalDeviceAccelerationStructureFeaturesKHR::default()
.acceleration_structure(true);

// 光追的渲染管线特性
let mut raytracing_pipeline =
vk::PhysicalDeviceRayTracingPipelineFeaturesKHR::default().ray_tracing_pipeline(true);

// 启用的扩展
let mut enabled_extension_names = vec![
vk::KHR_RAY_TRACING_PIPELINE_NAME.as_ptr(), // 光追的渲染管线
vk::KHR_ACCELERATION_STRUCTURE_NAME.as_ptr(), // 光追的加速结构
vk::KHR_DEFERRED_HOST_OPERATIONS_NAME.as_ptr(), // 允许在 CPU 上异步执行
vk::KHR_SPIRV_1_4_NAME.as_ptr(), // 着色器版本
vk::EXT_SCALAR_BLOCK_LAYOUT_NAME.as_ptr(), // 简化 shader buffer 布局
];

// 窗口模式需要 swapchain 扩展
if !headless_mode {
enabled_extension_names.push(vk::KHR_SWAPCHAIN_NAME.as_ptr());
}

// 填表并连接一系列的 feature
let device_create_info = vk::DeviceCreateInfo::default()
.push_next(&mut features2)
.push_next(&mut features12)
.push_next(&mut as_feature)
.push_next(&mut raytracing_pipeline)
.queue_create_infos(&queue_create_infos)
.enabled_extension_names(&enabled_extension_names);

unsafe { instance.create_device(physical_device, &device_create_info, None) }
}

至此,我们就完成了不需要窗口的 Vulkan 应用的整体的初始化,我们可以添加一个 src/lib.rs 文件作为入口,然后在 src/main.rs 中调用测试。

lib.rs 如下:

1
2
3
pub mod vulkan_base;

pub use vulkan_base::*;

main.rs 中依次调用之前定义的函数

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
use vulkan_raytracing::*;
use ash::khr;

fn main() -> Result<(), Box<dyn std::error::Error>> {
// ========== 渲染配置 ==========
const HEADLESS_MODE: bool = false;
const WIDTH: u32 = 1200;
const HEIGHT: u32 = 800;

// ========== GLFW 初始化 ==========
let mut glfw = glfw::init(glfw::fail_on_errors)?;
let window = if !HEADLESS_MODE {
glfw.window_hint(glfw::WindowHint::ClientApi(glfw::ClientApiHint::NoApi));
glfw.window_hint(glfw::WindowHint::Resizable(false));
let (mut win, _events) = glfw
.create_window(
WIDTH,
HEIGHT,
"Vulkan Raytracing",
glfw::WindowMode::Windowed,
)
.expect("Failed to create GLFW window.");

win.set_key_callback(|window, key, _scancode, action, _modifiers| {
if key == glfw::Key::Escape && action == glfw::Action::Press {
window.set_should_close(true);
}
});

Some(win)
} else {
None
};

// ========== 验证层设置 ==========
let validation = ValidationLayerConfig::new();
let entry = unsafe { ash::Entry::load() }?;
assert!(validation.check_support(&entry)?, "Validation layer not supported");

// ========== Vulkan Instance 创建 ==========
let instance_extensions = get_instance_extensions(HEADLESS_MODE);
let instance = create_instance(
&entry,
&validation.as_ptrs(),
&instance_extensions,
validation.enabled,
)?;

println!("Vulkan Instance created successfully");

// ========== 物理设备和队列族选择 ==========
let (physical_device, queue_indices) = pick_physical_device_and_queue_family_indices(
&instance,
None,
None,
&[
khr::acceleration_structure::NAME,
khr::deferred_host_operations::NAME,
khr::ray_tracing_pipeline::NAME,
],
true, // need_compute: 光线追踪需要 compute 队列
)?
.ok_or("No suitable physical device found")?;

let graphics_queue_index = queue_indices.graphics_family.unwrap();

// 打印物理设备信息
let device_properties = unsafe { instance.get_physical_device_properties(physical_device) };
let device_name = unsafe {
std::ffi::CStr::from_ptr(device_properties.device_name.as_ptr())
.to_string_lossy()
};
println!("Selected physical device: {}", device_name);
println!("Graphics queue family index: {}", graphics_queue_index);
if let Some(compute_index) = queue_indices.compute_family {
println!("Compute queue family index: {}", compute_index);
}
if let Some(present_index) = queue_indices.present_family {
println!("Present queue family index: {}", present_index);
}

// ========== 逻辑设备创建 ==========
let device = create_device(&instance, physical_device, &queue_indices, HEADLESS_MODE)?;

println!("Logical device created successfully");

// 获取队列
let graphics_queue = unsafe { device.get_device_queue(graphics_queue_index, 0) };
println!("Graphics queue obtained: {:?}", graphics_queue);

// ========== 主循环 ==========
while !HEADLESS_MODE {
glfw.poll_events();
if let Some(win) = window.as_ref() {
if win.should_close() {
break;
}
}

std::thread::sleep(std::time::Duration::from_millis(16));
}

// ========== 资源清理 ==========
println!("Cleaning up resources...");

unsafe {
device.device_wait_idle()?;

// 销毁逻辑设备
device.destroy_device(None);

// 销毁 Instance
instance.destroy_instance(None);
}
Ok(())
}

如果代码上没有任何错误,你应该可以在命令行中看到类似这样的输出:

1
2
3
4
5
6
7
Vulkan Instance created successfully
Selected physical device: NVIDIA GeForce RTX 2070 SUPER
Graphics queue family index: 0
Compute queue family index: 0
Logical device created successfully
Graphics queue obtained: 0x1807eb14b40
Cleaning up resources...

窗口与交换链(可选)

创建窗口

由于 Vulkan 是与平台无关的 API,因此我们需要一个抽象的 surface 来和平台特定的窗口进行对接。在实现上我们只需要调用 create_surface 然后传入一个实例、窗口的显示服务 handle 以及一个窗口的 handle 即可。

main.rs 中,我们可以在实例创建后添加如下的代码:

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
// 创建实例的代码
// ...
// ========== Surface 创建 ==========
let surface_loader = if !HEADLESS_MODE {
Some(khr::surface::Instance::new(&entry, &instance))
} else {
None
};

let surface = if !HEADLESS_MODE {
let win = window.as_ref().unwrap();
Some(unsafe {
ash_window::create_surface(
&entry,
&instance,
win.display_handle().expect("Failed to get display handle").as_raw(),
win.window_handle().expect("Failed to get window handle").as_raw(),
None,
)?
})
} else {
None
};

if surface.is_some() {
println!("Surface created successfully");
}

这里的 surface_loader 用于辅助我们调用 surface 相关的函数以获得整个 surface 的支持信息,例如图像数量、尺寸、像素格式、呈现模式等。

同时选择物理设备时也要传入这两个东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ========== 物理设备和队列族选择 ==========
let (physical_device, queue_indices) = pick_physical_device_and_queue_family_indices(
&instance,
surface_loader.as_ref(), // 修改的部分
surface, // 修改的部分
&[
khr::acceleration_structure::NAME,
khr::deferred_host_operations::NAME,
khr::ray_tracing_pipeline::NAME,
],
true,
)?
.ok_or("No suitable physical device found")?;
// 其他代码
// ...

创建交换链

交换链是指当图像用于渲染或者写入时,已经渲染好的图像可以被呈现引擎相应,交替呈现在窗口中的数张图像的集合就是交换链。

我们首先在 src 文件夹下创建一个 windowed.rs 文件用于处理所有和渲染到窗口相关的代码,然后定义一个交换链的数据结构,之后在第二篇博客中会对他进行进一步封装。

1
2
3
4
5
6
7
8
9
10
use ash::{khr, vk};

pub struct Swapchain {
pub swapchain: vk::SwapchainKHR,
pub images: Vec<vk::Image>,
pub image_views: Vec<vk::ImageView>,
pub format: vk::Format,
pub extent: vk::Extent2D,
pub loader: khr::swapchain::Device,
}

这里保存了交换链的所有信息,包括交换链本身、图像、图像视图、格式、尺寸、加载器等。其中图像就是一块内存块,储存的数据用作图像。而图像视图用于指定图像的使用方式,例如一个6层的二维图像,可以用作6层的二维图像数组,也可用作立方体贴图。有了这些信息,我们就可以编写创建和销毁交换链的代码了。关于创建交换链,我们需要填一张这样的表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub struct SwapchainCreateInfoKHR<'a> {
pub s_type: StructureType, // 结构类型,必须为 `SwapchainCreateInfoKHR`
pub p_next: *const c_void,
pub flags: SwapchainCreateFlagsKHR,
pub surface: SurfaceKHR, // window surface
pub min_image_count: u32, // 交换链中的最小图像数量
pub image_format: Format, // 图像格式
pub image_color_space: ColorSpaceKHR, // 图像颜色空间
pub image_extent: Extent2D, // 图像尺寸
pub image_array_layers: u32, // 对于立体显示或者多视图显示,可以指定图像数组的层数,普通就是 1
pub image_usage: ImageUsageFlags, // 图像使用方式
pub image_sharing_mode: SharingMode, // 图像共享模式,优先使用 `EXCLUSIVE`
pub queue_family_index_count: u32, // 如果共享模式是 `CONCURRENT`,需要指定队列族索引数量
pub p_queue_family_indices: *const u32, // 如果共享模式是 `CONCURRENT`,需要指定队列族索引
pub pre_transform: SurfaceTransformFlagsKHR, // 图像预处理,例如旋转、镜像等
pub composite_alpha: CompositeAlphaFlagsKHR, // 如何处理图像的 alpha 通道
pub present_mode: PresentModeKHR, // 呈现模式
pub clipped: Bool32, // 是否允许舍弃窗口中不显示的像素
pub old_swapchain: SwapchainKHR, // 旧交换链,重建交换链时可以使用
pub _marker: PhantomData<&'a ()>,
}

然后我们要做的就是填这张巨大的表,其中关于图像的数量、尺寸、视点数量、预处理、alpha 通道的用法、支持的呈现模式等都可以通过 get_physical_device_surface_capabilities 得到。

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
impl Swapchain {
pub fn new(
instance: &ash::Instance,
device: &ash::Device,
physical_device: vk::PhysicalDevice,
surface: vk::SurfaceKHR,
surface_loader: &khr::surface::Instance,
width: u32,
height: u32,
) -> Result<Self, Box<dyn std::error::Error>> {
let surface_capabilities = unsafe {
surface_loader
.get_physical_device_surface_capabilities(physical_device, surface)
}?;

let surface_formats = unsafe {
surface_loader
.get_physical_device_surface_formats(physical_device, surface)
}?;

if surface_formats.is_empty() {
return Err("No surface formats available".into());
}

let surface_format = surface_formats
.iter()
.find(|f| {
f.format == vk::Format::B8G8R8A8_SRGB
&& f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR
})
.unwrap_or(&surface_formats[0]);

let present_modes = unsafe {
surface_loader
.get_physical_device_surface_present_modes(physical_device, surface)
}?;

if present_modes.is_empty() {
return Err("No present modes available".into());
}

let present_mode = present_modes
.iter()
.find(|&&m| m == vk::PresentModeKHR::MAILBOX)
.unwrap_or(&vk::PresentModeKHR::FIFO);

let image_count = (surface_capabilities.min_image_count + 1)
.min(surface_capabilities.max_image_count.max(surface_capabilities.min_image_count + 1));

let extent = if surface_capabilities.current_extent.width != u32::MAX {
surface_capabilities.current_extent
} else {
vk::Extent2D {
width: width.clamp(
surface_capabilities.min_image_extent.width,
surface_capabilities.max_image_extent.width,
),
height: height.clamp(
surface_capabilities.min_image_extent.height,
surface_capabilities.max_image_extent.height,
),
}
};
}
}

我们首先先得到这些信息,然后再手动寻找我们需要的部分。对于图像尺寸,我们在这里使用 clamp 来限制图像尺寸在最小和最大尺寸之间。

对于图像格式,surface_format 返回的数据中包含两个成员,format 表示格式,color_space 表示色彩空间。我们希望使用 B8G8R8A8_SRGB 的格式和 SRGB_NONLINEAR 的色彩空间,如果找不到就使用第一个支持的格式。这里的 B8G8R8A8 表示一个 32 位的数据按照 BGRA 来排列,然后 SRGB 会帮我们自动进行伽马矫正。

对于呈现模式,我们希望使用 MAILBOX,如果找不到就用 FIFO

引用英特尔的教程配图,

  • IMMEDIATE 表示立即模式,该模式下不限制帧率且帧率在所有模式中是最高的。该模式不等待垂直同步信号,一旦图片渲染完,用于呈现的图像就会被立刻替换掉,这可能导致画面撕裂。
  • FIFO 表示 FIFO 模式,该模式限制帧率与屏幕刷新率一致,这种模式是必定支持的。在该模式下,图像被推送进一个用于待呈现图像的队列,然后等待垂直同步信号,按顺序被推出队列并输出到屏幕,因此叫先入先出。
  • FIFO_RELAXEDFIFO 的区别在于,,若屏幕上图像的停留时间长于一个刷新间隔,呈现引擎可能在下一个垂直同步信号到来前便试图将呈现队列中的图像输出到屏幕,该模式相比 FIFO 更不容易引起阻塞或迟滞,但在帧率较低时可能会导致画面撕裂。
  • MAILBOX 是一种类似于三重缓冲的模式。它的待呈现图像队列中只容纳一个元素,在等待垂直同步信号期间若有新的图像入队,那么旧的图像会直接出队而不被输出到屏幕(即出队不需要等待垂直同步信号,因此不限制帧率),出现在屏幕上的总会是最新的图像。

对于图像的数量,我们就设定为比最小值大 1 同时不超过最大值的数值。最后,我们就可以填这张表了。同时调用 create_swapchain 创建交换链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let swapchain_loader = khr::swapchain::Device::new(instance, device);

let swapchain_create_info = vk::SwapchainCreateInfoKHR::default()
.surface(surface)
.min_image_count(image_count)
.image_format(surface_format.format)
.image_color_space(surface_format.color_space)
.image_extent(extent)
.image_array_layers(1)
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.pre_transform(surface_capabilities.current_transform)
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
.present_mode(*present_mode)
.clipped(true);

let swapchain = unsafe {
swapchain_loader
.create_swapchain(&swapchain_create_info, None)
}?;

这里的透明度我们设置为不透明,因为我们的场景必然会有颜色。

在创建完交换链后,我们需要得到图像的 handle,然后为他们创建图像视图。创建方法也是填一张表。

1
2
3
4
5
6
7
8
9
10
11
pub struct ImageViewCreateInfo<'a> {
pub s_type: StructureType, // 结构类型,必须为 `ImageViewCreateInfo`
pub p_next: *const c_void,
pub flags: ImageViewCreateFlags,
pub image: Image, // 图像 handle
pub view_type: ImageViewType, // 图像视图类型(例如 1D/2D/3D、立方体图像、图像数组)
pub format: Format, // 图像视图格式,一般和图像一致
pub components: ComponentMapping, // 通道映射关系(例如 R -> R, G -> G, B -> B, A -> A,R -> 1, R -> 0)
pub subresource_range: ImageSubresourceRange, // 子资源范围(另一张表)
pub _marker: PhantomData<&'a ()>,
}

这里的子资源范围定义了视图能访问的图像资源的范围,例如 mipmap 的等级、层面等。我们这里使用默认值即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let images = unsafe { swapchain_loader.get_swapchain_images(swapchain) }?;

let mut image_views = Vec::with_capacity(images.len());
for &image in &images {
let view_info = vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(surface_format.format)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
let view = unsafe { device.create_image_view(&view_info, None) }?;
image_views.push(view);
}

由此,我们就创建了交换链和图像视图。只需要返回整个对象即可,Swapchain::new 的完整代码如下:

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
pub fn new(
instance: &ash::Instance,
device: &ash::Device,
physical_device: vk::PhysicalDevice,
surface: vk::SurfaceKHR,
surface_loader: &khr::surface::Instance,
width: u32,
height: u32,
) -> Result<Self, Box<dyn std::error::Error>> {
let surface_capabilities = unsafe {
surface_loader
.get_physical_device_surface_capabilities(physical_device, surface)
}?;

let surface_formats = unsafe {
surface_loader
.get_physical_device_surface_formats(physical_device, surface)
}?;

if surface_formats.is_empty() {
return Err("No surface formats available".into());
}

let surface_format = surface_formats
.iter()
.find(|f| {
f.format == vk::Format::B8G8R8A8_SRGB
&& f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR
})
.unwrap_or(&surface_formats[0]);

let present_modes = unsafe {
surface_loader
.get_physical_device_surface_present_modes(physical_device, surface)
}?;

if present_modes.is_empty() {
return Err("No present modes available".into());
}

let present_mode = present_modes
.iter()
.find(|&&m| m == vk::PresentModeKHR::MAILBOX)
.unwrap_or(&vk::PresentModeKHR::FIFO);

let image_count = (surface_capabilities.min_image_count + 1)
.min(surface_capabilities.max_image_count.max(surface_capabilities.min_image_count + 1));

let extent = if surface_capabilities.current_extent.width != u32::MAX {
surface_capabilities.current_extent
} else {
vk::Extent2D {
width: width.clamp(
surface_capabilities.min_image_extent.width,
surface_capabilities.max_image_extent.width,
),
height: height.clamp(
surface_capabilities.min_image_extent.height,
surface_capabilities.max_image_extent.height,
),
}
};

let swapchain_loader = khr::swapchain::Device::new(instance, device);

let swapchain_create_info = vk::SwapchainCreateInfoKHR::default()
.surface(surface)
.min_image_count(image_count)
.image_format(surface_format.format)
.image_color_space(surface_format.color_space)
.image_extent(extent)
.image_array_layers(1)
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.pre_transform(surface_capabilities.current_transform)
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
.present_mode(*present_mode)
.clipped(true);

let swapchain = unsafe {
swapchain_loader
.create_swapchain(&swapchain_create_info, None)
}?;

let images = unsafe { swapchain_loader.get_swapchain_images(swapchain) }?;

let mut image_views = Vec::with_capacity(images.len());
for &image in &images {
let view_info = vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(surface_format.format)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
let view = unsafe { device.create_image_view(&view_info, None) }?;
image_views.push(view);
}

Ok(Self {
swapchain,
images,
image_views,
format: surface_format.format,
extent,
loader: swapchain_loader,
})
}

销毁交换链

当我们不再需要交换链时,需要将其销毁。我们需要删除所有的图像视图和交换链。

1
2
3
4
5
6
7
8
pub fn destroy(&self, device: &ash::Device) {
unsafe {
for &view in &self.image_views {
device.destroy_image_view(view, None);
}
self.loader.destroy_swapchain(self.swapchain, None);
}
}

由此,我们完成了交换链和图像视图以及 surface 的创建和管理。整个 windowed.rs 的代码如下:

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
126
127
128
129
130
131
132
133
134
use ash::{khr, vk};

pub struct Swapchain {
pub swapchain: vk::SwapchainKHR,
pub images: Vec<vk::Image>,
pub image_views: Vec<vk::ImageView>,
pub format: vk::Format,
pub extent: vk::Extent2D,
pub loader: khr::swapchain::Device,
}

impl Swapchain {
pub fn new(
instance: &ash::Instance,
device: &ash::Device,
physical_device: vk::PhysicalDevice,
surface: vk::SurfaceKHR,
surface_loader: &khr::surface::Instance,
width: u32,
height: u32,
) -> Result<Self, Box<dyn std::error::Error>> {
let surface_capabilities = unsafe {
surface_loader
.get_physical_device_surface_capabilities(physical_device, surface)
}?;

let surface_formats = unsafe {
surface_loader
.get_physical_device_surface_formats(physical_device, surface)
}?;

if surface_formats.is_empty() {
return Err("No surface formats available".into());
}

let surface_format = surface_formats
.iter()
.find(|f| {
f.format == vk::Format::B8G8R8A8_SRGB
&& f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR
})
.unwrap_or(&surface_formats[0]);

let present_modes = unsafe {
surface_loader
.get_physical_device_surface_present_modes(physical_device, surface)
}?;

if present_modes.is_empty() {
return Err("No present modes available".into());
}

let present_mode = present_modes
.iter()
.find(|&&m| m == vk::PresentModeKHR::MAILBOX)
.unwrap_or(&vk::PresentModeKHR::FIFO);

let image_count = (surface_capabilities.min_image_count + 1)
.min(surface_capabilities.max_image_count.max(surface_capabilities.min_image_count + 1));

let extent = if surface_capabilities.current_extent.width != u32::MAX {
surface_capabilities.current_extent
} else {
vk::Extent2D {
width: width.clamp(
surface_capabilities.min_image_extent.width,
surface_capabilities.max_image_extent.width,
),
height: height.clamp(
surface_capabilities.min_image_extent.height,
surface_capabilities.max_image_extent.height,
),
}
};

let swapchain_loader = khr::swapchain::Device::new(instance, device);

let swapchain_create_info = vk::SwapchainCreateInfoKHR::default()
.surface(surface)
.min_image_count(image_count)
.image_format(surface_format.format)
.image_color_space(surface_format.color_space)
.image_extent(extent)
.image_array_layers(1)
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.pre_transform(surface_capabilities.current_transform)
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
.present_mode(*present_mode)
.clipped(true);

let swapchain = unsafe {
swapchain_loader
.create_swapchain(&swapchain_create_info, None)
}?;

let images = unsafe { swapchain_loader.get_swapchain_images(swapchain) }?;

let mut image_views = Vec::with_capacity(images.len());
for &image in &images {
let view_info = vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(surface_format.format)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
let view = unsafe { device.create_image_view(&view_info, None) }?;
image_views.push(view);
}

Ok(Self {
swapchain,
images,
image_views,
format: surface_format.format,
extent,
loader: swapchain_loader,
})
}

pub fn destroy(&self, device: &ash::Device) {
unsafe {
for &view in &self.image_views {
device.destroy_image_view(view, None);
}
self.loader.destroy_swapchain(self.swapchain, None);
}
}
}

添加调用

我们在 main.rs 当中添加调用:

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
// 创建逻辑设备的代码
// ...

// ========== Swapchain 创建 ==========
let swapchain = if !HEADLESS_MODE && surface.is_some() {
let sc = Swapchain::new(
&instance,
&device,
physical_device,
surface.unwrap(),
surface_loader.as_ref().unwrap(),
WIDTH,
HEIGHT,
)?;
println!(
"Swapchain created: format={:?}, extent={}x{}, images={}",
sc.format,
sc.extent.width,
sc.extent.height,
sc.images.len()
);
Some(sc)
} else {
None
};

// 主循环
// ...
// 资源清理
println!("Cleaning up resources...");

unsafe {
device.device_wait_idle()?;

// 新增内容:销毁 Swapchain
if let Some(sc) = swapchain {
sc.destroy(&device);
}

// 销毁逻辑设备
device.destroy_device(None);

// 新增内容:销毁 Surface
if let Some(s) = surface {
if let Some(loader) = surface_loader.as_ref() {
loader.destroy_surface(s, None);
}
}

// 销毁 Instance
instance.destroy_instance(None);
}
Ok(())

如果代码没有问题,运行之后会看到类似于这样的输出:

1
2
3
4
5
6
7
8
9
10
Vulkan Instance created successfully
Surface created successfully
Selected physical device: NVIDIA GeForce RTX 2060 SUPER
Graphics queue family index: 0
Compute queue family index: 0
Present queue family index: 0
Logical device created successfully
Graphics queue obtained: 0x1c423a9b1c0
Swapchain created: format=B8G8R8A8_SRGB, extent=1200x800, images=3
Cleaning up resources...

到此为止我们就完成了 Vulkan 程序的所有初始化。所有的必备的对象都被创建、管理和销毁。在下一篇文章中我们将开始学习如何真正的开始渲染图像。