開發(fā)環(huán)境配置
假設你的電腦上已經(jīng)安裝了 Android Studio,從菜單欄打開 SDK 管理器(Tools > SDK Manager > Android SDK > SDK Tools),勾選以下 3 個選項后點擊 OK 按鈕確認:
- Android SDK Build-Tools
- Android SDK Command-line Tools
- NDK(Side by side)

然后,設置如下兩個系統(tǒng)環(huán)境變量:
export ANDROID_SDK_ROOT=$HOME/Library/Android/sdk
# 注意,此處需要替換為你電腦上安裝的 NDK 的版本號
export NDK_HOME=$ANDROID_SDK_ROOT/ndk/23.1.7779620
添加安卓構建目標支持
到目前為止,Android 模擬器和虛擬設備還不支持 Vulkan 圖形 API(僅支持 OpenGL ES),所以開發(fā)或調(diào)試 wgpu 程序在 Android 系統(tǒng)上的運行時,建議使用真機(各種云測平臺的云真機也行)。
如果需要支持模擬器運行,還得加上 x86_64-linux-android 和 i686-linux-android 這兩個構建目標的支持。需要注意的是,如果指定了 wgpu 項目使用 Vulkan 圖形后端(Instance::new(wgpu::Backends::VULKAN)),則在模擬內(nèi)運行時會崩潰:
rustup target add aarch64-linux-android armv7-linux-androideabi
自定義窗口對象
要實現(xiàn)一個 wgpu 里能使用的窗口對象,就必須實現(xiàn) raw-window-handle 中 raw_window_handle() raw_display_handle() 這兩個分別定義在 HasRawWindowHandle HasRawDisplayHandle trait 里的抽象接口。
實現(xiàn) raw_display_handle() 最為簡單, 只需要實例化一個空的 AndroidDisplayHandle 對象做為參數(shù)。查看 raw-window-handle 的源碼就會發(fā)現(xiàn),實現(xiàn) raw_window_handle() 抽象接口需要用到 AndroidNdkWindowHandle 對象,此對象有一個叫 a_native_window 的字段,用來指向安卓 App 的 ANativeWindow 實例。
下面我們來一步步實現(xiàn)它。
先給項目添加必要的依賴:
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.19"
# 星號表示不鎖定特定版本,在項目構建及運行時始終保持使用最新版本
ndk-sys = "*"
raw-window-handle = "0.5"
然后定義一個 NativeWindow 結構體,它只有一個叫 a_native_window 的字段:
struct NativeWindow {
a_native_window: *mut ndk_sys::ANativeWindow,
}
impl NativeWindow {
// env 和 surface 都是安卓端傳遞過來的參數(shù)
fn new(env: *mut JNIEnv, surface: jobject) -> Self {
let a_native_window = unsafe {
// 獲取與安卓端 surface 對象關聯(lián)的 ANativeWindow,以便能通過 Rust 與之交互。
// 此函數(shù)在返回 ANativeWindow 的同時會自動將其引用計數(shù) +1,以防止該對象在安卓端被意外釋放。
ndk_sys::ANativeWindow_fromSurface(env as *mut _, surface as *mut _)
};
Self { a_native_window }
}
}
最后給 NativeWindow 實現(xiàn) raw-window-handle 抽象接口:
unsafe impl HasRawWindowHandle for NativeWindow {
fn raw_window_handle(&self) -> RawWindowHandle {
let mut handle = AndroidNdkWindowHandle::empty();
handle.a_native_window = self.a_native_window as *mut _ as *mut c_void;
RawWindowHandle::AndroidNdk(handle)
}
}
unsafe impl HasRawDisplayHandle for NativeWindow {
fn raw_display_handle(&self) -> RawDisplayHandle {
RawDisplayHandle::Android(AndroidDisplayHandle::empty())
}
}
定義 FFI
Rust 有一個關鍵字 extern(kotlin 中定義 JNI 函數(shù)時也有一個對應的關鍵字叫 external, 我們接下來會用到),當需要與其他語言編寫的代碼進行交互時,用于創(chuàng)建和使用外部函數(shù)接口(FFI,F(xiàn)oreign Function Interface)。FFI 是一種編程語言定義函數(shù)的方式,可以讓不同的 ”外部“ 編程語言調(diào)用這些函數(shù)。
在 Rust 這一端,我們通過給公開函數(shù)添加 #[no_mangle] 屬性來允許安卓端調(diào)用此函數(shù):
#[no_mangle]
#[jni_fn("name.jinleili.wgpu.RustBridge")]
pub fn createWgpuCanvas(env: *mut JNIEnv, _: JClass, surface: jobject, idx: jint) -> jlong {
android_logger::init_once(Config::default().with_min_level(Level::Trace));
let canvas = WgpuCanvas::new(AppSurface::new(env as *mut _, surface), idx as i32);
info!("WgpuCanvas created!");
// 使用 Box 對 Rust 對象進行裝箱操作。
// 我們無法將 Rust 對象直接傳遞給外部語言,通過裝箱來傳遞此對象的裸指針
// into_raw 返回指針的同時,也將此對象的內(nèi)存管理權轉交給調(diào)用方
Box::into_raw(Box::new(canvas)) as jlong
}
#[no_mangle]
#[jni_fn("name.jinleili.wgpu.RustBridge")]
pub fn enterFrame(_env: *mut JNIEnv, _: JClass, obj: jlong) {
// 直接獲取到指針指代的 Rust 對象的可變借用
let obj = unsafe { &mut *(obj as *mut WgpuCanvas) };
obj.enter_frame();
}
#[no_mangle]屬性告訴 Rust 關閉函數(shù)名稱修改功能。如果不加這個屬性,Rust 編譯器就會修改函數(shù)名,這是現(xiàn)代編譯器為了解決唯?名稱解析引起的各種問題所引?的技術。如果函數(shù)名被修改了,外部編程語言就?法按原名稱調(diào)?,開發(fā)者也沒辦法知道修改后的函數(shù)名。
#[jni_fn("XXX")] 這個函數(shù)簽名屬性需要重點介紹一下,做過安卓 JNI 開發(fā)的都知道,JNI 函數(shù)的簽名是又臭又長,比如上面的 createWgpuCanvas 函數(shù),手寫符合 JNI 規(guī)范的函數(shù)簽名就會是 Java_name_jinleili_wgpu_RustBridge_createWgpuCanvas 這樣,難寫且難維護 #[jni_fn("name.jinleili.wgpu.RustBridge")] 這個屬性能自動幫我們生成兼容 JNI 的函數(shù)簽名,使正確編寫函數(shù)簽名變得更加容易。為此,我們需要 jni_fn 依賴項:
[target.'cfg(target_os = "android")'.dependencies]
jni_fn = "0.1"
# 其它依賴項
在安卓端,我們定義一個命名空間為 name.jinleili.wgpu 的 RustBridge 類來加載 Rust 程序,并使用 external 關鍵字標記好具體實現(xiàn)在 Rust 端的外部函數(shù)聲明:
package name.jinleili.wgpu
import android.view.Surface
class RustBridge {
init {
System.loadLibrary("wgpu_in_app")
}
external fun createWgpuCanvas(surface: Surface, idx: Int): Long
external fun enterFrame(rustObj: Long)
// ...
}
你可以使用任意符合安卓規(guī)范的命名空間,只需要記得讓 Rust 端
#[jni_fn("")]屬性里的字符串與安卓端代碼里的命名空間一致。
實現(xiàn) cargo so 子命令
實現(xiàn) so 子命令的目的是為了一勞永逸地解決 Rust 項目配置 Android NDK 鏈接的問題。如果你對如何給 wgpu 項目手動配置 NDK 感興趣,Mozilla 的這篇文章有詳細的步驟。 so 子命令的代碼非常簡單,而且我已經(jīng)將它發(fā)布到了 Rust 的包注冊網(wǎng)站 crates.io, 可以直接安裝使用:
let args = std::env::args();
match Subcommand::new(args, "so", |_, _| Ok(false)) {
Ok(cmd) => match cmd.cmd() {
"build" | "b" => {
let ndk = Ndk::from_env().unwrap();
let build_targets = if let Some(target) = cmd.target() {
vec![Target::from_rust_triple(target).ok().unwrap()]
} else {
vec![
Target::Arm64V8a,
Target::ArmV7a,
Target::X86,
Target::X86_64,
]
};
for target in build_targets {
let triple = target.rust_triple();
// setting ar, linker value
let mut cargo = cargo_ndk(&ndk, target, 24).unwrap();
cargo.arg("rustc");
if cmd.target().is_none() {
cargo.arg("--target").arg(triple);
}
cargo.args(cmd.args());
if ndk.build_tag() > 7272597 {
if !cmd.args().contains(&"--".to_owned()) {
cargo.arg("--");
}
let gcc_link_dir = cmd.target_dir().join("gcc-temp-extra-link-libraries");
let _ = std::fs::create_dir_all(&gcc_link_dir);
std::fs::write(gcc_link_dir.join("libgcc.a"), "INPUT(-lunwind)")
.expect("Failed to write");
cargo.arg("-L").arg(gcc_link_dir);
}
if !cargo.status().unwrap().success() {
println!("{:?}", NdkError::CmdFailed(cargo));
}
}
}
_ => {}
},
Err(_) => {}
};
編譯為 .so 庫文件
首先,安裝我們上面實現(xiàn) so 子命令:
# 從 crates.io 安裝
cargo install cargo-so
# 或者
# 也可以從源碼安裝
cargo install --path ./cargo-so
然后,使用 so 子命令來構建 wgpu 項目:
# 將 wgpu 程序構建為 Android .so 庫文件
cargo so b --lib --target aarch64-linux-android --release
cargo so b --lib --target armv7-linux-androideabi --release
# 將 .so 復制到安卓項目的 jniLibs/ 目錄下
cp target/aarch64-linux-android/release/libwgpu_in_app.so android/app/libs/arm64-v8a/libwgpu_in_app.so
cp target/armv7-linux-androideabi/release/libwgpu_in_app.so android/app/libs/armeabi-v7a/libwgpu_in_app.so
我們還可以上面的構建與復制命令放進一個 .sh 命令行文件內(nèi),之后編譯項目時只需要執(zhí)行此命令行文件就可以了:
sh ./android_lib_build.sh
自定義 WGPUSurfaceView
安卓視圖組件 SurfaceView 提供了一個可嵌入在視圖層級結構中的專用于繪制的視圖。它負責繪制表面(Surface)在屏幕上的正確位置,還控制著繪制表面的像素格式及分辨率大小。
SurfaceView 持有的繪制表面是獨立于 App 窗口的,可以在單獨的線程中進行繪制而不占用主線程資源。所以使用 SurfaceView 可以實現(xiàn)復雜而高效的渲染(比如,游戲、視頻播放、相機預覽等),且不會阻塞用戶交互(觸摸、鍵盤輸入等)的響應。
安卓系統(tǒng)上的繪制表面是縱深排序(Z-Ordered)的,它默認處在 App 窗口的后面, SurfaceView 通過在 App 窗口上面設置透明區(qū)域來展示處在后面的繪制表面。
如果想將繪制表面放置到窗口的最上層,可以通過 setZOrderOnTop() 函數(shù)來實現(xiàn):
mySurfaceView.setZOrderOnTop(true)
這里有必要多解釋一句:wgpu 里的
Surface對象雖然最終指向的就是 SurfaceView 持有的繪制表面,但它是一個經(jīng)過統(tǒng)一封裝的結構體,所以并不是同一個對象:pub struct Surface { pub(crate) presentation: Option<Presentation>, #[cfg(vulkan)] pub vulkan: Option<HalSurface<hal::api::Vulkan>>, #[cfg(metal)] pub metal: Option<HalSurface<hal::api::Metal>>, #[cfg(dx12)] pub dx12: Option<HalSurface<hal::api::Dx12>>, #[cfg(dx11)] pub dx11: Option<HalSurface<hal::api::Dx11>>, #[cfg(gl)] pub gl: Option<HalSurface<hal::api::Gles>>, }
窗口的視圖層級結構決定了與繪制表面的正確合成,也就是說,繪制表面的展示會受到視圖層級關系的影響,在 SurfaceView 所處層級之上的視圖會覆蓋(遮擋)在合成后的繪制表面之上。
需要注意的是,如果覆蓋內(nèi)容存在透明度,則每次繪制表面渲染完成后,都會進行一次完整的 alpha 混合合成,這會對性能產(chǎn)生不利影響。
我們只能通過 SurfaceHolder 接口來訪問繪制表面。當 SurfaceView 在窗口中可見時,繪制表面就會被創(chuàng)建,而不可見時(比如,App 被切換到后臺運行)繪制表面會被銷毀,所以需要實現(xiàn) SurfaceHolder 的回調(diào)接口 surfaceCreated 及 surfaceDestroyed 來發(fā)現(xiàn)繪制表面的創(chuàng)建和銷毀。
下邊的代碼實現(xiàn)了一個繼承自 SurfaceView 的 WGPUSurfaceView:
// 為當前類實現(xiàn) SurfaceHolder 的回調(diào)接口
class WGPUSurfaceView : SurfaceView, SurfaceHolder.Callback2 {
private var rustBrige = RustBridge()
// Rust 對象的指針
private var wgpuObj: Long = Long.MAX_VALUE
private var idx: Int = 0
//...
init {
// 將當前類設置為 SurfaceHolder 的回調(diào)接口代理
holder.addCallback(this)
}
// 繪制表面被創(chuàng)建后,創(chuàng)建/重新創(chuàng)建 wgpu 對象
override fun surfaceCreated(holder: SurfaceHolder) {
holder.let { h ->
wgpuObj = rustBrige.createWgpuCanvas(h.surface, this.idx)
// SurfaceView 默認不會自動開始繪制,setWillNotDraw(false) 用于通知 App 已經(jīng)準備好開始繪制了。
setWillNotDraw(false)
}
}
// 繪制表面被銷毀后,也銷毀 wgpu 對象
override fun surfaceDestroyed(holder: SurfaceHolder) {
if (wgpuObj != Long.MAX_VALUE) {
rustBrige.dropWgpuCanvas(wgpuObj)
wgpuObj = Long.MAX_VALUE
}
}
override fun draw(canvas: Canvas?) {
super.draw(canvas)
// 考慮到邊界情況,這個條件判斷不能省略
if (wgpuObj == Long.MAX_VALUE) {
return
}
rustBrige.enterFrame(wgpuObj)
// invalidate() 函數(shù)通知通知 App,在下一個 UI 刷新周期重新調(diào)用 draw() 函數(shù)
invalidate()
}
}
App 中加載 WGPUSurfaceView
現(xiàn)在可以在 Activity 或 Fragment(此處僅指安卓 Fragment,與著色器里的片元無關)里加載 WGPUSurfaceView 實例了,通過 XML 或者 Java/Kotlin 代碼來加載很常見,下面我們來看看在安卓上的新一代 UI 開發(fā)框架 Jetpack Compose 中如何加載:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = colorResource(id = R.color.white)
) {
SurfaceCard()
}
}
}
}
}
@Composable
fun SurfaceCard() {
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
Column(modifier = Modifier.fillMaxSize()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(text = "wgpu on Android", fontSize = 20.sp, fontWeight = FontWeight.Bold)
}
// ...
// 通過 AndroidView 容器來加載我們的 WGPUSurfaceView
AndroidView(
factory = { ctx ->
WGPUSurfaceView(context = ctx)
},
modifier = Modifier
.fillMaxWidth()
.height(screenWidth),
)
}
}
基于以上代碼,我寫了一個叫 wgpu-in-app 的示例程序,運行效果如下:
