D言語で書かれたゲームをWASM経由でAppleTVやXboxに移植する実験
前回 、 前々回 と、UnityというかEmscripten、つまり C++ で書かれたゲームを wasm2c
、 JavaScriptインタプリタ(Node.jsやDuktape)、自前のJavaScriptライブラリを使用して移植してきた。
原理的には 、WebAssemblyで実装されたものであればC++以外の言語で開発されたゲームも移植できるはずなので試してみた。
結果、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
今回動かしてみるTorus TrooperはD言語で書かれたゲームで既にPWAアプリとしてWebAssemblyに移植されている(移植したのは別の方):
ゲームも面白いし、(古いD言語から現在のD言語、WebAssemblyへの) 移植記 も面白いし最高だな。
Webブラウザ版は X(決定) C(溜め打ち) P(ポーズ) ESC(中断)キーと矢印キーで操作する。
とりあえず動かす
WebGL1や requestAnimationFrame
のような機能は既にemscripten2nativeのランタイム側に実装済なので微調整で良い。
- 専用のローダ(JavaScript側): https://github.com/okuoku/em2native-proto/commit/de283d4b67e8b77926c1459ec0d7de350bb07c35
- WebGLのVAO拡張のサポート
ただし、emscripten2nativeは名前の通りEmscriptenが出力したJavaScriptの実行を前提としているので、自前でWebAssembly ←→ Webブラウザ インターフェースを実装しているプロジェクトでは、専用のローダーがいちいち必要になってしまう。
VAO( OES_vertex_array_object
拡張 )は今まで実装をサボってきたがまぁ有った方が便利なので実装してみた。もっとも、単に下位のOpenGL ES2実装を呼んでるだけだけど。。
ゲームパッド操作に対応する
簡単なパッチを用意してゲームパッドでも操作できるようにした。これはemscripten2native自体がW3C的なGamepad APIをサポートしているので、JavaScriptから単にそれを呼べば良い。
emscripten2native側に汎用のキーボードエミュレーションを装備した方が良いのかもしれない。
遅い
Node.jsでは比較的常識的な速度で動くものの、JavaScript側をDuktape、WASM側を wasm2c
によるAOTにすると超遅くなってしまった。(60fps出ない)
原因調査
Instrumentsで雑に確認してみたところ、 sin
や cos
のような数学関数が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のような処理系に比べて実装がかなり非効率になってしまう。
sin
や cos
でいちいち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ダメじゃんというのは前書いたので良いとして、
ゲームで使うのに十分なサブセットを考えるのが割と大変な作業でなかなか進捗が出ない。。今や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
andMTLFeatureSet_OSX_GPUFamily1_v1
feature sets allow you to define a framework-side sampler comparison function for a MTLSamplerState object.
パイプラインキャッシュを諦めれば(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ゲーム?
実際crisp-game-libとか動かせないもんか一瞬検討したけど、audioタグとか割と真面目なDOMが必要だったりとかがあり、emscripten2nativeにその辺を真面目に実装するよりも互換ライブラリをC++か何かで用意する方が100倍早そうだったので諦めた。
EjectaとかBabylonNativeみたいのも方向性としてあんまり流行ってないし、
何かこの手のもの -- ゲームのネイティブ化が容易であることを宣伝するようなJavaScriptゲームライブラリがあんまりないのは、実装上厳しいとか有るんだろうか。
Discussion