wgpu 與 iOS App 集成

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)入只需要操作一次:

lib.png

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

search.png

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

links.png

當(dāng) Xcode 版本 >= 13 且 iOS Deployment Target >= 12.0 時(shí),Other Linker Flags 欄的設(shè)置可以省略。

以上就是所有的關(guān)鍵代碼和步驟了,我寫了一個(gè)叫 wgpu-in-app 的示例程序,效果如下:

wgpu in iOS App

查看 wgpu-in-app 完整項(xiàng)目源碼!

更多 wgpu 學(xué)習(xí)教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • OC語言基礎(chǔ) 1.類與對象 類方法 OC的類方法只有2種:靜態(tài)方法和實(shí)例方法兩種 在OC中,只要方法聲明在@int...
    奇異果好補(bǔ)閱讀 4,528評論 0 11
  • 文章已發(fā)在快手大前端公眾號,歡迎關(guān)注,文章地址如下: A站 的 Swift 實(shí)踐 —— 上篇[https://mp...
    星光社的戴銘閱讀 3,305評論 4 35
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 30,271評論 8 265
  • 聲明:面試是對自我審視的一種過程,面試題和iOS程序員本身技術(shù)水平?jīng)]有對等關(guān)聯(lián),無論你能否全部答出,都不要對自己產(chǎn)...
    Kevin_wzx閱讀 2,360評論 1 2
  • 參考公眾號:WeMobileDev APP開發(fā)中,總會想要去盡可能的優(yōu)化項(xiàng)目,這是我們作為程序員最基本的追求之一。...
    senpaiLi閱讀 1,838評論 0 3

友情鏈接更多精彩內(nèi)容