依赖与窗口
部分读者可能已经熟悉如何在 Rust 中打开窗口程序,且有自己偏好的窗口管理库。但本教程是为所有人设计的,所以不免要涉及这部分的内容。所幸你可以跳过这部分,但有一点值得了解,即无论使用什么样的窗口解决方案,都需要实现 raw-window-handle 里定义的 raw_window_handle()
及 raw_display_handle()
两个抽象接口。如果有兴趣自己动手来为 wgpu 实现一个基础的窗口,可以参考 wgpu-in-app,与 Android App 集成这一章节也有详情的介绍。
我们要使用哪些包?
我们将尽量保持基础部分的简单性。后续我们会逐渐添加依赖,先列出相关的 Cargo.toml
依赖项如下:
[dependencies]
winit = "0.29.10"
env_logger = "0.10"
log = "0.4"
wgpu = "0.19"
使用 Rust 的新版解析器
自 0.10 版本起,wgpu 需要使用 cargo 的 新版特性解析器,这在 Rust 的 2021 edition(即任何基于 Rust 1.56.0 或更新版本的新项目)中是默认启用的。但如果你仍在使用 2018 edition,那么就需要在单包项目 Cargo.toml
的 [package]
配置中,或者在⼯作空间的根级 Cargo.toml
的 [workspace]
配置中添加 resolver = "2"
项。
关于 env_logger
通过 env_logger::init()
来启用日志是非常重要的。当 wgpu 遇到各类错误时,它都会用一条通用性的消息抛出 panic,并通过日志包来记录实际的错误信息。 也就是说,如果不添加 env_logger::init()
,wgpu 将静默地退出,从而令你非常困惑!
(下面的代码中已经启用)
创建一个新项目
运行 cargo new xxx
,xxx 是指你的项目名称。
(下面的例子中我使用了 tutorial1_window
)
示例代码
这一部分没有什么特别之处,所以直接贴出完整的代码。只需将其粘贴到你的 main.rs
中即可:
use winit::{
event::*,
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
pub fn run() {
env_logger::init();
let event_loop = EventLoop::new().unwrap();
let window = WindowBuilder::new().build(&event_loop).unwrap();
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
use winit::platform::web::EventLoopExtWebSys;
let event_loop_function = EventLoop::spawn;
} else {
let event_loop_function = EventLoop::run;
}
}
let _ = (event_loop_function)(
event_loop,
move |event: Event<()>, elwt: &EventLoopWindowTarget<()>| {
if event == Event::NewEvents(StartCause::Init) {
state.start();
}
if let Event::WindowEvent { event, .. } = event {
match event {
WindowEvent::KeyboardInput {
event:
KeyEvent {
logical_key: Key::Named(NamedKey::Escape),
..
},
..
} | WindowEvent::CloseRequested => elwt.exit(),
_ => {}
}
}
});
}
上述代码所做的全部工作就是创建了一个窗口,并在用户关闭或按下 escape 键前使其保持打开。接下来,我们需要在入口函数中运行这些代码。很简单,只需在 main()
函数中调用 run()
,然后运行!
fn main() {
run();
}
(其中 tutorial1_window
是你之前用 cargo 创建的项目的名称)
当你只打算支持桌面环境时,上边这些就是全部所要做的!在下一个教程中,我们将真正开始使用 wgpu!
添加对 web 的支持
如果讲完了这个关于 WebGPU 的教程,却不提如何在 web 上使用它,那么这个教程就是不完整的。幸运的是,让一个 wgpu 程序在浏览器中运行并不难。
让我们从修改 Cargo.toml
开始:
[lib]
crate-type = ["cdylib", "rlib"]
这几行告诉 cargo 允许项目构建(build)一个本地的 Rust 静态库(rlib)和一个 C/C++ 兼容库(cdylib)。 我们需要 rlib 来在桌面环境中运行 wgpu,需要 cdylib 来构建在浏览器中运行的 Web Assembly。
仅在需要将项目做为其他 Rust 项目的包(crate)提供时,[lib]
项的配置才是必须的。所以我们的示例程序可以省略上面这一步。
添加上述 [lib]
内容依赖于像原作者那样将主要代码写入一个 lib.rs
文件,而如果想要通过下文的 wasm-pack 方法构建,则需要进行上述步骤。
Web Assembly
Web Assembly 即 WASM,是大多数现代浏览器支持的二进制格式,它令 Rust 等底层语言能在网页上运行。这允许我们用 Rust 编写应用程序,并使用几行 Javascript 来加载它到 Web 浏览器中运行。
现在,我们仅需添加一些专门用于在 WASM 中运行的依赖项:
[dependencies]
cfg-if = "1"
# 其他常规依赖...
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
console_log = "1.0"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.34"
web-sys = { version = "0.3.64", features = [
"Document",
"Window",
"Element",
]}
cfg-if提供了一个宏,使得更加容易管理特定平台的代码。
[target.'cfg(target_arch = "wasm32")'.dependencies]
行告诉 cargo,如果我们的目标是 wasm32 架构,则只包括这些依赖项。接下来的几个依赖项只是让我们与 javascript 的交互更容易。
- console_error_panic_hook 配置
panic!
宏以将错误发送到 javascript 控制台。如果没有这个,当遇到程序崩溃时,你就会对导致崩溃的原因一无所知。 - console_log 实现了 log API。它将所有日志发送到 javascript 控制台。它还可以配置为仅发送特定级别的日志,这非常适合用于调试。
- 当我们想在大多数当前浏览器上运行时,就需要在 wgpu 上启用
WebGL
功能。因为目前只在 Firefox Nightly、Chrome/Edge 113+、 Chrome/Edge Canary 才支持直接使用 WebGPU API。
为了简单起见,教程大部分代码的编译会使用 WebGL 功能,直到 WebGPU API 达到一个更稳定的状态。
如果你想了解更多详细信息,请查看 wgpu 源码仓库 上的 web 编译指南 - wasm-bindgen 是此列表中最重要的依赖项。它负责生成样板代码,并告诉浏览器如何使用我们的项目。它还允许我们在 Rust 中公开可在 Javascript 中使用的函数,反之亦然。
我不会详细介绍 wasm-bindgen,所以如果你需要入门(或者是复习),请查看这里 - web-sys 是一个包含了许多在 javascript 程序中可用的函数和结构体的工具箱,如:
get_element_by_id
、append_child
。features = [...]
数组里列出的是我们目前最低限度需要的功能。
更多示例代码
首先, 我们需要在 main.rs
内引入 wasm-bindgen
:
#[cfg(target_arch="wasm32")]
use wasm_bindgen::prelude::*;
接下来,需要告诉 wasm-bindgen 在 WASM 被加载后执行我们的 run()
函数。
#[cfg_attr(target_arch="wasm32", wasm_bindgen(start))]
pub async fn run() {
// 省略的代码...
}
然后需要根据是否在 WASM 环境来切换我们正在使用的日志包。在 run()
函数内添加以下代码替换 env_logger::init()
行。
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Warn).expect("无法初始化日志库");
} else {
env_logger::init();
}
}
上边的代码判断了构建目标,在 web 构建中设置 console_log
和 console_error_panic_hook
。这很重要,因为 env_logger
目前不支持 Web Assembly。
另一种实现
在第 3~8 章,run()
函数及遍历 event_loop 的代码被统一封装到了 framework.rs
中, 还定义了 Action
trait 来抽象每一章中不同的 State
。 然后通过调用 wasm_bindgen_futures 包的 spawn_local
函数来创建 State
实例并处理 JS 异常。
第 1 ~ 2 章的代码通过 cargo run-wasm --bin xxx
运行时,在浏览器的控制台中会看到的 ...Using exceptions for control flow, don't mind me. This isn't actually an error!
错误现在被消除了:
#[cfg(target_arch = "wasm32")]
pub fn run<A: Action + 'static>() {
// ...
wasm_bindgen_futures::spawn_local(async move {
let (event_loop, instance) = create_action_instance::<A>().await;
let run_closure = Closure::once_into_js(move || start_event_loop::<A>(event_loop, instance));
// 处理运行过程中抛出的 JS 异常。
// 否则 wasm_bindgen_futures 队列将中断,且不再处理任何任务。
if let Err(error) = call_catch(&run_closure) {
// ...
}
}
}
接下来,在创建了事件循环与窗口之后,我们需要在应用程序所在的 HTML 网页中添加一个画布(canvas):
#[cfg(target_arch = "wasm32")]
{
use winit::dpi::PhysicalSize;
let _ = window.request_inner_size(PhysicalSize::new(450, 400));
use winit::platform::web::WindowExtWebSys;
web_sys::window()
.and_then(|win| win.document())
.and_then(|doc| {
let dst = doc.get_element_by_id("wasm-example")?;
let canvas = web_sys::Element::from(window.canvas());
dst.append_child(&canvas).ok()?;
Some(())
})
.expect("无法将画布添加到网页上");
}
"wasm-example"
这个 ID 是针对我的项目(也就是本教程)的。你可以你在 HTML 中使用任何 ID 来代替,或者,你也可以直接将画布添加到 <body>
中,就像 wgpu 源码仓库中所做的那样,这部分最终由你决定。
上边这些就是我们现在需要的所有 web 专用代码。接下来要做的就是构建 Web Assembly 本身。
译者注:以下关于 wasm-pack
的内容来自原文。但是由于它和 WebGPU 接口都尚未稳定,暂时不推荐用它构建此教程中的项目。参考本教程和原作者的仓库,这里给出一个使用 cargo build
的简易构建过程,如有疏漏请 PR 指正。
- 如果要支持 WebGL,那么在
Cargo.toml
中加入以下描述来启用 cargo 的--features
参数,参考 wgpu 的运行指南:
[features]
default = []
webgl = ["wgpu/webgl"]
- 运行
cargo build --target wasm32-unknown-unknown --features webgl
。 - 安装
wasm-bindgen
并运行:
cargo install -f wasm-bindgen-cli --version 0.2.84
wasm-bindgen --no-typescript --out-dir {你的输出目录,例如 ./tutorial1_window_output} --web {wasm 所在的目录,例如 .\target\wasm32-unknown-unknown\release\tutorial1_window.wasm}
- 此时会得到一个包含 .wasm 和 .js 文件的文件夹,可以用下文的 html 引入该 .js 文件。或者,可参考此项目仓库下
docs/.vitepress/components/WasmExample.vue
中的实现。
Wasm Pack
你可以只用 wasm-bindgen 来构建一个 wgpu 应用程序,但我在这样做的时候遇到了一些问题。首先,你需要在电脑上安装 wasm-bindgen,并将其作为一个依赖项。作为依赖关系的版本需要与你安装的版本完全一致,否则构建将会失败。
为了克服这个缺点,并使阅读这篇教程的人更容易上手,我选择在组合中加入 wasm-pack。wasm-pack 可以为你安装正确的 wasm-bindgen 版本,而且它还支持为不同类型的 web 目标进行构建:浏览器、NodeJS 和 webpack 等打包工具。
使用 wasm-pack 前,你需要先安装。
完成安装后,就可以用它来构建我们的项目了。当你的项目是一个独立的包(crate)时,可以直接使用 wasm-pack build
。如果是工作区(workspace),就必须指定你要构建的包。想象一下包是一个名为 game
的目录,你就会使用:
wasm-pack build game
译者注:wasm-pack build
需要如之前所说的那样加入 [lib]
等来构建静态库。
一旦 wasm-pack 完成构建,在你的包目录下就会有一个 pkg
目录,运行 WASM 代码所需的所有 javascript 代码都在这里。然后在 javascript 中导入 WASM 模块:
const init = await import("./pkg/game.js");
init().then(() => console.log("WASM Loaded"));
这个网站使用了 VitePress,并且是在 Vue 组件中加载 WASM。如果想看看我是怎么做的,可以查看这里。
如果打算在一个普通的 HTML 网站中使用你的 WASM 模块,只需告诉 wasm-pack 以 web 为构建目标:
wasm-pack build --target web
然后就可以在一个 ES6 模块中运行 WASM 代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pong with WASM</title>
</head>
<body id="wasm-example">
<script type="module">
import init from "./pkg/pong.js";
init().then(() => {
console.log("WASM Loaded");
});
</script>
<style>
canvas {
background-color: black;
}
</style>
</body>
</html>
点击下面的按钮查看示例代码运行!