D言語で書かれたゲームをWASM経由でAppleTVやXboxに移植する実験

9 min read読了の目安(約8300字

※ バイナリをそのまま流用することに重きを置いているので、 D言語処理系には一切触れない 。その辺は Torus Trooper自体の移植記 の方にバリバリ書かれているのでそちらを参照。

前回前々回 と、UnityというかEmscripten、つまり C++ で書かれたゲームを wasm2c 、 JavaScriptインタプリタ(Node.jsやDuktape)、自前のJavaScriptライブラリを使用して移植してきた。

原理的には 、WebAssemblyで実装されたものであればC++以外の言語で開発されたゲームも移植できるはずなので試してみた。

https://www.youtube.com/watch?v=y8mvFWyqR7o

https://twitter.com/dotmjt/status/1381914477587656710

結果、Torus TrooperのWeb版をほぼそのままAppleTVやXboxで動作させることができた。

emscripten2native

現在制作しているWebGL-Native改めemscripten2nativeは

  • アセットファイルのリスト
  • WebAssemblyをロードして実行するJavaScriptファイル
  • WebAssemblyバイナリ

の3点セットを渡すと、Win32、UWP(Xbox)、Mac、iOS、Androidの各アプリケーションパッケージを生成するCMakeスクリプト(とサポートライブラリ)として制作している。(完成度はまだ10%未満)

これが完成すると、WebGLで動作するWebAssemblyゲームを用意すれば、各プラットフォーム向けのパッケージを 単一バイナリ から生成できる。ただしemscripten2native自体は完全なWebブラウザではないので(当然)、Webブラウザ固有の機能(fetch とか)を使っていないのが前提となる。

Torus Trooper

http://www.asahi-net.or.jp/~cs8k-cyu/windows/tt.html

今回動かしてみるTorus TrooperはD言語で書かれたゲームで既にPWAアプリとしてWebAssemblyに移植されている(移植したのは別の方):

https://torustrooper.xyz/

https://twitter.com/abagames/status/1340262443109371909

ゲームも面白いし、(古いD言語から現在のD言語、WebAssemblyへの) 移植記 も面白いし最高だな。

Webブラウザ版は X(決定) C(溜め打ち) P(ポーズ) ESC(中断)キーと矢印キーで操作する。

とりあえず動かす

WebGL1や requestAnimationFrame のような機能は既にemscripten2nativeのランタイム側に実装済なので微調整で良い。

ただし、emscripten2nativeは名前の通りEmscriptenが出力したJavaScriptの実行を前提としているので、自前でWebAssembly ←→ Webブラウザ インターフェースを実装しているプロジェクトでは、専用のローダーがいちいち必要になってしまう。

VAO( OES_vertex_array_object 拡張 )は今まで実装をサボってきたがまぁ有った方が便利なので実装してみた。もっとも、単に下位のOpenGL ES2実装を呼んでるだけだけど。。

ゲームパッド操作に対応する

https://github.com/okuoku/emtestapp2/commit/d842e62555140dac7c2cf78c94cca49fc097b3b7

簡単なパッチを用意してゲームパッドでも操作できるようにした。これはemscripten2native自体がW3C的なGamepad APIをサポートしているので、JavaScriptから単にそれを呼べば良い。

emscripten2native側に汎用のキーボードエミュレーションを装備した方が良いのかもしれない。

遅い

Node.jsでは比較的常識的な速度で動くものの、JavaScript側をDuktape、WASM側を wasm2c によるAOTにすると超遅くなってしまった。(60fps出ない)

原因調査

Instrumentsで雑に確認してみたところ、 sincos のような数学関数がJavaScript側を呼出すことで実装されており、これに実行時間の大半を消費していた。

( w2c で始まる関数はwasm2cがWASMから生成したもので、スタックトレースの instub_Z_envZ_wasm_cosZ_ff 以降で、JavaScriptインタプリタのduktapeを呼出している。)

このデザインは意図的に取られているようだ。

I will also note that I didn’t want to implement my own math functions so those are redirected to the JS Math ones and it works for now.

V8のようなJavaScriptとWASMの両方を実行できる処理系では、JavaScript ←→ WASM間の呼出しはかなり最適化されている。しかし、emscripten2nativeでは、JavaScript処理系とWebAssembly処理系が完全に分離されていて一切の仮定を置けないため、V8のような処理系に比べて実装がかなり非効率になってしまう。

sincos でいちいちJavaScript側を呼ばないようにする

emscripten2nativeでは、JavaScript処理系とWASM処理系で共通の呼出し規約であるNCCC(Normalized C Calling Convention) を設計した上で使用している。

通常の WASM → JavaScript呼出しをする場合、JavaScript側の関数がNCCCなC関数になっているように見せ掛けているので、同じように、NCCCな sin とか cos を用意して、それをWASM側に見せれば良いことになる。

型の調査

まず、WASM側でどのような型を期待しているかを調べる必要がある。

WASMバイナリが期待しているシンボル名は、JavaScript側を見ると wasm_cos のようなシンボルであることがわかる。

    wasm_cos: Math.cos,
    wasm_sin: Math.sin,
    wasm_atan2: Math.atan2,
    wasm_sqrt: Math.sqrt,
    wasm_pow: Math.pow,
    wasm_fmodf: function(x, y) { return x % y; },

(スーパーどうでも良いけどJavaScriptの % って整数以外にも使えるのか...)

次に wasm_cos に期待される型を調べる。これにはWABTに収録されている wasm2wat ツールで逆アセンブルして確認できる。

  (type (;53;) (func (param f32) (result f32)))
...
  (import "env" "wasm_sqrt" (func $wasm_sqrt (type 53)))
  (import "env" "wasm_cos" (func $wasm_cos (type 53)))
  (import "env" "wasm_sin" (func $wasm_sin (type 53)))
...

よって型としては、 [f32] => [f32] 、つまり float 1つを入力して float 1つを出力する関数として実装すれば良いことがわかる。

ネイティブ関数に開く

これをNCCCなC言語関数として表現すると、

UTILLIB_API void
math_cos(const uint64_t* in, uint64_t* out){
    float x;
    float r;
    x = *(float *)in;
    r = cosf(x);
    *(float *)out = r;
}

のように書ける。直感的だな!(※ 実際には uint64_t* → float* のキャストが安全とは保証されない -- 型を取ってalignを手で管理しないと安全に書けない)

あとはJavaScript関数のポインタをWASM側に渡しているところをインターセプトして、今回用意したNCCC関数に置き換えてしまえば良い。

これで、常識的な速度で動作するようになった。

しかしここまで対策してみると、逆にいちいちJavaScript側呼んでも20fps出るのかと感心するな。。

かんそう

Torus Trooperは良いゲームだと再認識した。

... まぁ仮に仕事でやれって言われたらC++に移植するかな(だいなし)。。 原作の再現度に不満があるのと、線が細いとかGeometry warsばりにパーティクル出せとか言われそうだし。。 (例えば FezはC#からC++に移植されていたり とこの手の事例は割と存在する)

オーディオが心残り

良いゲームなんだけど、BGMやSFXが無いと魅力半減(個人の感想です)なゲームなのでオーディオを実装していないのはかなり心残り。。

WebAudioダメじゃんというのは前書いたので良いとして、

https://zenn.dev/okuoku/articles/13c39882596c92

ゲームで使うのに十分なサブセットを考えるのが割と大変な作業でなかなか進捗が出ない。。今やMSもAppleも3DオーディオをOS組込みで実装している時代なのでそれを活かしつつやりたいんだけど。。

Metal

OpenGLESはもうiOS的にはdeprecatedなのでMetalで動かしたい。一応emscripten2native側には、ANGLEを使用してMetalを使うオプションは既に入っていて MacではMetalでも動く

が、ANGLEのMetalサポートが割と新しめ(A9以降)のGPUをターゲットとしていて、手元のAppleTV(A8)では動作しなかった。

The MTLFeatureSet_iOS_GPUFamily3_v1 and MTLFeatureSet_OSX_GPUFamily1_v1 feature sets allow you to define a framework-side sampler comparison function for a MTLSamplerState object.

https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf

パイプラインキャッシュを諦めれば(OpenGL的なステートが変わるたびにパイプライン捨ててよければ)要らない機能ではあるけどまぁそういうわけにも行かないよな。。

WASM経由にして何がうれしいのか?

この手の哲学的な質問には弱いが、何かemscripten2nativeに強力な機能が付けばみんな使ってくれるんじゃないかと楽観的に考えている。

現状のロードマップ で最も強力だと思うのはTAS対応で、真剣にやれば割とウケるんではないかと考えている。ただし技術的な難度は割と高い。例えばオーディオどうすんのかといった問題があり、マルチスレッドにも良い解が存在しない。幸い、現状のWebAssemblyゲームはシングルスレッドなのでTAS向きではある。

仮にネイティブコードに絞ることにしたとしても、TASを実現するには周辺環境をかなり高度に抽象化する必要がある。それらを真面目にやって、かつ専用プラットフォームでないと動かない ...というよりは、Webブラウザでも動く可能性を残した方がユーザ(ゲーム開発者)としては多分うれしいんじゃないだろうか。

... 真面目な話としては ゲームエンジンとプラットフォームの分離 を真剣に考察するとどうなるのかというポイントだと考えている。現在はゲームエンジンが各プラットフォーム差異を吸収する方向で、ゲームエンジンが完全にone stopとなるようになっているが、もうちょっと良い分割があるような気がしている。ゲームエンジンのようなプラットフォームでソースコード界面の互換性によって実現されていることを、WebAssemblyを使うことでバイナリの形式でも実現できるのではないだろうか。

JavaScript ←→ WASM 間パフォーマンスの差異

ここの部分が絶対にWebブラウザ上のゲームに勝てないのは、ちょっとemscripten2nativeにとっては脅威だと感じている。

今回のように手動でひらけるものはどうでも良いとしても、通常のゲームで1フレームに何百回も処理系の境界を越えることは想定していないので実用性の面で障害になる可能性が高い。

逆に言えば、Webブラウザはこのポイントを強く推すことでWebブラウザとWebアプリの分離を防ぐことができるし、WebAssemblyに移植する際はこの点に注意することでemscripten2nativeのような構成での動作を改善できる。

もっとも、WebAssembly生成の最大手であるEmscripten自体が、V8のような高速なJavaScript + WebAssembly処理系を強く前提としているのでemscripten2nativeの実用性もそこに制約される危険性がある。

まぁWASIのように純粋なWebAssemblyでエコシステムを作る方向に行くというのも有りかもしれないけど、マーケティング的な要請上 Unity WebGL でオーサリングできるというのは非常に重要なので。。

純粋なJavaScriptゲーム?

https://aba.hatenablog.com/entry/2021/04/02/204732

実際crisp-game-libとか動かせないもんか一瞬検討したけど、audioタグとか割と真面目なDOMが必要だったりとかがあり、emscripten2nativeにその辺を真面目に実装するよりも互換ライブラリをC++か何かで用意する方が100倍早そうだったので諦めた。

EjectaとかBabylonNativeみたいのも方向性としてあんまり流行ってないし、

https://github.com/phoboslab/Ejecta

https://github.com/BabylonJS/BabylonNative

何かこの手のもの -- ゲームのネイティブ化が容易であることを宣伝するようなJavaScriptゲームライブラリがあんまりないのは、実装上厳しいとか有るんだろうか。