wgpu 與 iOS App 集成相比于 Android 要簡單一些。
添加 iOS 構(gòu)建目標(biāo)支持
# 添加 iOS 構(gòu)建目標(biāo)支持
rustup target add aarch64-apple-ios
# 添加 iOS 模擬器構(gòu)建目標(biāo)支持
# Intel CPU Mac
rustup target add x86_64-apple-ios
# M1+ Mac
rustup target add aarch64-apple-ios-sim
由于從 A7 芯片(iPhone 5S,iPad Mini 2) 開始,iPhone iPad 都是 64 位的設(shè)備,所以我們不需要 armv7s-apple-ios、 armv7-apple-ios 這兩個(gè)構(gòu)建目標(biāo)。
iOS 模擬器相比于真機(jī)設(shè)備的特殊之處
當(dāng)運(yùn)行 WebGPU 程序時(shí),模擬器并不會試圖完全模擬你正在模擬的 iOS 設(shè)備的 GPU。例如,如果選擇 iPhone 14 Pro 模擬器,它不會試圖模擬 A16 GPU 的能力。相反,模擬器會翻譯你的任何調(diào)用,并將它們引導(dǎo)到 Mac 主機(jī)上的選定 GPU。
蘋果為模擬器單獨(dú)提供了一個(gè)設(shè)備對象,其功能被限制為蘋果 GPU 家族的 Apple2 型號(也就是古早的 A8 芯片),這意味著模擬器往往比實(shí)際的 GPU 支持更少的功能或更多的限制。從這篇文檔 可以查看到功能限制的詳情。
開發(fā)調(diào)試 GPU 應(yīng)用,使用真機(jī)永遠(yuǎn)是最好的選擇。
定義 FFI
在 iOS/macOS 上,使用 CAMetalLayer 也能創(chuàng)建繪制表面的實(shí)例,所以我們無須去實(shí)現(xiàn) raw-window-handle 抽象接口。
先給項(xiàng)目添加上必要的依賴:
[target.'cfg(target_os = "ios")'.dependencies]
libc = "*"
objc = "0.2.7"
然后定義一個(gè) IOSViewObj 結(jié)構(gòu)體:
#[repr(C)]
pub struct IOSViewObj {
// metal_layer 所在的 UIView 容器
// UIView 有一系列方便的函數(shù)可供我們在 Rust 端來調(diào)用
pub view: *mut Object,
// 指向 iOS 端 CAMetalLayer 的指針
pub metal_layer: *mut c_void,
// 不同的 iOS 設(shè)備支持不同的屏幕刷新率,有時(shí)我們的 GPU 程序需要用到這類信息
pub maximum_frames: i32,
// 外部函數(shù)接口,用于給 iOS 端傳遞狀態(tài)碼
pub callback_to_swift: extern "C" fn(arg: i32),
}
#[repr(C)] 屬性標(biāo)注 IOSViewObj 的內(nèi)存布局兼容 C-ABI。
什么是 ABI?
ABI 是?個(gè)規(guī)范,它涵蓋以下內(nèi)容:
· 調(diào)?約定。?個(gè)函數(shù)的調(diào)?過程本質(zhì)就是參數(shù)、函數(shù)、返回值如何傳遞。編譯器按照調(diào)?規(guī)則去編譯,把數(shù)據(jù)放到相應(yīng)的堆棧中,函數(shù)的調(diào)??和被調(diào)??(函數(shù)本?)都需要遵循這個(gè)統(tǒng)?的約定。
· 內(nèi)存布局。主要是??和對齊?式。
· 處理器指令集。
· ?標(biāo)?件和庫的?進(jìn)制格式。為什么使用 C-ABI?
不同的操作系統(tǒng)、編程語?、每種編程語?的不同編譯器 實(shí)現(xiàn)基本都有??規(guī)定或者遵循的 ABI 和調(diào)?規(guī)范。?前只能通過 FFI 技術(shù)遵循 C 語? ABI 才可以做到編程語?的相互調(diào)?。也就是說,C-ABI 是唯?通?的穩(wěn)定的標(biāo)準(zhǔn) ABI。這是由歷史原因決定的,C 語?伴隨著操作系 統(tǒng)?路發(fā)展?來,導(dǎo)致其成為事實(shí)上的標(biāo)準(zhǔn) ABI。
假設(shè)我們已經(jīng)實(shí)現(xiàn)好了一個(gè) wgpu 程序叫 WgpuCanvas, 現(xiàn)在來實(shí)現(xiàn)兩個(gè)供 iOS 端調(diào)用的、控制 WgpuCanvas 初始化及幀渲染的函數(shù):
#[no_mangle]
pub fn create_wgpu_canvas(ios_obj: IOSViewObj) -> *mut libc::c_void {
let obj = WgpuCanvas::new(AppSurface::new(ios_obj), 0_i32);
// 使用 Box 對 Rust 對象進(jìn)行裝箱操作。
// 我們無法將 Rust 對象直接傳遞給外部語言,通過裝箱來傳遞此對象的裸指針
let box_obj = Box::new(obj);
Box::into_raw(box_obj) as *mut libc::c_void
}
#[no_mangle]
pub fn enter_frame(obj: *mut libc::c_void) {
// 將指針轉(zhuǎn)換為其指代的實(shí)際 Rust 對象,同時(shí)也拿回此對象的內(nèi)存管理權(quán)
// from_raw 是 unsafe 函數(shù),它的調(diào)用需要放在 unsafe {} 塊中
let mut obj: Box<WgpuCanvas> = unsafe { Box::from_raw(obj as *mut _) };
obj.enter_frame();
// 將 obj 對象的內(nèi)存管理權(quán)重新轉(zhuǎn)交給調(diào)用方
Box::into_raw(obj);
}
#[no_mangle] 屬性告訴 Rust 關(guān)閉函數(shù)名稱修改功能。如果不加這個(gè)屬性,Rust 編譯器就會修改函數(shù)名,這是現(xiàn)代編譯器為了解決唯?名稱解析引起的各種問題所引?的技術(shù)。如果函數(shù)名被修改了,外部編程語言就?法按原名稱調(diào)?,開發(fā)者也沒辦法知道修改后的函數(shù)名。
你應(yīng)該已注意到了,上面的 enter_frame(obj: *mut libc::c_void) 函數(shù)里,我們做了兩次內(nèi)存管理權(quán)的轉(zhuǎn)移,先是取回了內(nèi)存管理權(quán),后又再次轉(zhuǎn)交給調(diào)用方。有沒有辦法避免這兩次轉(zhuǎn)移來提升性能呢?可以,直接從裸指針獲取到對象的可變借用:
#[no_mangle]
pub fn enter_frame(obj: *mut libc::c_void) {
// 直接獲取到指針指代的 Rust 對象的可變借用
let obj = unsafe { &mut *(obj as *mut WgpuCanvas) };
obj.enter_frame();
}
Unsafe Rust
Unsafe Rust 是 Safe Rust 的?個(gè)超集。也就是說,在
unsafe {}塊中,并不會禁? Safe Rust 中的任何安全檢查。它僅在進(jìn)?以下五類操作時(shí),不提供安全檢查:
- 裸指針的解引?或類型轉(zhuǎn)換;
- 調(diào)? unsafe 的函數(shù);
- 訪問或修改可變靜態(tài)變量;
- 實(shí)現(xiàn) unsafe trait;
- 讀寫 Union 聯(lián)合體中的字段;
&mut *(obj as *mut WgpuCanvas)之所以要放在unsafe {}塊中,不僅僅是由于obj參數(shù)是裸指針,還因?yàn)?Rust 在編譯階段的靜態(tài)安全檢查此時(shí)完全沒有?武之地,所以也就沒必要提供安全檢查了。
還需要寫一個(gè)簡單的 C 語言的頭文件來對應(yīng)上面定義的結(jié)構(gòu)體與函數(shù)。
讓我們按照慣例,使用項(xiàng)目編譯出來的 .a 庫文件名稱為此頭文件命名:
#ifndef libwgpu_in_app_h
#define libwgpu_in_app_h
#include <stdint.h>
// 這個(gè)不透明結(jié)構(gòu)體用來指代 Rust 端的 WgpuCanvas 對象
struct wgpu_canvas;
// 對應(yīng) Rust 端的 IOSViewObj 對象
struct ios_view_obj {
void *view;
// CAMetalLayer
void *metal_layer;
int maximum_frames;
void (*callback_to_swift)(int32_t arg);
};
struct wgpu_canvas *create_wgpu_canvas(struct ios_view_obj object);
void enter_frame(struct wgpu_canvas *data);
#endif /* libwgpu_in_app_h */
將上面的頭文件放置到 iOS 項(xiàng)目中。如果你的 iOS 項(xiàng)目是使用 Swift 創(chuàng)建的,則還需要將頭文件引入到橋接文件(XXX-Bridging-Header.h)中:
#ifndef wgpu_iOS_Bridging_Header_h
#define wgpu_iOS_Bridging_Header_h
#import "libwgpu_in_app.h"
#endif /* wgpu_iOS_Bridging_Header_h */
App 中加載 WgpuCanvas 對象
先在 iOS 項(xiàng)目中自定義一個(gè)繼承自 UIView 的 MetalView,代碼很簡單:
class MetalView: UIView {
// 這里將 View 的默認(rèn) Layer 指定為 CAMetalLayer
override class var layerClass: AnyClass {
return CAMetalLayer.self
}
override func awakeFromNib() {
super.awakeFromNib()
configLayer()
}
private func configLayer() {
guard let layer = self.layer as? CAMetalLayer else {
return
}
layer.presentsWithTransaction = false
layer.framebufferOnly = true
// nativeScale is real physical pixel scale
// https://tomisacat.xyz/tech/2017/06/17/scale-nativescale-contentsscale.html
self.contentScaleFactor = UIScreen.main.nativeScale
}
}
然后在 ViewController 中實(shí)例化 WgpuCanvas:
// ...
// 我是通過 StoryBoard 綁定的 MetalView,當(dāng)然,你也可以手動創(chuàng)建
@IBOutlet var metalV: MetalView!
// 指向 Rust 端 WgpuCanvas 的指針
var wgpuCanvas: OpaquePointer?
lazy var displayLink: CADisplayLink = {
CADisplayLink.init(target: self, selector: #selector(enterFrame))
}()
// ...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 我們需要保證 WgpuCanvas 只被實(shí)例化一次
if wgpuCanvas == nil {
// 將 Swift 對象轉(zhuǎn)換為裸指針
let viewPointer = Unmanaged.passRetained(self.metalV).toOpaque()
let metalLayer = Unmanaged.passRetained(self.metalV.layer).toOpaque()
let maximumFrames = UIScreen.main.maximumFramesPerSecond
// 創(chuàng)建 IOSViewObj 實(shí)例
let viewObj = ios_view_obj(view: viewPointer, metal_layer: metalLayer,maximum_frames: Int32(maximumFrames), callback_to_swift: callback_to_swift)
// 創(chuàng)建 WgpuCanvas 實(shí)例
wgpuCanvas = create_wgpu_canvas(viewObj)
}
self.displayLink.isPaused = false
}
@objc func enterFrame() {
guard let canvas = self.wgpuCanvas else {
return
}
// 執(zhí)行 WgpuCanvas 幀渲染
enter_frame(canvas)
}
func callback_to_swift(arg: Int32) {
// callback_to_swift 函數(shù)是在 WgpuCanvas 中被調(diào)用的,WgpuCanvas 的代碼很可能沒有運(yùn)行在 iOS 的 UI 線程,
// 如果此處涉及到 UI 操作,就必須切換到 UI 線程。
DispatchQueue.main.async {
switch arg {
// ...
}
}
}
編譯與運(yùn)行
# 編譯為 iOS 真機(jī)支持的庫
# debug 庫
cargo build --target aarch64-apple-ios
# release 庫
cargo build --target aarch64-apple-ios --release
# 編譯為 iOS 模擬器支持的庫
# M1+ Mac 上執(zhí)行:
cargo build --target aarch64-apple-ios-sim
# Intel 芯片的 Mac 上執(zhí)行:
cargo build --target x86_64-apple-ios
打開 iOS 項(xiàng)目,在項(xiàng)目的 General 選項(xiàng)卡下找到 Frameworks, Libraries, and Embedded Content 欄, 導(dǎo)入系統(tǒng)的 livresolv.tbd 及我們剛編譯的 .a 庫,此導(dǎo)入只需要操作一次:

然后在 Build Settings 選項(xiàng)卡下找到 Search Paths -> Library Search Paths 欄, 將 .a 庫的 debug 和 release 路徑填到對應(yīng)的字段中:

最后,還是在 Build Settings 選項(xiàng)卡下,找到 Linking -> Other Linker Flags 欄,添加 -ObjC、-lc++ 兩個(gè)鏈接標(biāo)記:

當(dāng) Xcode 版本 >= 13 且
iOS Deployment Target>= 12.0 時(shí),Other Linker Flags欄的設(shè)置可以省略。
以上就是所有的關(guān)鍵代碼和步驟了,我寫了一個(gè)叫 wgpu-in-app 的示例程序,效果如下:
