Rust Vulkan 快速入门:构建你的第一个图形程序
引言:性能与安全的交汇点
在现代图形编程领域,追求极致的性能和精细的控制是永恒的主题。Vulkan,作为下一代图形和计算 API,以其低开销、显式控制和多线程友好的特性,成为了高性能应用和游戏开发的首选。然而,Vulkan 极低的抽象层次也意味着陡峭的学习曲线和手动管理大量资源的复杂性。
与此同时,Rust 语言凭借其无畏的并发、内存安全保证和卓越的性能,在系统编程、高性能计算乃至游戏开发领域异军突起。当 Rust 的安全特性与 Vulkan 的裸机控制相结合时,便擦出了令人兴奋的火花:一个既能提供极致性能,又能大幅降低传统 C/C++ Vulkan 开发中常见错误(如内存泄漏、数据竞争)风险的强大组合。
本篇文章将带领你踏上 Rust Vulkan 的初探之旅。我们将从零开始,搭建开发环境,深入理解 Vulkan 的核心概念,并亲手编写代码,最终在屏幕上绘制出一个简单的三角形。这不仅仅是一个教程,更是一次关于性能、安全和现代图形编程理念的深度探索。
目标读者:
- 对图形编程感兴趣的开发者。
- 熟悉 Rust 基础语法和概念的程序员。
- 渴望了解 Vulkan 核心工作原理的求知者。
你将学到什么:
- Rust Vulkan 开发环境的搭建。
- Vulkan API 的基本结构和关键组件。
- 如何使用
ash
库进行 Vulkan 调用。 - 理解图形管线(Graphics Pipeline)的核心流程。
- 绘制第一个在屏幕上可见的三角形。
让我们一起深入这个充满挑战但回报丰厚的领域吧!
第一章: Rust & Vulkan 生态概览
在开始编码之前,有必要了解一下我们即将使用的工具和技术栈。
1.1 为什么选择 Rust 驱动 Vulkan?
- 内存安全: Rust 的所有权(Ownership)系统、借用(Borrowing)规则和生命周期(Lifetimes)机制,在编译时就消除了空指针解引用、数据竞争等常见的内存安全问题。在 Vulkan 这种需要大量手动内存管理和同步的 API 中,Rust 的这些特性尤为宝贵,可以避免调试时间漫长的神秘崩溃。
- 性能媲美 C/C++: Rust 是一门无 GC (Garbage Collection) 的语言,其运行时开销极低,编译出的代码性能与 C/C++ 不相上下,甚至在某些情况下更优。这对于对性能敏感的图形应用至关重要。
- 现代工具链与生态: Rust 拥有优秀的包管理器 Cargo 和构建系统,以及活跃的社区。虽然 Rust 的 Vulkan 绑定相对年轻,但
ash
、vulkano
等库正在迅速成熟。 - 表达力强: Rust 的类型系统和模式匹配使得代码更加清晰、可维护。错误处理(
Result
枚举)强制开发者考虑所有可能的失败情况,这在复杂的 Vulkan 初始化过程中尤其有用。
1.2 Vulkan:显式控制的艺术
Vulkan 是 Khronos Group 推出的跨平台、低开销的图形和计算 API。它旨在取代 OpenGL 等较老的 API,以适应现代 GPU 硬件架构和多核 CPU 设计。
- 低开销与显式控制: 与 OpenGL 这样的“黑箱”API 不同,Vulkan 几乎将所有渲染状态的管理权交给了开发者。你需要明确地创建、配置和销毁所有资源(如缓冲区、纹理、着色器模块、渲染管线等)。这种显式控制带来了更高的性能潜力,但也意味着更多的手动工作。
- 多线程友好: Vulkan 的设计理念允许开发者在多个线程上并行地构建命令缓冲区,然后在主线程上一次性提交。这大大提升了多核 CPU 的利用率,减少了驱动程序瓶颈。
- 平台无关性: Vulkan 支持 Windows、Linux、Android、macOS (通过 MoltenVK) 等多个平台,使得跨平台开发变得更加容易。
1.3 Rust Vulkan 绑定:ash
与 vulkano
在 Rust 生态中,与 Vulkan 交互主要有两种方式:
ash
: 这是 Vulkan 的轻量级、无抽象层的绑定。它直接将 Vulkan C API 函数映射为 Rust 函数,并处理了一些 FFI(Foreign Function Interface)相关的细节。ash
提供了对 Vulkan API 的最直接访问,对于需要极致控制和深入理解 Vulkan 的开发者来说是理想选择。本教程将主要使用ash
。vulkano
: 这是一个更高层级的 Vulkan 封装库。它提供了更友好的 API,抽象了许多 Vulkan 的复杂性,例如自动内存管理、描述符集更新、同步等。对于快速开发原型或希望减少 Vulkan 学习曲线的开发者来说,vulkano
是一个很好的选择,但它引入了自己的抽象层。
选择 ash
是为了让我们能够更贴近 Vulkan 的核心机制,理解每一个 API 调用的目的。
第二章:环境搭建与项目初始化
在开始编写代码之前,我们需要确保所有必要的工具和依赖都已准备就绪。
2.1 安装 Rust
如果你尚未安装 Rust,请通过 rustup
工具链管理器进行安装。这是最推荐的方式:
bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装完成后,运行以下命令验证:
bash
rustc --version
cargo --version
请确保你的 Rust 版本至少是 1.60+,因为一些库可能需要较新的 Rust 特性。
2.2 安装 Vulkan SDK
Vulkan SDK 包含了 Vulkan 头文件、加载器、验证层、工具和示例。这是进行 Vulkan 开发的必需品。
- Windows: 访问 LunarG Vulkan SDK 下载最新版本的 SDK 安装包并安装。
- Linux: 通常可以通过包管理器安装:
- Ubuntu/Debian:
sudo apt install libvulkan-dev vulkan-validationlayers
- Arch Linux:
sudo pacman -S vulkan-headers vulkan-validation-layers
- Fedora:
sudo dnf install vulkan-headers vulkan-validation-layers
- Ubuntu/Debian:
- macOS: Vulkan 在 macOS 上需要通过 MoltenVK 模拟,它将 Vulkan 调用转换为 Apple 的 Metal API。通常安装 Vulkan SDK for macOS 也会包含 MoltenVK。同样访问 LunarG Vulkan SDK 下载。
验证安装:
安装完成后,在终端中运行 vulkaninfo
命令。如果显示了你的 GPU 信息和 Vulkan 支持情况,说明安装成功。如果出现错误,请检查环境变量(如 VK_SDK_PATH
)是否设置正确,或重新安装 SDK。
2.3 创建 Rust 项目
我们将使用 Cargo 创建一个新的 Rust 项目:
bash
cargo new rust_vulkan_triangle --bin
cd rust_vulkan_triangle
2.4 添加依赖
编辑 Cargo.toml
文件,添加必要的依赖。
“`toml
[package]
name = “rust_vulkan_triangle”
version = “0.1.0”
edition = “2021”
[dependencies]
Vulkan 核心绑定
ash = “0.37” # 使用当前最新的稳定版本
窗口管理库
winit = “0.28”
跨平台窗口句柄抽象,ash 需要
raw-window-handle = “0.5”
简单的错误处理
anyhow = “1.0”
用于编译 GLSL 成为 SPIR-V
spirv-builder = “0.5” # 用于 build.rs
spirv-std = “0.4” # 用于 shader cargo project
调试验证层,仅在 debug 构建时启用
[target.’cfg(debug_assertions)’.dependencies]
ash-debug-utils = { version = “0.3”, features = [“debug”] }
用于 shader cargo project
[workspace]
members = [
“shader” # 我们将把 shader 放在单独的 cargo project 中
]
“`
创建 Shader 子项目:
在项目根目录下创建一个 shader
文件夹,并在其中初始化一个新的 Cargo 项目:
bash
mkdir shader
cd shader
cargo init --lib
cd ..
编辑 shader/Cargo.toml
:
“`toml
[package]
name = “shader”
version = “0.1.0”
edition = “2021”
[lib]
crate-type = [“cdylib”] # 编译为动态库
[dependencies]
spirv-std = { version = “0.4”, default-features = false, features = [“no-std”] }
如果你想使用更多 math 类型,可以添加 glam
glam = { version = “0.24”, default-features = false, features = [“mint”] }
“`
编辑 shader/src/lib.rs
,这将是我们的着色器代码的入口点:
“`rust
![no_std] // 着色器不需要标准库
![feature(asm_experimental_inline)] // SPIR-V 编译器的内部需要
use spirv_std::spirv;
[spirv(vertex)]
pub fn vertex_shader_main(
#[spirv(vertex_index)] vert_id: u32,
#[spirv(position)] mut out_pos: spirv_std::glam::Vec4,
) {
let positions = [
spirv_std::glam::vec4(0.0, -0.5, 0.0, 1.0), // 底部
spirv_std::glam::vec4(0.5, 0.5, 0.0, 1.0), // 右上
spirv_std::glam::vec4(-0.5, 0.5, 0.0, 1.0), // 左上
];
out_pos = positions[vert_id as usize];
}
[spirv(fragment)]
pub fn fragment_shader_main(#[spirv(frag_depth)] _depth: Option
spirv_std::glam::vec4(1.0, 0.0, 0.0, 1.0) // 红色
}
``
build.rs` 文件:**
**添加
在项目根目录下创建 build.rs
文件。这个脚本会在编译主项目时,自动编译 shader
子项目中的 Rust 代码为 SPIR-V 字节码。
rust
// build.rs
fn main() -> anyhow::Result<()> {
spirv_builder::SpirvBuilder::new("./shader", "spirv-unknown-vulkan1.0")
.build()?;
Ok(())
}
现在,我们的项目结构和依赖都已就绪。
第三章:Vulkan 核心概念速览
在深入代码之前,我们必须对 Vulkan 的核心组件及其工作流程有一个基本的理解。这将帮助我们理解每一行代码背后的逻辑。
3.1 实例 (Instance) 与物理设备 (Physical Device)
VkInstance
: 你的 Vulkan 应用程序的入口点。它表示了 Vulkan API 本身的一个“实例”,用于查询系统支持的 Vulkan 功能,并与特定平台的窗口系统集成。VkPhysicalDevice
: 代表系统中的一个支持 Vulkan 的 GPU 或其他硬件设备。你可以查询其属性(如设备名称、内存类型、支持的功能)、队列族(Queue Families)等。通常,我们会选择一个性能最佳的 GPU。
3.2 逻辑设备 (Logical Device) 与队列 (Queue)
VkDevice
: 物理设备的一个“逻辑”表示。当你选定了一个物理设备后,你需要创建一个逻辑设备来与它交互。逻辑设备是你真正用于执行 Vulkan 命令的句柄。VkQueue
: 命令队列。GPU 上的操作(如渲染、计算、数据传输)是通过向队列提交命令缓冲区来执行的。Vulkan 设备通常提供不同类型的队列族(如图形队列、计算队列、传输队列、呈现队列),你需要选择并请求这些队列。
3.3 表面 (Surface) 与交换链 (Swap Chain)
VkSurfaceKHR
: 连接 Vulkan 与操作系统窗口的抽象。它允许 Vulkan 将渲染结果呈现在屏幕上。它是一个平台特定的扩展。VkSwapchainKHR
: 负责管理一组用于在屏幕上显示图像的缓冲区(通常是双缓冲或三缓冲)。渲染器在其中一个缓冲区上绘制,而另一个缓冲区则被显示。当渲染完成时,交换链会将两个缓冲区的角色互换(“呈现”),从而实现平滑的动画。
3.4 图像视图 (Image View) 与帧缓冲 (Framebuffer)
VkImageView
: 图像(VkImage
,如交换链中的图像或纹理)的一个“视图”。图像可以有不同的格式、大小和布局。图像视图定义了如何解释和访问图像的特定部分。VkFramebuffer
: 帧缓冲是渲染通道的实际目标。它绑定了渲染通道所需的所有图像视图(如颜色附件、深度附件),告诉 GPU 渲染结果应该存储在哪里。
3.5 渲染通道 (Render Pass) 与子通道 (Subpass)
VkRenderPass
: 描述了一系列渲染操作的步骤。它定义了渲染过程中使用的附件(图像)以及这些附件在每个阶段的加载/存储操作。VkSubpass
: 渲染通道中的一个独立渲染阶段。一个渲染通道可以包含多个子通道,这些子通道可以相互依赖,例如一个子通道的输出可以作为下一个子通道的输入(延迟渲染)。
3.6 图形管线 (Graphics Pipeline)
图形管线是 Vulkan 中最核心的概念之一,它定义了从顶点数据到像素显示在屏幕上的所有阶段。它是一个不可变的状态对象,一旦创建就不能修改。
- 顶点着色器 (Vertex Shader): 处理每个顶点的数据,例如将模型空间坐标转换为裁剪空间坐标。
- 输入装配 (Input Assembly): 将顶点数据组装成图元(点、线、三角形)。
- 视图端口 (Viewport) 与裁剪矩形 (Scissor): 定义渲染区域和裁剪区域。
- 光栅化 (Rasterization): 将几何图元转换为一系列片段(Fragments,即潜在的像素)。
- 片段着色器 (Fragment Shader): 处理每个片段的颜色、深度等信息。
- 颜色混合 (Color Blending): 将片段着色器的输出与帧缓冲中已有的颜色进行混合。
- 管线布局 (Pipeline Layout): 定义了着色器可以访问的资源(如统一缓冲区、纹理)。
3.7 命令池 (Command Pool) 与命令缓冲区 (Command Buffer)
VkCommandPool
: 用于分配命令缓冲区的内存池。不同类型的命令池可以有不同的特性(如可重置)。VkCommandBuffer
: 记录一系列 GPU 命令的对象。所有实际的绘制、传输、计算等操作都需要通过命令缓冲区来记录,然后一次性提交给队列执行。
3.8 同步原语 (Synchronization Primitives)
Vulkan 是异步的,CPU 和 GPU 同时工作。为了协调它们的动作,Vulkan 提供了多种同步原语:
VkFence
: 用于 CPU 和 GPU 之间的同步。CPU 可以等待一个 Fence 信号,表示 GPU 已经完成了特定操作。VkSemaphore
: 用于 GPU 内部或 GPU 队列之间的同步。一个 Semaphore 可以信号另一个操作可以开始执行。例如,一个 Semaphore 可以表示图像已从交换链中获取并准备好渲染,另一个 Semaphore 可以表示渲染已完成并准备好呈现。
这些概念构成了 Vulkan 复杂而强大的体系结构。理解它们是成功构建 Vulkan 应用的关键。
第四章:构建你的第一个三角形程序 (代码实现)
现在,让我们把这些概念转化为实际代码。我们将在 src/main.rs
文件中实现整个程序。
为了代码的可读性和管理,我们将整个程序分解为几个逻辑阶段。
“`rust
// src/main.rs
use ash::{
vk,
Entry,
Instance,
Device,
};
use winit::{
event::{Event, WindowEvent, StartCause},
event_loop::{ControlFlow, EventLoop},
window::{WindowBuilder, Window},
};
use anyhow::{Result, Context};
use std::{
ffi::{CStr, CString},
borrow::Cow,
ops::Drop,
};
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
// 宏,用于检查 Vulkan 函数调用的结果
macro_rules! ash_check {
($expr:expr) => {
$expr.map_err(|e| anyhow::anyhow!(“{:?}”, e))?
};
}
// ============== 常量和辅助函数 ==============
// 应用程序名称和版本
const APPLICATION_NAME: &CStr = cstr::cstr!(“Rust Vulkan Triangle”);
const APPLICATION_VERSION: u32 = vk::make_api_version(0, 1, 0, 0);
// 引擎名称和版本
const ENGINE_NAME: &CStr = cstr::cstr!(“No Engine”);
const ENGINE_VERSION: u32 = vk::make_api_version(0, 1, 0, 0);
// Vulkan API 版本
const API_VERSION: u32 = vk::API_VERSION_1_0;
// 设备扩展 (必要:交换链)
const DEVICE_EXTENSIONS: &[&CStr] = &[
ash::extensions::khr::Swapchain::name(),
];
// 调试层 (仅在 debug 模式下启用)
[cfg(debug_assertions)]
const VALIDATION_LAYERS: &[&CStr] = &[
cstr::cstr!(“VK_LAYER_KHR_validation”),
];
[cfg(not(debug_assertions))]
const VALIDATION_LAYERS: &[&CStr] = &[];
// 调试回调函数 (仅在 debug 模式下使用)
[cfg(debug_assertions)]
unsafe extern “system” fn vulkan_debug_callback(
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
message_type: vk::DebugUtilsMessageTypeFlagsEXT,
p_callback_data: const vk::DebugUtilsMessengerCallbackDataEXT,
_user_data: mut std::ffi::c_void,
) -> vk::Bool32 {
let callback_data = *p_callback_data;
let message_id_number = callback_data.message_id_number;
let message_id_name = if callback_data.p_message_id_name.is_null() {
Cow::from(“”)
} else {
CStr::from_ptr(callback_data.p_message_id_name).to_string_lossy()
};
let message = if callback_data.p_message.is_null() {
Cow::from(“”)
} else {
CStr::from_ptr(callback_data.p_message).to_string_lossy()
};
println!(
"{:?}: {:?} [{} ({})] : {}",
message_severity,
message_type,
message_id_name,
message_id_number,
message,
);
vk::FALSE
}
// ============== 核心结构体 ==============
struct VulkanApp {
entry: Entry,
instance: Instance,
#[cfg(debug_assertions)]
debug_utils: ash::extensions::ext::DebugUtils,
#[cfg(debug_assertions)]
debug_messenger: vk::DebugUtilsMessengerEXT,
surface: ash::extensions::khr::Surface,
surface_khr: vk::SurfaceKHR,
physical_device: vk::PhysicalDevice,
device: Device,
graphics_queue: vk::Queue,
present_queue: vk::Queue,
swapchain_loader: ash::extensions::khr::Swapchain,
swapchain: vk::SwapchainKHR,
swapchain_images: Vec
swapchain_image_views: Vec
render_pass: vk::RenderPass,
pipeline_layout: vk::PipelineLayout,
graphics_pipeline: vk::Pipeline,
framebuffers: Vec
command_pool: vk::CommandPool,
command_buffers: Vec
image_available_semaphores: Vec
render_finished_semaphores: Vec
in_flight_fences: Vec
current_frame: usize, // 用于循环使用同步对象
window: Window,
event_loop: EventLoop<()>,
resized: bool,
}
impl VulkanApp {
pub fn new(window_builder: WindowBuilder) -> Result
let event_loop = EventLoop::new()?;
let window = window_builder.build(&event_loop)?;
// 1. Vulkan Entry 和 Instance 创建
let entry = ash::Entry::load()?;
let instance = Self::create_instance(&entry)?;
// 2. 调试工具 (可选)
#[cfg(debug_assertions)]
let (debug_utils, debug_messenger) = Self::setup_debug_messenger(&entry, &instance)?;
// 3. 表面创建
let surface = ash::extensions::khr::Surface::new(&entry, &instance);
let surface_khr = unsafe {
ash_check!(winit_surface::create_surface(&entry, &instance, &window))
};
// 4. 物理设备选择
let (physical_device, queue_family_indices) = Self::pick_physical_device(&instance, &surface, surface_khr)?;
// 5. 逻辑设备创建
let (device, graphics_queue, present_queue) = Self::create_logical_device(
&instance,
physical_device,
queue_family_indices,
)?;
// 6. 交换链创建 (首次创建)
let swapchain_loader = ash::extensions::khr::Swapchain::new(&instance, &device);
let (
swapchain,
swapchain_images,
swapchain_format,
swapchain_extent,
) = Self::create_swapchain(
&instance,
&device,
physical_device,
surface_khr,
&swapchain_loader,
queue_family_indices,
&window,
None, // 首次创建,没有旧的交换链
)?;
// 7. 图像视图创建
let swapchain_image_views = Self::create_image_views(
&device,
&swapchain_images,
swapchain_format,
)?;
// 8. 渲染通道创建
let render_pass = Self::create_render_pass(&device, swapchain_format)?;
// 9. 图形管线创建 (包括 shader 编译)
let (pipeline_layout, graphics_pipeline) = Self::create_graphics_pipeline(
&device,
render_pass,
swapchain_extent,
)?;
// 10. 帧缓冲创建
let framebuffers = Self::create_framebuffers(
&device,
render_pass,
&swapchain_image_views,
swapchain_extent,
)?;
// 11. 命令池和命令缓冲区创建
let command_pool = Self::create_command_pool(&device, queue_family_indices.graphics_family)?;
let command_buffers = Self::create_command_buffers(
&device,
command_pool,
render_pass,
&framebuffers,
graphics_pipeline,
swapchain_extent,
)?;
// 12. 同步对象创建
let (image_available_semaphores, render_finished_semaphores, in_flight_fences) =
Self::create_sync_objects(&device, framebuffers.len())?;
Ok(Self {
entry,
instance,
#[cfg(debug_assertions)]
debug_utils,
#[cfg(debug_assertions)]
debug_messenger,
surface,
surface_khr,
physical_device,
device,
graphics_queue,
present_queue,
swapchain_loader,
swapchain,
swapchain_images,
swapchain_image_views,
render_pass,
pipeline_layout,
graphics_pipeline,
framebuffers,
command_pool,
command_buffers,
image_available_semaphores,
render_finished_semaphores,
in_flight_fences,
current_frame: 0,
window,
event_loop,
resized: false,
})
}
// --- 各个初始化步骤的详细实现 ---
fn create_instance(entry: &Entry) -> Result<Instance> {
let app_info = vk::ApplicationInfo::builder()
.application_name(APPLICATION_NAME)
.application_version(APPLICATION_VERSION)
.engine_name(ENGINE_NAME)
.engine_version(ENGINE_VERSION)
.api_version(API_VERSION);
let required_extensions = ash_check!(
ash_window::enumerate_required_extensions(&entry, &winit::event_loop::EventLoop::<()>::new()?)
)
.iter()
.map(|&s| s.as_ptr())
.collect::<Vec<*const i8>>();
let required_extensions_cstr: Vec<CStr> = ash_check!(
ash_window::enumerate_required_extensions(&entry, &winit::event_loop::EventLoop::<()>::new()?)
).into_iter().collect();
// 检查验证层支持
if cfg!(debug_assertions) {
let available_layers = ash_check!(entry.enumerate_instance_layer_properties());
let found_validation_layer = VALIDATION_LAYERS.iter().all(|&layer_name| {
available_layers.iter().any(|layer_prop| {
unsafe { CStr::from_ptr(layer_prop.layer_name.as_ptr()) == layer_name }
})
});
if !found_validation_layer {
anyhow::bail!("Validation layers requested, but not available!");
}
}
let instance_create_info = vk::InstanceCreateInfo::builder()
.application_info(&app_info)
.enabled_extension_names(&required_extensions)
.enabled_layer_names(
&VALIDATION_LAYERS.iter().map(|&s| s.as_ptr()).collect::<Vec<*const i8>>()
);
let instance = ash_check!(unsafe { entry.create_instance(&instance_create_info, None) });
Ok(instance)
}
#[cfg(debug_assertions)]
fn setup_debug_messenger(entry: &Entry, instance: &Instance) -> Result<(ash::extensions::ext::DebugUtils, vk::DebugUtilsMessengerEXT)> {
use ash::extensions::ext::DebugUtils;
let debug_utils_loader = DebugUtils::new(entry, instance);
let debug_info = vk::DebugUtilsMessengerCreateInfoEXT::builder()
.message_severity(
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING |
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
)
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL |
vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION |
vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE
)
.pfn_user_callback(Some(vulkan_debug_callback));
let debug_messenger = ash_check!(unsafe { debug_utils_loader.create_debug_utils_messenger(&debug_info, None) });
Ok((debug_utils_loader, debug_messenger))
}
// 辅助结构体,用于存储队列族索引
#[derive(Debug, Clone, Copy)]
struct QueueFamilyIndices {
graphics_family: u32,
present_family: u32,
}
impl QueueFamilyIndices {
fn is_complete(&self) -> bool {
true // 在此示例中,我们假设找到的索引是完整的
}
}
fn find_queue_families(
instance: &Instance,
physical_device: vk::PhysicalDevice,
surface: &ash::extensions::khr::Surface,
surface_khr: vk::SurfaceKHR,
) -> Result<QueueFamilyIndices> {
let queue_families = unsafe {
instance.get_physical_device_queue_family_properties(physical_device)
};
let mut graphics_family = None;
let mut present_family = None;
for (i, queue_family) in queue_families.iter().enumerate() {
if queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) {
graphics_family = Some(i as u32);
}
let present_support = unsafe {
ash_check!(surface.get_physical_device_surface_support(
physical_device,
i as u32,
surface_khr,
))
};
if present_support {
present_family = Some(i as u32);
}
if graphics_family.is_some() && present_family.is_some() {
break;
}
}
match (graphics_family, present_family) {
(Some(graphics), Some(present)) => Ok(QueueFamilyIndices {
graphics_family: graphics,
present_family: present,
}),
_ => anyhow::bail!("Failed to find suitable queue families!"),
}
}
fn check_device_extension_support(instance: &Instance, device: vk::PhysicalDevice) -> Result<bool> {
let available_extensions = unsafe {
ash_check!(instance.enumerate_device_extension_properties(device))
};
let required_extensions_cstrings: Vec<CString> = DEVICE_EXTENSIONS.iter()
.map(|&s| CString::new(s.to_bytes()).unwrap())
.collect();
let all_extensions_supported = required_extensions_cstrings.iter().all(|req_ext| {
available_extensions.iter().any(|ext_prop| {
unsafe { CStr::from_ptr(ext_prop.extension_name.as_ptr()) == req_ext.as_c_str() }
})
});
Ok(all_extensions_supported)
}
fn pick_physical_device(
instance: &Instance,
surface: &ash::extensions::khr::Surface,
surface_khr: vk::SurfaceKHR,
) -> Result<(vk::PhysicalDevice, QueueFamilyIndices)> {
let physical_devices = unsafe { ash_check!(instance.enumerate_physical_devices()) };
for physical_device in physical_devices {
let queue_families = Self::find_queue_families(instance, physical_device, surface, surface_khr)?;
let extensions_supported = Self::check_device_extension_support(instance, physical_device)?;
let swapchain_adequate = Self::query_swapchain_support(physical_device, surface, surface_khr)?.is_adequate();
if queue_families.is_complete() && extensions_supported && swapchain_adequate {
let properties = unsafe { instance.get_physical_device_properties(physical_device) };
println!(
"Picked physical device: {:?}",
unsafe { CStr::from_ptr(properties.device_name.as_ptr()) }
);
return Ok((physical_device, queue_families));
}
}
anyhow::bail!("Failed to find a suitable GPU!");
}
fn create_logical_device(
instance: &Instance,
physical_device: vk::PhysicalDevice,
queue_family_indices: QueueFamilyIndices,
) -> Result<(Device, vk::Queue, vk::Queue)> {
let queue_priority = 1.0f32;
let mut queue_create_infos = Vec::new();
let mut unique_queue_families = std::collections::HashSet::new();
unique_queue_families.insert(queue_family_indices.graphics_family);
unique_queue_families.insert(queue_family_indices.present_family);
for &queue_family_index in &unique_queue_families {
let queue_create_info = vk::DeviceQueueCreateInfo::builder()
.queue_family_index(queue_family_index)
.queue_priorities(&[queue_priority]);
queue_create_infos.push(queue_create_info);
}
let device_features = vk::PhysicalDeviceFeatures::builder(); // 暂不开启任何特殊功能
let device_extensions_ptrs: Vec<*const i8> = DEVICE_EXTENSIONS.iter()
.map(|&s| s.as_ptr())
.collect();
let device_create_info = vk::DeviceCreateInfo::builder()
.queue_create_infos(&queue_create_infos)
.enabled_features(&device_features)
.enabled_extension_names(&device_extensions_ptrs)
.enabled_layer_names(
&VALIDATION_LAYERS.iter().map(|&s| s.as_ptr()).collect::<Vec<*const i8>>()
);
let device = ash_check!(unsafe { instance.create_device(physical_device, &device_create_info, None) });
let graphics_queue = unsafe { device.get_device_queue(queue_family_indices.graphics_family, 0) };
let present_queue = unsafe { device.get_device_queue(queue_family_indices.present_family, 0) };
Ok((device, graphics_queue, present_queue))
}
struct SwapchainSupportDetails {
capabilities: vk::SurfaceCapabilitiesKHR,
formats: Vec<vk::SurfaceFormatKHR>,
present_modes: Vec<vk::PresentModeKHR>,
}
impl SwapchainSupportDetails {
fn is_adequate(&self) -> bool {
!self.formats.is_empty() && !self.present_modes.is_empty()
}
}
fn query_swapchain_support(
physical_device: vk::PhysicalDevice,
surface: &ash::extensions::khr::Surface,
surface_khr: vk::SurfaceKHR,
) -> Result<SwapchainSupportDetails> {
unsafe {
let capabilities = ash_check!(surface.get_physical_device_surface_capabilities(physical_device, surface_khr));
let formats = ash_check!(surface.get_physical_device_surface_formats(physical_device, surface_khr));
let present_modes = ash_check!(surface.get_physical_device_surface_present_modes(physical_device, surface_khr));
Ok(SwapchainSupportDetails {
capabilities,
formats,
present_modes,
})
}
}
fn choose_swapchain_format(available_formats: &[vk::SurfaceFormatKHR]) -> vk::SurfaceFormatKHR {
for &format in available_formats {
if format.format == vk::Format::B8G8R8A8_SRGB && format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR {
return format;
}
}
available_formats[0] // 如果没有找到理想的,就用第一个
}
fn choose_swapchain_present_mode(available_present_modes: &[vk::PresentModeKHR]) -> vk::PresentModeKHR {
for &mode in available_present_modes {
if mode == vk::PresentModeKHR::MAILBOX { // 垂直同步,避免撕裂,通常性能较好
return mode;
}
}
vk::PresentModeKHR::FIFO // 保证垂直同步
}
fn choose_swapchain_extent(capabilities: &vk::SurfaceCapabilitiesKHR, window: &Window) -> vk::Extent2D {
if capabilities.current_extent.width != u32::MAX {
capabilities.current_extent
} else {
let window_size = window.inner_size();
let mut actual_extent = vk::Extent2D {
width: window_size.width,
height: window_size.height,
};
actual_extent.width = actual_extent.width.clamp(
capabilities.min_image_extent.width,
capabilities.max_image_extent.width,
);
actual_extent.height = actual_extent.height.clamp(
capabilities.min_image_extent.height,
capabilities.max_image_extent.height,
);
actual_extent
}
}
fn create_swapchain(
instance: &Instance,
device: &Device,
physical_device: vk::PhysicalDevice,
surface_khr: vk::SurfaceKHR,
swapchain_loader: &ash::extensions::khr::Swapchain,
queue_family_indices: QueueFamilyIndices,
window: &Window,
old_swapchain: Option<vk::SwapchainKHR>,
) -> Result<(vk::SwapchainKHR, Vec<vk::Image>, vk::Format, vk::Extent2D)> {
let swapchain_support = Self::query_swapchain_support(physical_device, &ash::extensions::khr::Surface::new(instance, device), surface_khr)?;
let surface_format = Self::choose_swapchain_format(&swapchain_support.formats);
let present_mode = Self::choose_swapchain_present_mode(&swapchain_support.present_modes);
let extent = Self::choose_swapchain_extent(&swapchain_support.capabilities, window);
let mut image_count = swapchain_support.capabilities.min_image_count + 1;
if swapchain_support.capabilities.max_image_count > 0 && image_count > swapchain_support.capabilities.max_image_count {
image_count = swapchain_support.capabilities.max_image_count;
}
let (image_sharing_mode, queue_family_indices_vec) = if queue_family_indices.graphics_family != queue_family_indices.present_family {
(vk::SharingMode::CONCURRENT, vec![queue_family_indices.graphics_family, queue_family_indices.present_family])
} else {
(vk::SharingMode::EXCLUSIVE, vec![])
};
let mut create_info = vk::SwapchainCreateInfoKHR::builder()
.surface(surface_khr)
.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(image_sharing_mode)
.queue_family_indices(&queue_family_indices_vec)
.pre_transform(swapchain_support.capabilities.current_transform)
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
.present_mode(present_mode)
.clipped(true);
if let Some(old_swapchain) = old_swapchain {
create_info = create_info.old_swapchain(old_swapchain);
}
let swapchain = ash_check!(unsafe { swapchain_loader.create_swapchain(&create_info, None) });
let swapchain_images = ash_check!(unsafe { swapchain_loader.get_swapchain_images(swapchain) });
Ok((swapchain, swapchain_images, surface_format.format, extent))
}
fn create_image_views(
device: &Device,
images: &[vk::Image],
format: vk::Format,
) -> Result<Vec<vk::ImageView>> {
images.iter().map(|&image| {
let create_info = vk::ImageViewCreateInfo::builder()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(format)
.components(vk::ComponentMapping {
r: vk::ComponentSwizzle::IDENTITY,
g: vk::ComponentSwizzle::IDENTITY,
b: vk::ComponentSwizzle::IDENTITY,
a: vk::ComponentSwizzle::IDENTITY,
})
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
ash_check!(unsafe { device.create_image_view(&create_info, None) })
}).collect()
}
fn create_render_pass(device: &Device, format: vk::Format) -> Result<vk::RenderPass> {
let color_attachment = vk::AttachmentDescription::builder()
.format(format)
.samples(vk::SampleCountFlags::TYPE_1) // 不进行多重采样
.load_op(vk::AttachmentLoadOp::CLEAR) // 渲染前清除帧缓冲内容
.store_op(vk::AttachmentStoreOp::STORE) // 渲染后存储帧缓冲内容
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
.stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
.initial_layout(vk::ImageLayout::UNDEFINED) // 渲染前未定义布局
.final_layout(vk::ImageLayout::PRESENT_SRC_KHR); // 渲染后转换为呈现布局
let color_attachment_ref = vk::AttachmentReference::builder()
.attachment(0) // 索引到 attachments 数组的第一个附件
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL); // 渲染过程中使用的布局
let subpass = vk::SubpassDescription::builder()
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
.color_attachments(&[color_attachment_ref]);
let dependency = vk::SubpassDependency::builder()
.src_subpass(vk::SUBPASS_EXTERNAL) // 外部子通道 (即渲染通道开始之前)
.dst_subpass(0) // 我们的第一个子通道
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.src_access_mask(vk::AccessFlags::empty())
.dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
let render_pass_info = vk::RenderPassCreateInfo::builder()
.attachments(&[color_attachment])
.subpasses(&[subpass])
.dependencies(&[dependency]);
ash_check!(unsafe { device.create_render_pass(&render_pass_info, None) })
}
fn create_graphics_pipeline(
device: &Device,
render_pass: vk::RenderPass,
swapchain_extent: vk::Extent2D,
) -> Result<(vk::PipelineLayout, vk::Pipeline)> {
// 编译 shader (已在 build.rs 中完成)
// 从 SpirvBuilder 获取编译后的 SPIR-V 路径
let vert_shader_path = spirv_builder::module_path_from_cargo_pkg("shader", "vert.spv")?;
let frag_shader_path = spirv_builder::module_path_from_cargo_pkg("shader", "frag.spv")?;
let vert_shader_code = std::fs::read(vert_shader_path)?;
let frag_shader_code = std::fs::read(frag_shader_path)?;
let vert_shader_module_info = vk::ShaderModuleCreateInfo::builder()
.code(vk::util::read_spv(&vert_shader_code)?);
let vert_shader_module = ash_check!(unsafe { device.create_shader_module(&vert_shader_module_info, None) });
let frag_shader_module_info = vk::ShaderModuleCreateInfo::builder()
.code(vk::util::read_spv(&frag_shader_code)?);
let frag_shader_module = ash_check!(unsafe { device.create_shader_module(&frag_shader_module_info, None) });
let main_function_name = CString::new("main").unwrap(); // 着色器中的入口函数名
let vert_shader_stage_info = vk::PipelineShaderStageCreateInfo::builder()
.stage(vk::ShaderStageFlags::VERTEX)
.module(vert_shader_module)
.name(&main_function_name);
let frag_shader_stage_info = vk::PipelineShaderStageCreateInfo::builder()
.stage(vk::ShaderStageFlags::FRAGMENT)
.module(frag_shader_module)
.name(&main_function_name);
let shader_stages = [vert_shader_stage_info.build(), frag_shader_stage_info.build()];
// 顶点输入 (我们不使用实际的顶点缓冲区,而是通过 gl_VertexIndex 自动生成顶点)
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder()
.vertex_binding_descriptions(&[])
.vertex_attribute_descriptions(&[]);
// 输入装配 (绘制三角形列表)
let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::builder()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST)
.primitive_restart_enable(false);
// 视口和裁剪矩形
let viewport = vk::Viewport::builder()
.x(0.0)
.y(0.0)
.width(swapchain_extent.width as f32)
.height(swapchain_extent.height as f32)
.min_depth(0.0)
.max_depth(1.0);
let scissor = vk::Rect2D::builder()
.offset(vk::Offset2D { x: 0, y: 0 })
.extent(swapchain_extent);
let viewport_state = vk::PipelineViewportStateCreateInfo::builder()
.viewports(&[viewport])
.scissors(&[scissor]);
// 光栅化
let rasterizer = vk::PipelineRasterizationStateCreateInfo::builder()
.depth_clamp_enable(false)
.rasterizer_discard_enable(false)
.polygon_mode(vk::PolygonMode::FILL)
.line_width(1.0)
.cull_mode(vk::CullModeFlags::BACK)
.front_face(vk::FrontFace::CLOCKWISE) // 顺时针为正面
.depth_bias_enable(false);
// 多重采样 (禁用)
let multisampling = vk::PipelineMultisampleStateCreateInfo::builder()
.sample_shading_enable(false)
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
// 颜色混合
let color_blend_attachment = vk::PipelineColorBlendAttachmentState::builder()
.color_write_mask(
vk::ColorComponentFlags::R
| vk::ColorComponentFlags::G
| vk::ColorComponentFlags::B
| vk::ColorComponentFlags::A,
)
.blend_enable(false);
let color_blending = vk::PipelineColorBlendStateCreateInfo::builder()
.logic_op_enable(false)
.attachments(&[color_blend_attachment]);
// 管线布局 (我们不使用 Uniforms 或 Push 常量,所以布局是空的)
let pipeline_layout_info = vk::PipelineLayoutCreateInfo::builder();
let pipeline_layout = ash_check!(unsafe { device.create_pipeline_layout(&pipeline_layout_info, None) });
// 创建图形管线
let graphics_pipeline_info = vk::GraphicsPipelineCreateInfo::builder()
.stages(&shader_stages)
.vertex_input_state(&vertex_input_info)
.input_assembly_state(&input_assembly)
.viewport_state(&viewport_state)
.rasterization_state(&rasterizer)
.multisample_state(&multisampling)
.color_blend_state(&color_blending)
.layout(pipeline_layout)
.render_pass(render_pass)
.subpass(0); // 我们的第一个也是唯一的子通道
let graphics_pipeline = ash_check!(unsafe {
device.create_graphics_pipelines(vk::PipelineCache::null(), &[graphics_pipeline_info.build()], None)
})[0];
// 销毁着色器模块 (它们在管线创建后就不再需要了)
unsafe {
device.destroy_shader_module(vert_shader_module, None);
device.destroy_shader_module(frag_shader_module, None);
}
Ok((pipeline_layout, graphics_pipeline))
}
fn create_framebuffers(
device: &Device,
render_pass: vk::RenderPass,
image_views: &[vk::ImageView],
extent: vk::Extent2D,
) -> Result<Vec<vk::Framebuffer>> {
image_views.iter().map(|&image_view| {
let attachments = [image_view];
let framebuffer_info = vk::FramebufferCreateInfo::builder()
.render_pass(render_pass)
.attachments(&attachments)
.width(extent.width)
.height(extent.height)
.layers(1);
ash_check!(unsafe { device.create_framebuffer(&framebuffer_info, None) })
}).collect()
}
fn create_command_pool(device: &Device, graphics_family: u32) -> Result<vk::CommandPool> {
let pool_info = vk::CommandPoolCreateInfo::builder()
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER) // 允许重置命令缓冲区
.queue_family_index(graphics_family);
ash_check!(unsafe { device.create_command_pool(&pool_info, None) })
}
fn create_command_buffers(
device: &Device,
command_pool: vk::CommandPool,
render_pass: vk::RenderPass,
framebuffers: &[vk::Framebuffer],
graphics_pipeline: vk::Pipeline,
swapchain_extent: vk::Extent2D,
) -> Result<Vec<vk::CommandBuffer>> {
let allocate_info = vk::CommandBufferAllocateInfo::builder()
.command_pool(command_pool)
.level(vk::CommandBufferLevel::PRIMARY) // 主命令缓冲区
.command_buffer_count(framebuffers.len() as u32);
let command_buffers = ash_check!(unsafe { device.allocate_command_buffers(&allocate_info) });
for (i, &command_buffer) in command_buffers.iter().enumerate() {
let begin_info = vk::CommandBufferBeginInfo::builder()
.flags(vk::CommandBufferUsageFlags::SIMULTANEOUS_USE); // 可以同时使用多次
ash_check!(unsafe { device.begin_command_buffer(command_buffer, &begin_info) });
let clear_value = vk::ClearValue {
color: vk::ClearColorValue {
float32: [0.0, 0.0, 0.0, 1.0], // 黑色背景
},
};
let render_pass_begin_info = vk::RenderPassBeginInfo::builder()
.render_pass(render_pass)
.framebuffer(framebuffers[i])
.render_area(vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: swapchain_extent,
})
.clear_values(&[clear_value]);
unsafe {
device.cmd_begin_render_pass(command_buffer, &render_pass_begin_info, vk::SubpassContents::INLINE);
device.cmd_bind_pipeline(command_buffer, vk::PipelineBindPoint::GRAPHICS, graphics_pipeline);
// 绘制 3 个顶点,1 个实例,从第 0 个顶点开始,第 0 个实例开始
device.cmd_draw(command_buffer, 3, 1, 0, 0);
device.cmd_end_render_pass(command_buffer);
ash_check!(device.end_command_buffer(command_buffer));
}
}
Ok(command_buffers)
}
fn create_sync_objects(device: &Device, count: usize) -> Result<(Vec<vk::Semaphore>, Vec<vk::Semaphore>, Vec<vk::Fence>)> {
let mut image_available_semaphores = Vec::with_capacity(count);
let mut render_finished_semaphores = Vec::with_capacity(count);
let mut in_flight_fences = Vec::with_capacity(count);
let semaphore_info = vk::SemaphoreCreateInfo::builder();
let fence_info = vk::FenceCreateInfo::builder()
.flags(vk::FenceCreateFlags::SIGNALED); // 初始状态为已发信号,这样第一次循环不会卡住
for _ in 0..count {
image_available_semaphores.push(ash_check!(unsafe { device.create_semaphore(&semaphore_info, None) }));
render_finished_semaphores.push(ash_check!(unsafe { device.create_semaphore(&semaphore_info, None) }));
in_flight_fences.push(ash_check!(unsafe { device.create_fence(&fence_info, None) }));
}
Ok((image_available_semaphores, render_finished_semaphores, in_flight_fences))
}
// --- 渲染循环和事件处理 ---
fn recreate_swapchain(&mut self) -> Result<()> {
unsafe {
// 等待设备空闲
ash_check!(self.device.device_wait_idle());
// 清理旧的交换链相关资源
for &framebuffer in &self.framebuffers {
self.device.destroy_framebuffer(framebuffer, None);
}
self.device.free_command_buffers(self.command_pool, &self.command_buffers);
self.device.destroy_pipeline(self.graphics_pipeline, None);
self.device.destroy_pipeline_layout(self.pipeline_layout, None);
self.device.destroy_render_pass(self.render_pass, None);
for &image_view in &self.swapchain_image_views {
self.device.destroy_image_view(image_view, None);
}
self.swapchain_loader.destroy_swapchain(self.swapchain, None);
// 重新查询交换链支持,因为窗口大小可能改变
let queue_family_indices = Self::find_queue_families(
&self.instance,
self.physical_device,
&self.surface,
self.surface_khr,
)?;
let (
new_swapchain,
new_swapchain_images,
new_swapchain_format,
new_swapchain_extent,
) = Self::create_swapchain(
&self.instance,
&self.device,
self.physical_device,
self.surface_khr,
&self.swapchain_loader,
queue_family_indices,
&self.window,
Some(self.swapchain), // 传递旧的交换链,以便驱动程序优化
)?;
self.swapchain = new_swapchain;
self.swapchain_images = new_swapchain_images;
// 交换链格式和范围也可能改变,虽然我们在这里没有直接存储,但在重新创建图像视图和管线时会用到
// 实际上,为了简化,我们假设格式和物理设备在整个生命周期内不变
// 但 extent 和 image_views 肯定是新的
let new_swapchain_image_views = Self::create_image_views(
&self.device,
&self.swapchain_images,
new_swapchain_format,
)?;
self.swapchain_image_views = new_swapchain_image_views;
self.render_pass = Self::create_render_pass(&self.device, new_swapchain_format)?;
let (new_pipeline_layout, new_graphics_pipeline) = Self::create_graphics_pipeline(
&self.device,
self.render_pass,
new_swapchain_extent,
)?;
self.pipeline_layout = new_pipeline_layout;
self.graphics_pipeline = new_graphics_pipeline;
let new_framebuffers = Self::create_framebuffers(
&self.device,
self.render_pass,
&self.swapchain_image_views,
new_swapchain_extent,
)?;
self.framebuffers = new_framebuffers;
// 重新录制命令缓冲区
self.command_buffers = Self::create_command_buffers(
&self.device,
self.command_pool,
self.render_pass,
&self.framebuffers,
self.graphics_pipeline,
new_swapchain_extent,
)?;
self.resized = false;
}
Ok(())
}
fn draw_frame(&mut self) -> Result<()> {
unsafe {
// 1. 等待前一帧完成
let current_fence = self.in_flight_fences[self.current_frame];
ash_check!(self.device.wait_for_fences(&[current_fence], true, u64::MAX));
// 2. 从交换链获取图像
let image_available_semaphore = self.image_available_semaphores[self.current_frame];
let (image_index, suboptimal) = match self.swapchain_loader.acquire_next_image(
self.swapchain,
u64::MAX,
image_available_semaphore,
vk::Fence::null(),
) {
Ok((idx, suboptimal)) => (idx, suboptimal),
Err(vk_result) => match vk_result {
vk::Result::ERROR_OUT_OF_DATE_KHR => {
self.resized = true; // 交换链过期,需要重建
return Ok(());
},
_ => anyhow::bail!("Failed to acquire next image: {:?}", vk_result),
},
};
// 如果交换链过时,或者需要重建,则跳过当前帧
if suboptimal {
self.resized = true;
return Ok(());
}
ash_check!(self.device.reset_fences(&[current_fence])); // 重置 fence 以便下次使用
// 3. 提交命令缓冲区
let command_buffer = self.command_buffers[image_index as usize];
let render_finished_semaphore = self.render_finished_semaphores[self.current_frame];
let wait_semaphores = [image_available_semaphore];
let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
let signal_semaphores = [render_finished_semaphore];
let submit_info = vk::SubmitInfo::builder()
.wait_semaphores(&wait_semaphores)
.wait_dst_stage_mask(&wait_stages)
.command_buffers(&[command_buffer])
.signal_semaphores(&signal_semaphores);
ash_check!(self.device.queue_submit(
self.graphics_queue,
&[submit_info.build()],
current_fence,
));
// 4. 呈现图像到屏幕
let present_info = vk::PresentInfoKHR::builder()
.wait_semaphores(&signal_semaphores)
.swapchains(&[self.swapchain])
.image_indices(&[image_index]);
let present_result = ash_check!(self.swapchain_loader.queue_present(self.present_queue, &present_info));
if present_result || self.resized { // 如果呈现过时或窗口已调整大小
self.resized = true;
}
self.current_frame = (self.current_frame + 1) % self.in_flight_fences.len();
}
Ok(())
}
pub fn run(mut self) -> Result<()> {
let event_loop = std::mem::replace(&mut self.event_loop, EventLoop::new()?); // 获取所有权
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Poll; // 持续触发事件
match event {
Event::NewEvents(StartCause::Init) => {
println!("Application started.");
},
Event::WindowEvent { event, window_id: _ } => {
match event {
WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
},
WindowEvent::Resized(_new_size) => {
self.resized = true;
},
_ => {},
}
},
Event::MainEventsCleared => {
// 主事件循环空闲时,触发绘制
if self.resized {
// 在调整大小事件后重新创建交换链
if let Err(e) = self.recreate_swapchain() {
eprintln!("Error recreating swapchain: {:?}", e);
*control_flow = ControlFlow::Exit;
}
}
if let Err(e) = self.draw_frame() {
eprintln!("Error drawing frame: {:?}", e);
*control_flow = ControlFlow::Exit;
}
},
Event::LoopDestroyed => {
// 应用程序退出时,等待 GPU 空闲,确保所有操作完成
unsafe {
if let Err(e) = self.device.device_wait_idle() {
eprintln!("Error waiting for device idle on exit: {:?}", e);
}
}
println!("Application exited gracefully.");
},
_ => {},
}
})?;
Ok(())
}
}
// ============== 资源清理 ==============
impl Drop for VulkanApp {
fn drop(&mut self) {
unsafe {
// 等待设备空闲,确保所有命令都已执行完毕
self.device.device_wait_idle().expect(“Failed to wait for device idle on drop!”);
// 销毁同步对象
for i in 0..self.in_flight_fences.len() {
self.device.destroy_semaphore(self.render_finished_semaphores[i], None);
self.device.destroy_semaphore(self.image_available_semaphores[i], None);
self.device.destroy_fence(self.in_flight_fences[i], None);
}
// 销毁命令池
self.device.destroy_command_pool(self.command_pool, None);
// 销毁帧缓冲
for &framebuffer in &self.framebuffers {
self.device.destroy_framebuffer(framebuffer, None);
}
// 销毁图形管线和管线布局
self.device.destroy_pipeline(self.graphics_pipeline, None);
self.device.destroy_pipeline_layout(self.pipeline_layout, None);
// 销毁渲染通道
self.device.destroy_render_pass(self.render_pass, None);
// 销毁图像视图
for &image_view in &self.swapchain_image_views {
self.device.destroy_image_view(image_view, None);
}
// 销毁交换链
self.swapchain_loader.destroy_swapchain(self.swapchain, None);
// 销毁表面
self.surface.destroy_surface(self.surface_khr, None);
// 销毁逻辑设备
self.device.destroy_device(None);
// 销毁调试信使 (如果存在)
#[cfg(debug_assertions)]
self.debug_utils.destroy_debug_utils_messenger(self.debug_messenger, None);
// 销毁 Vulkan 实例
self.instance.destroy_instance(None);
}
}
}
// winit 和 ash 的 Surface 兼容层
mod winit_surface {
use ash::Entry;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use vk::SurfaceKHR;
use std::os::raw::c_void;
#[cfg(target_os = "windows")]
pub unsafe fn create_surface(
entry: &Entry,
instance: &ash::Instance,
window: &winit::window::Window,
) -> ash::Result<SurfaceKHR> {
use ash::extensions::khr::Win32Surface;
use vk::Win32SurfaceCreateInfoKHR;
let surface_create_info = Win32SurfaceCreateInfoKHR::builder()
.hinstance(window.raw_window_handle().as_win32().unwrap().hinstance)
.hwnd(window.raw_window_handle().as_win32().unwrap().hwnd);
let win32_surface_loader = Win32Surface::new(entry, instance);
win32_surface_loader.create_win32_surface(&surface_create_info, None)
}
#[cfg(target_os = "android")]
pub unsafe fn create_surface(
entry: &Entry,
instance: &ash::Instance,
window: &winit::window::Window,
) -> ash::Result<SurfaceKHR> {
use ash::extensions::khr::AndroidSurface;
use vk::AndroidSurfaceCreateInfoKHR;
let surface_create_info = AndroidSurfaceCreateInfoKHR::builder()
.window(window.raw_window_handle().as_android().unwrap().a_native_window);
let android_surface_loader = AndroidSurface::new(entry, instance);
android_surface_loader.create_android_surface(&surface_create_info, None)
}
#[cfg(target_os = "linux")]
pub unsafe fn create_surface(
entry: &Entry,
instance: &ash::Instance,
window: &winit::window::Window,
) -> ash::Result<SurfaceKHR> {
match window.raw_window_handle() {
RawWindowHandle::Xlib(handle) => {
use ash::extensions::khr::XlibSurface;
use vk::XlibSurfaceCreateInfoKHR;
let surface_create_info = XlibSurfaceCreateInfoKHR::builder()
.dpy(handle.display as *mut c_void)
.window(handle.window);
let xlib_surface_loader = XlibSurface::new(entry, instance);
xlib_surface_loader.create_xlib_surface(&surface_create_info, None)
}
RawWindowHandle::Xcb(handle) => {
use ash::extensions::khr::XcbSurface;
use vk::XcbSurfaceCreateInfoKHR;
let surface_create_info = XcbSurfaceCreateInfoKHR::builder()
.connection(handle.connection as *mut c_void)
.window(handle.window);
let xcb_surface_loader = XcbSurface::new(entry, instance);
xcb_surface_loader.create_xcb_surface(&surface_create_info, None)
}
RawWindowHandle::Wayland(handle) => {
use ash::extensions::khr::WaylandSurface;
use vk::WaylandSurfaceCreateInfoKHR;
let surface_create_info = WaylandSurfaceCreateInfoKHR::builder()
.display(handle.display)
.surface(handle.surface);
let wayland_surface_loader = WaylandSurface::new(entry, instance);
wayland_surface_loader.create_wayland_surface(&surface_create_info, None)
}
_ => panic!("Unsupported Linux window handle type!"),
}
}
#[cfg(target_os = "macos")]
pub unsafe fn create_surface(
entry: &Entry,
instance: &ash::Instance,
window: &winit::window::Window,
) -> ash::Result<SurfaceKHR> {
use ash::extensions::ext::MetalSurface;
use ash::vk::MetalSurfaceCreateInfoEXT;
use core_graphics_types::base::CGFloat;
use core_graphics_types::geometry::CGSize;
use metal::{Layer, MetalLayer};
use objc::{rc::autoreleasepool, runtime::YES};
use raw_window_handle::RawWindowHandle;
let raw_handle = window.raw_window_handle();
let view = match raw_handle {
RawWindowHandle::AppKit(handle) => handle.ns_view,
_ => panic!("Unsupported macOS window handle type!"),
};
let size = window.inner_size();
let layer = MetalLayer::new();
layer.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
layer.set_contents_scale(window.scale_factor() as CGFloat); // IMPORTANT
layer.set_frame(cgmath::Rect::new(0.0, 0.0, size.width as CGFloat, size.height as CGFloat).to_box());
let view_obj = unsafe { objc::runtime::NSObject::from_ptr(view as *mut objc::runtime::NSObject) };
view_obj.setWantsLayer(YES);
view_obj.setLayer(Some(&layer));
let surface_create_info = MetalSurfaceCreateInfoEXT::builder().layer(layer.as_ptr() as *const c_void);
let metal_surface_loader = MetalSurface::new(entry, instance);
metal_surface_loader.create_metal_surface(&surface_create_info, None)
}
#[cfg(not(any(target_os = "windows", target_os = "android", target_os = "linux", target_os = "macos")))]
pub unsafe fn create_surface(
_entry: &Entry,
_instance: &ash::Instance,
_window: &winit::window::Window,
) -> ash::Result<SurfaceKHR> {
panic!("Unsupported platform for surface creation!");
}
}
fn main() -> Result<()> {
let window_builder = WindowBuilder::new()
.with_title(“Rust Vulkan Triangle”)
.with_inner_size(winit::dpi::LogicalSize::new(800, 600));
let app = VulkanApp::new(window_builder)?;
app.run()
}
“`
代码结构和关键点解释:
- 宏
ash_check!
: 简单地将ash::Result
转换为anyhow::Result
,方便错误处理。 - 常量和调试层: 定义了应用程序名称、版本、Vulkan API 版本、必需的设备扩展 (
VK_KHR_swapchain
)。在debug_assertions
模式下,启用了VK_LAYER_KHR_validation
验证层,它会在 Vulkan API 使用不当或出现错误时打印详细信息,这对于开发和调试至关重要。 VulkanApp
结构体: 封装了所有 Vulkan 相关的对象和状态,包括Entry
、Instance
、Device
、Swapchain
、Pipeline
等。它通过Drop
trait 实现了所有 Vulkan 资源的自动清理。VulkanApp::new
: 这是应用程序的初始化入口,它按顺序调用了一系列辅助函数来完成 Vulkan 资源的创建。create_instance
: 创建 Vulkan 实例。这是应用程序与 Vulkan 运行时通信的起点。它需要指定应用程序信息和必要的扩展(如VK_KHR_surface
和平台特定的表面扩展)。setup_debug_messenger
: (仅调试模式)设置 Vulkan 调试回调,用于接收验证层报告的警告和错误。winit_surface::create_surface
: 使用winit
和ash_window
库创建 Vulkan 表面,将 Vulkan 与操作系统窗口连接起来。pick_physical_device
: 遍历系统中的所有物理设备(GPU),选择一个满足我们需求的设备(支持图形队列、呈现队列、交换链扩展)。create_logical_device
: 基于选定的物理设备,创建一个逻辑设备。这是我们实际与之交互的 GPU 抽象。我们还会获取图形和呈现队列的句柄。create_swapchain
: 创建交换链。这是在屏幕上显示渲染图像的关键组件。它涉及到选择最佳的图像格式、呈现模式和图像数量。create_image_views
: 为交换链中的每个图像创建图像视图。图像视图定义了如何解释和使用这些图像。create_render_pass
: 创建渲染通道。它定义了渲染操作的步骤和附件(如颜色缓冲区)的行为。create_graphics_pipeline
: 构建图形管线,这是 Vulkan 最复杂的部分之一。它包含了所有渲染阶段的配置:- Shader Modules: 加载并创建顶点和片段着色器模块。这里的着色器是 Rust 编写的,通过
spirv-builder
在编译时自动转换为 SPIR-V。- 顶点着色器:使用
gl_VertexIndex
内置变量,无需提供实际的顶点缓冲区,直接计算三角形的三个顶点位置。 - 片段着色器:简单地输出红色。
- 顶点着色器:使用
- Vertex Input: 配置顶点输入,本例中由于使用
gl_VertexIndex
,所以为空。 - Input Assembly: 定义图元类型(三角形列表)。
- Viewport & Scissor: 定义渲染区域。
- Rasterization: 光栅化设置,如多边形模式、剔除面。
- Multisampling: 多重采样设置(本例禁用)。
- Color Blending: 颜色混合设置(本例禁用)。
- Pipeline Layout: 定义着色器资源的布局(本例为空)。
- Shader Modules: 加载并创建顶点和片段着色器模块。这里的着色器是 Rust 编写的,通过
create_framebuffers
: 为每个交换链图像创建帧缓冲,将它们与渲染通道关联起来。create_command_pool
&create_command_buffers
: 创建命令池和命令缓冲区。命令缓冲区用于记录所有的绘制命令。在这里,我们记录了开始渲染通道、绑定管线和绘制三角形的命令。create_sync_objects
: 创建同步原语(信号量和围栏),用于协调 CPU 和 GPU 之间的操作以及 GPU 内部的图像获取、渲染和呈现。
recreate_swapchain
: 实现了当窗口大小改变时,重建交换链和所有依赖于它的资源(图像视图、帧缓冲、管线、命令缓冲区)的逻辑。这是 Vulkan 应用中非常重要且容易出错的部分。draw_frame
: 渲染循环的核心,负责每一帧的绘制逻辑:- 等待前一帧完成(通过围栏)。
- 从交换链获取下一个可用的图像索引(通过信号量)。
- 提交记录好的命令缓冲区到图形队列。
- 将渲染好的图像呈现到屏幕(通过信号量)。
run
方法: 主事件循环,使用winit
库处理窗口事件(如关闭、调整大小)并调用draw_frame
进行渲染。Drop
实现: 这是 Rust 安全性的一个体现。当VulkanApp
实例超出作用域时,drop
方法会被自动调用,负责按照正确的顺序销毁所有 Vulkan 资源,防止内存泄漏和未定义行为。
运行你的程序
- 编译着色器: 确保你的
build.rs
文件和shader
目录结构正确。cargo build
会自动运行build.rs
编译着色器。 - 运行程序:
bash
cargo run
如果一切顺利,一个标题为 “Rust Vulkan Triangle” 的窗口将弹出,并在黑色背景上显示一个红色的三角形。
如果出现错误,请仔细检查控制台输出。在调试模式下,Vulkan 验证层会提供非常有用的错误信息,帮助你定位问题。
第五章:超越三角形:下一步的探索
恭喜你!你已经成功地在 Rust 中用 Vulkan 绘制了第一个三角形。这虽然看起来简单,但其背后包含了 Vulkan 初始化、渲染管线、资源管理和同步的复杂流程。现在,你已经掌握了最核心的基础知识,可以开始更深入的探索:
- 顶点缓冲区与索引缓冲区: 我们的三角形是通过
gl_VertexIndex
虚拟生成的。真正的应用会从 CPU 内存(顶点缓冲区)加载顶点数据到 GPU 内存,并可能使用索引缓冲区来高效绘制复杂网格。 - 统一缓冲区 (Uniform Buffers): 学习如何向着色器传递动态数据,例如模型-视图-投影矩阵,实现相机控制和物体变换。
- 纹理映射: 了解如何加载图像数据并将其作为纹理传递给片段着色器,为物体添加细节。
- 深度测试与深度缓冲区: 添加深度缓冲区以正确处理物体之间的遮挡关系。
- 描述符集 (Descriptor Sets): 这是 Vulkan 中管理着色器资源(如统一缓冲区、纹理)绑定的核心机制,对于复杂场景必不可少。
- 内存管理: Vulkan 的内存管理非常手动。你可以尝试使用
vk-mem-alloc
或gpu-allocator
这样的库来简化 GPU 内存的分配和管理。 - 多线程渲染: 尝试利用 Vulkan 的多线程友好特性,在不同线程上构建命令缓冲区。
- 异步加载: 在不阻塞主渲染循环的情况下加载资源。
- 更复杂的渲染管线: 尝试实现更高级的渲染技术,如延迟渲染、基于物理的渲染 (PBR)、阴影贴图等。
- 错误处理和鲁棒性: 增强错误处理机制,确保应用程序在各种异常情况下都能优雅地关闭或恢复。
- 使用更高级的封装库: 当你对
ash
的底层机制足够熟悉后,可以尝试转向vulkano
,它提供了更高的抽象层次和更多的便利功能,可以显著提高开发效率。
总结
本篇文章深入浅出地介绍了 Rust Vulkan 开发的整个流程,从环境搭建到核心概念,再到实际的代码实现。你已经亲手构建了一个在屏幕上绘制三角形的 Vulkan 应用程序,并在这个过程中接触到了 Vulkan 最基础但最重要的组件。
Rust 的内存安全和性能与 Vulkan 的极致控制相结合,为你打开了高性能图形编程的大门。虽然 Vulkan 的学习曲线确实陡峭,但每一次克服挑战都会带来对 GPU 工作原理更深刻的理解。
希望这篇指南能为你点亮 Rust Vulkan 之路上的第一盏灯。记住,图形编程是一场漫长而精彩的旅程,持续学习和实践是成功的关键。祝你在探索 Vulkan 世界的旅程中一切顺利,创作出令人惊叹的视觉效果!
资源
- Vulkan 官方规范: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/vkspec.html
- Vulkan SDK: https://sdk.lunarg.com/sdk/download/
ash
crate 文档: https://docs.rs/ash/winit
crate 文档: https://docs.rs/winit/- Rust GPU (SPIR-V for Rust): https://github.com/EmbarkStudios/rust-gpu (我们使用了其底层
spirv-std
和spirv-builder
) - Vulkan-tutorial.com (C++): 尽管是 C++ 教程,但其概念和步骤对于理解 Vulkan 流程非常有帮助:https://vulkan-tutorial.com/