今年 WebAssembly でつくった3つのアプリ

WebAssembly Advent Calendar 2019 の 11日目の記事です。

WebAssembly の登場で C/C++/Rust など JavaScript 以外の言語のエコシステムをウェブブラウザーに持ち込むことができるようになり嬉しいな〜ということで、どのくらい動くのかという検証もかねて、3つほどアプリをつくって動作させてみました。

Sapporo.CSS (SaCSS) vol 110 LT 資料(2020/01/25 追加)

Emscripten 編

C言語でかかれたゲーム機メガドライブのエミュレーター Genesis-Plus-GX に WebAssembly 用のインターフェースを追加し、Emscripten でコンパイルして動作させてみました。

かなり重めのサウンドコアエミュレーションを有効にしてコンパイルしているのですが、iOS Safari を含め非常に高速に動作しています。(ベアナックル2が最後までプレイできました 🙂

主に Firefox と iOS Safari で確認しています。 Firefox は7段キーボード世代の ThinkPad(いつのだ…)で動作させていますが 60 fps 維持できています。

ソースコードを以下で公開しています。cmake/make と webpack でビルドできるようになっています。

Genesis-Plus-GX WebAssembly porting

https://github.com/h1romas4/wasm-genplus

ROM を吸い出す環境がない方は、homebrew の ROM が動作するかもしれません。.wasm はコンパイル済みのバイナリをコミットしていますので node だけあれば遊べると思います。

ゲームパッドのアサインは手持ちの XBOX ONE 用になっていますので適当に修正ください。。ちなみに、iOS 13 から PS4/XBOX ONE コントローラーサポートが入りましたが、Safari の GamePad API からも接続できました。

Emscripten 環境で少し詰まったのは次のポイントでした。

Emscripten を webpack からモジュールとして import する方法:

リンカオプションで MODULARIZE=1 を指定。

add_compile_flags(LD
    "-s MODULARIZE=1"
)

JavaScirpt で import して module を取得

import wasm from './genplus.js';

wasm().then(function(module) {
    gens = module;
});

Wasm 側で malloc したメモリーポインタの取得する方法:

モジュールの module.HEAPU8.buffer など HEAP* ビューで取得。

wasm().then(function(module) {
    gens = module;
    // memory allocate
    gens._init();
    // load rom
    fetch(ROM_PATH).then(response => response.arrayBuffer())
    .then(bytes => {
        // create buffer from wasm
        romdata = new Uint8Array(gens.HEAPU8.buffer, gens._get_rom_buffer_ref(bytes.byteLength), bytes.byteLength);
        romdata.set(new Uint8Array(bytes));
        message("TOUCH HERE!");
        initialized = true;
    });
});

大きめの static のアロケートに失敗した場合:

リンカオプションで初期メモリーサイズを指定。

add_compile_flags(LD
    "-s ALLOW_MEMORY_GROWTH=1"
    "-s TOTAL_MEMORY=32MB"
)

Rust / wasm-pack 編 (1)

Rust/wasm-pack で最初につくったアプリです。

Wasm 側でアロケートしたメモリーを仮想 VRAM として、Rust で何も考えずにむちゃ描きしたらどれくらいの速度になるだろうということで試したものになります。

デモサイトから実際に動作するところが見れます。

https://github.com/h1romas4/wasm-canvas-bitblt

sin/cos で画像回転させながらラスタースクロール的な動きをさせていますが、思うままにプログラムをかいているため RGBA の 4Byte 転送を全画素で何度も回していたりします。

ちょっと興味があったのが、速くなるかなと Rust の unsafe のブロック転送を使い、

unsafe {
    ptr::copy_nonoverlapping(
        [color.0, color.1, color.2, 0xff].as_ptr(),
        self.vram.as_mut_ptr().offset(pos),
        4,
    );
}

のようにしてみたのですが、これは Wasm 的には単純なループで展開されてコンパイルされていました。これは今後 Bulk memory operations が入りコンパイラが対応することで改善するかもしれません。

WebAssembly/bulk-memory-operations

Some people have mentioned that memcpy and memmove functions are hot when profiling some WebAssembly benchmarks.

なお、このプログラムは前述の古い ThinkPad T420s では 45fps そこそこでしたが、iPhone X では余裕で 60fps でていました。速い。。

Rust / wasm-pack 編 (2)

最初の Emscripten メガドライブエミュレーターから、ゲーム機の音源部分(FM音源・PSG)を取り出しプログラムを C言語から Rust に移植したものです。

エミュレーターから音源 LSI に発行するコマンドを横取りして保存した、ゲームミュージックを楽しむ VGM という形式のファイルがありますが、それを再生するプレイヤーになっています。

YM2612/SN76489 VGM player by Rust

こちらもデモサイトから動作を見ることができます。自分がつくったサンプル VGM をひとつ入れています。しょぼいですがクリックで鳴ります。本当はもっとすごい楽曲が再生できます。。

.vgm を準備できる方はドラッグアンドドロップしてみてください。(なお全て WebAssembly で処理してますので、サーバーにファイルアップロードはされません。安心してお試しください)

ソースは以下から参照できます。

https://github.com/h1romas4/rust-synth-emulation

デバッグ手法:

プロジェクトを pure Rust 部分と、Wasm 部分に分けて構成しています。現在 WebAssemby のデバッグ環境はまだ整っていませんので、複雑な処理はネイティブで実行できる環境で行うと良さそうです。

Wasm のデバッグ環境については Chrome が DRAWF に対応しつつあるとのことで(まだステップ実行のみ)、今後整っていくのではないかと思います。

Improved WebAssembly debugging in Chrome DevTools

As a first step, DevTools now supports native source mapping using this information, so you can start debugging Wasm modules produced by any of these compilers without resorting to the disassembled format or having to use any custom scripts.

ライブラリの活用:

本プレイヤーアプリですが、.vgz と呼ばれる .gz 圧縮された .vgm ファイルの再生にも対応させています。

WebAssembly/Rust は stdlib でコンパイルできますので、pure Rust の Gzip, and Zlib ライブラリーである flate2 を dependencies に追加してコンパイルして、ファイル展開させてみたところ問題なく動作しました。

[dependencies]
flate2 = "1.0"

この辺は各言語のエコシステムを活用できる Wasm の強みだなと感じます。

Runtime Error: Index out of bounds.:

移植中 Rust のオブジェクトを JavaScript から new した際に、Index out of bounds. が発生してオブジェクトがつくられない事象が発生しました。ぱっと原因が分からなかったため、ソースを削る方向で試していくと、[0; 50000] ほどの配列の初期化の部分で発生していました。

Make stack size configurable

Currently the stack-size for local variables of the generated wasm code is preconfigured to be 1048576 bytes. It is easy to reach this limit,

どうやら stack-size の初期値が小さいということで、.cargo/config に次の記述をして回避しています。

[target.wasm32-unknown-unknown]
rustflags = [
  "-C", "link-args=-z stack-size=32000000",
]

WebAssembly 登場にてウェブブラウザーで好きな言語で、好きなプログラムを動かせるようになって嬉しいです。

今後も継続してウォッチしていきたいと思います。

関連記事

WebAssembly/Emscripten を使ってエミュレーターをブラウザで動かす

WebAssembly を使うとブラウザー上でいろいろな言語のエコシステムが使えて楽しいなと、最近 Rust/WebAssembly で遊んでいたのですが、ふと C もやってみようかなと hello world 的にメガドライブエミュレーター(Genesis-Plus-GX)を移植することに挑戦してみました。

実は Emscripten はかなり前に一度挑戦していたのですが、ブラウザーで動作させる際のビルド周りがなかなか大変で、あまり大きなものは動かすことができませんでした。

再挑戦ということで調査したところ、昨今はビルド周りも整備されていてなかなかいい感じに環境ができあがっているようです。

この記事のソースコードは github で公開しています。解説よりも、ソースを見ていただいたほうが早いかもしれません。 🙂

https://github.com/h1romas4/wasm-genplus

Genesis-Plus-GX WebAssembly porting (work in progress)

Firefox で動作を確認しています。

Emscripten + webpack ビルド

よい製作にはよいビルドということで、JavaScript 系は webpack を使ってビルドし Emscripten/wasm を読み込むようにしています。

当初は emcc-loader という webpack の extention を使って、webpack から emcc(Emscripten のコンパイラ)を呼び出す形にしていたのですが、現在メンテナンスされていないようで Emscripten 1.39.0 では emcc が出力する JavaScript のグルーコードがエラーとなりうまく wasm をロードすることができませんでした。

Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option 

どうやら emcc-loader が生成するコードが指定している環境変数指定が非推奨となったということのようです。emcc-loader の次の場所(ENVIRONMENT: 行)をコメントアウトすると動作させることができました。

/**
 * Builds a loader script.
 */
async buildLoaderScript(baseScriptContent : string, options : LoaderOption) {
    const config = {
        ENVIRONMENT: options.environment,
    };

動かせるようになったものの、emcc-loader はプログラムに修正がかかるとフルビルドになる動作となり、少し大きめのプロジェクトだと時間がかかるため、今回は emcc-loader は諦めて C の部分は通常の cmake / make でビルドするようにしています。

この場合で emcc で出力される wasm/JavaScript のグルーコードを webpack で読み込むときは、リンカーオプションを次のように指定しモジュール化してあげます。

add_compile_flags(LD
    "-s DEMANGLE_SUPPORT=1"
    "-s ALLOW_MEMORY_GROWTH=1"
    "-s MODULARIZE=1"
)

この上で自分の JavaScript から、emcc が出力したグルーコード JS を import して次のようにすると、 wasm が async でロードされ wasm モジュールが操作できるようになります。

import wasm from './genplus.js';

let gens;

wasm().then(function(module) {
    gens = module;
    gens._init();
    // ...
});

モジュールとなったグルーコードを wasm() などと受けて then 以下で取得します。

WebAssembly とインターフェースする C の関数は次のように定義し、JavaScript からはモジュールから _ 付の関数として呼び出しすることができます。

void EMSCRIPTEN_KEEPALIVE init(void)
{
    // ...
}

また、C側の make についてですが cmake、make ともに Emscripten のラッパーコマンドが準備されていて、これを経由して cmake / make することでコンパイルオプションなどを自動的に調整してくれるようです。

emcmake cmake ..
emmake make

github のほうにビルド手順を記載してあります。

メモリーの共有

WebAssembly 側で malloc したメモリーはモジュールの HEAP*.buffer ビュー経由で JavaScript から見ることができ、JS 側の TypedArray 経由でアクセスできます。

vram = new Uint8ClampedArray(gens.HEAPU8.buffer, gens._get_frame_buffer_ref(), CANVAS_WIDTH * CANVAS_HEIGHT * 4);
uint32_t *frame_buffer;

void EMSCRIPTEN_KEEPALIVE init(void)
{
    // ...
    frame_buffer = malloc(sizeof(uint32_t) * VIDEO_WIDTH * VIDEO_HEIGHT);
    // ...
}

uint32_t* EMSCRIPTEN_KEEPALIVE get_frame_buffer_ref(void) {
    return frame_buffer;
}

エミュレーターで作成した VRAM (uiint32_t) を JS 側で取得して canvas にそのまま描画しています。

ちなみに、この例では C 側は uint32_t ですが、canvas は RGBA を Uint8ClampedArray ビューで扱うためエンディアンで逆になってしまい色がおかしくなりました。。ややはまり。エミュレーター側で ABGR 順の VRAM をつくることで対応しています。

音声

ブラウザ WebAudio API 側は Float32Array の 2チャンネル分離で、エミュレーター側はサンプリングを S16LE で生成するため wasm 側で変換しています。

float_t convert_sample_i2f(int16_t i) {
    float_t f;
    if(i < 0) {
        f = ((float) i) / (float) 32768;
    } else {
        f = ((float) i) / (float) 32767;
    }
    if( f > 1 ) f = 1;
    if( f < -1 ) f = -1;
    return f;
}

また、生成したサンプリングを WebAudio API の createBufferSource で即発声させると、次の発声が途切れてしまうので web-audio-buffer-queue ライブラリを使わせてもらって、少しバッファリングしてから発声するようにしています。

残念ながら iOS Safari ではブラウザの発声規制にかかっているせいか音が鳴りません。一応、クリック後にコンテキストをつくるようにしてみたのですが…

WebAssembly/Rust で似たようなことをやった時は iOS でも発声していたので、要調査。

ソースコードデバッグ

C 側のソースですが、emcc がソースマップ出力に対応しているためブラウザ(Firefox で確認)でデバッグブレイクが可能です。ただし、変数の値などはみることはできないようです。

ソースマップを出力するためには emcc のコンパイルオプションで -g4 を指定し、–source-map-base オプションを指定します。

# source map option (but not working)
#    -g4
#    --source-map-base src/main/c

その上で、ソースコードがブラウザーから見えるように http の領域に配置します。

devServer: {
    inline: true,
    contentBase: [
        path.join(__dirname, '/docs'), // eslint-disable-line
        // for sourcemap - src/main/c
        path.join(__dirname, '/'), // eslint-disable-line
    ],

基本的にはこれで止まるのですが、残念ながら現在 –source-map-base オプションが wasm の場合はうまく効かず、出力がすべてフルパスになってしまうようです。(emcc-loader だといい感じに source-map がでていたので何か方法があるのかもしれません)

とりあえず、出力された source-map のフルパス部分を置換して http から見えるパスにしてあげれば動作します。

WebAssembly/Emscripten 上でプログラムを動作させると、メモリーアクセスに対して境界値チェックが働くパターンがあるようです。実は移植当初 ROM のおしり 0x800000 から SRAM がマッピングされているのに気が付かず(ネイティブだと動いてしまっていた)、wasm がダウンしてしまっていたのですが、ソースコードマッピングすることで場所を特定することができました。

C 側の軽いデバッグの場合は、EM_ASM_ マクロでブラウザーのコンソールに文字列を出力できます。

#include <emscripten/emscripten.h>

uint8_t *rom_buffer;

void console_log() {
    EM_ASM_({
        console.log('genplus_buffer0: ' + $0.toString(16));
    }, buffer[0x100]);
}

なお、wasm 側でエラーがコンソールに出力された場合は、このマクロの出力が消える場合があるようです。(Firefox にて)

Emscripten 移植のこつ

箇条書きにて。

  • C 側はネイティブでも動作を確認する環境をつくりつつ、Emscripten でコンパイルして動作させりるとすると切り分けが早いです。前述の境界値系のエラーが wasm ででた場合はソースをアタッチするといいと思います。
  • WebAssembly の場合、イベントや入出力系は全て JavaScript 側の役目になりますので、エミュレーター系の移植の場合は一貫して JS -> wasm の呼び出しパターンでプログラムを構成すると、処理の粒度的に分かりやすくなりそうです。
  • C のプログラムを動かすというよりも、ビルドや JS とのインターフェース周りを調査するのに時間がかかりました。逆説的には、そこがクリアできれば大抵のものが動かせそうです。

というわけで、WebAssembly はいろいろできて楽しいですね。エミュレーターのほうですがまだコントローラーをつないでないので、Gamepad API で接続してみたいと思います。 🙂

ついに iOS でエミュレーターが動かせる…!

関連

WSL + Alacritty で Powerline を使う

Windows 10 の WSL (Windows Subsystem for Linux) は Linux 環境が Windows 上で簡単に使えるため、UNIX 系の OS と親和性の高い開発系を持つウェブなどのプログラミングに大変便利です。

自分はこの WSL 環境に高速ターミナルエミュレーターである Alacritty から接続して使っていますが、日本語や Powerline を使う上でいくつか必要な設定がありますのでここに記載しておきます。(なお、残念ながら Windows 版ではまだ日本語のインライン入力はできません)

Alacritty の設定

Alacritty の設定ファイルは以下の位置にあります。(なお、AppData は隠しフォルダーです)

C:\Users\[ユーザ名]\AppData\Roaming\alacritty\alacritty.yml

Alacritty 起動時に cmd.exe ではなく WSL を直接起動する設定:

shell:
  program: /Windows/System32/wsl.exe

Windows の新しい API(conPTY / Windows 10 October 2018 update 以降)を有効にして、Powerline や vim でカーソル位置がずれないようにする:

enable_experimental_conpty_backend: true

(UPDATE 2020/1/7) Alacritty 0.4.1 からは ConPTY の使用がデフォルトで有効になりました。

On Windows, the ConPTY backend will now be used by default if available
The enable_experimental_conpty_backend config option has been replaced with winpty_backend

Powerline を使うため Ricty Diminished w/ Powerline patched フォントを設定する:

font:
  normal:
    family: Ricty Diminished Discord for Powerline

WSL の設定

WSL と WSL 2(現在 insider build)の Ubuntu 18.04 で有効です。

標準で ja ロケールが入っておらず文字化けするのでロケール追加:

$ sudo locale-gen ja_JP.UTF-8
$ sudo /usr/sbin/update-locale LANG=ja_JP.UTF-8
$ echo $LANG # 設定されていなければいったんターミナル再起動
ja_JP.UTF-8

ネットワークがプロキシ配下の場合は以下を実施:

# for curl
echo 'proxy = "http://example.com:8080"' > ~/.curlrc

# for wget
echo 'http_proxy=http://example.com:8080' >> ~/.wgetrc
echo 'https_proxy=http://example.com:8080' >> ~/.wgetrc

# for other (include nodejs/pip)
echo 'export HTTP_PROXY="http://example.com:8080"' >> ~/.bashrc
echo 'export HTTPS_PROXY_="${HTTP_PROXY}"'  >> ~/.bashrc

# for apt
sudo echo 'Acquire::http::proxy "http://example.com:8080";' >> /etc/apt/apt.conf
sudo echo 'Acquire::https::proxy "http://example.com:8080";' >> /etc/apt/apt.conf

# for jvm (optional)
# export JAVA_OPTS="-DproxyHost=example.com -DproxyPort=8080"

Powerline を導入(WSL 1 は速度が遅いので、素早く動作する Rust 製の powerline-rs を使っています):

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ sudo apt install pkg-config libssl-dev
$ cargo install powerline-rs

powerline-rs を .bashrc にてプロンプトに設定:

$ vi ~/.bashrc # 一番下に追加
prompt() {
    PS1="$(powerline-rs --shell bash $?)"
}
PROMPT_COMMAND=prompt

以上、あとは tmux などを入れてあげると便利なターミナル環境になると思います。

なお、powerline-rs は tmux のステータス行には対応していませんので、tmux も powerline 化した場合は pip3 から powerline-status も導入すると良さそうです。

WSL 1 では git リポジトリがあるディレクトリなどで powerline がちょっと遅くなりますが、powerline-rs ならほぼほぼ問題ない速度で動作するようです。WSL2 ではファイルシステムの速度が大幅に速くなっていますので、普通の Linux と遜色ない速度で動作しています。

本題とはあんまり関係ないですが、Poweline 設定導入後 VSCode から WSL に VSCode WSL Extention で接続すると次のようになります。 (VSCode のターミナルも powerline フォントを設定してあげます)

xterm.js すごい。

関連