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 で動作を確認しています。

Emscriten + webpack ビルド

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

当初は emcc-loader という webpack の extention を使って、webpack から emcc(Emascriten のコンパイラ)を呼び出す形にしていたのですが、現在メンテナンスされていないようで Emascriten 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/Emascripten 上でプログラムを動作させると、メモリーアクセスに対して境界値チェックが働くパターンがあるようです。実は移植当初 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 にて)

Emscriten 移植のこつ

箇条書きにて。

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

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

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

関連

Rust/WebAssembly でレトロシンセをエミュレートする

以前から WebAssembly を使ってレトロシンセ音源をエミュレートしてブラウザーで発声させてみたいと思っていたのですが、Rust が WebAssembly に直接コンパイルできるようになったのをきっかけに挑戦し、なんとか動かすことができました。

以下からデモを見ることができます。 🙂

WebAssembly 非対応の IE を除く、PC とモバイルのほとんどのブラウザーで動作すると思います。(なお、iOS 11 Safari と Android Chrome はサンプリングレートを無視してしまう処理があるようで高め・速めで再生されています。iOS 12 Safari では修正されたようです。)

https://another.maple4ever.net/wp-content/uploads/etc/synth/

ソースコードも github にコミットしました。

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

PSG(SN76489) VGM player by Rust

さて、初めての Rust だったこともあり製作に結構時間がかかりましたので顛末でも…。

何はなくとも Rust 言語を覚えなければということで、ちょうどオライリーから「プログラミング言語 Rust」が発売されたので購入。

読み進めるも若干飛ばし気味に進む展開に、まだ早かった…と先に公式ドキュメント版プログラミング言語 Rust を読みました。

プログラミング言語Rust: 2nd Edition”の日本語版PDFを作成した

プログラミング言語Rust: 2nd Editionの日本語版PDFを公開しました!

550ページ以上の素晴らしい翻訳と組版で本当に感謝しかありません…。2週間ほどかかりましたが最後まで通して読むことができました(オライリーは少しできるようになってから読んだほうがいいかもしれません)

合わせて、海外のハッカーさんが Rust でライブコーディングしている youtube 動画を見ながらプログラムの組み立て方などを覚えています。こちらも非常に参考になりました。

そんなこんなで半月ほどかけて、C 言語でかかれた SN76498(PSG)エミュレーターを Rust に移植し、PCM サンプリングファイルを出力させることに成功。 🙂

このプログラムを元に WebAssembly 化していきました。

Rust 側での状態の保持

WebAssembly は JavaScript と WebAssembly 間で関数を公開し、互いに呼び出すことができますが、WebAssembly(Rust)側で状態を保持したいことがあります。

今回のプログラムの構成は JavaScript 側で AudioContext イベントを回し、発声バッファが必要になったタイミングで Rust 側で PSG をエミュレーションし 2048 サンプルごとに渡すようなロジックになっていますが、Rust 側では楽曲のどこまで再生したかなどなどを覚えている必要があります。

保持したいデーターをつめた Rust 側の構造体は次のようにしました。

struct VgmPlay { sn76489: SN76489, vgmpos: usize, remain_frame_size: usize, vgmend: bool, buffer: [f32; MAX_BUFFRE_SIZE], vgmdata: [u8; 65536] }

C言語であれば static にしておけば OK ですが、Rust の static は実行前に大きさが決まっていないとコンパイルエラーとなるため、lazy_static! マクロを用いて Mutex 内にこの構造体を保持しています。

lazy_static! { static ref DATA: Mutex&amp;amp;amp;amp;amp;lt;VgmPlay&amp;amp;amp;amp;amp;gt; = Mutex::new(VgmPlay::from()); } 

JavaScript に公開する関数では Mutex をロックした上で中の構造体を取得し、構造体に impl した関数を呼び出します。

#[no_mangle] pub unsafe extern &amp;quot;C&amp;quot; fn play() -&amp;amp;amp;amp;amp;gt; f32 { let vgmplay = &amp;amp;amp;amp;amp;amp;mut DATA.lock().unwrap(); vgmplay.play() as f32 }

ブラウザに実装された JavaScript はシングルスレッドであるため、関数に再入がかかったり同じ構造体を使う別な関数が呼び出されることはありませんが、Mutex につめておくと安心ですね。 このあたりは次の記事が大変参考になりました。

Rocket – A Rust game running on WASM Technically, this isn’t necessary in the case of Javascript, since there will only be one thread. Still, the type system knows nothing about that… Hence the mutex.

感謝。

メモリーの共有

JavaScript/WebAssembly 間でメモリーのポインタを共有をすることができます。今回はサンプリングバッファを割当て、Rust 側で PSG をレンダリングしたメモリーをそのまま JavaScript からアクセスして AudioContext に書き込むことで発声させています。

Rust 側でメモリーの位置を返却。

#[no_mangle] pub unsafe extern &amp;quot;C&amp;quot; fn get_audio_buffer() -&amp;amp;amp;amp;amp;gt; *const f32 { let vgmplay = &amp;amp;amp;amp;amp;amp;mut DATA.lock().unwrap(); &amp;amp;amp;amp;amp;amp;amp;(vgmplay.buffer[0]) }

JavaScript 側で ArrayBuffer としてアクセス。

wasm_audio_buffer = new Float32Array( wasm_memory.buffer, wasm_exports.get_audio_buffer(), SAMPLE_LENGTH); // ... ev.outputBuffer.getChannelData(0).set(wasm_audio_buffer);

また、楽曲データーである .vgm ファイルを http して Rust のメモリーに格納することもしています。

Rust で上記のサンプリングバッファ同様 vgm_data のポインタを関数で公開した上で JavaScript 側から fetch した値を Uint8Array として set。

wasm_vgm_data = new Uint8Array( wasm_memory.buffer, wasm_exports.get_vgm_data(), MAX_VGM_DATA); // load vgm data fetch('./vgm/vgmsample.vgm') .then(response =&amp;amp;amp;amp;amp;gt; response.arrayBuffer()) .then(bytes =&amp;amp;amp;amp;amp;gt; wasm_vgm_data.set(new Uint8Array(bytes))) .then(results =&amp;amp;amp;amp;amp;gt; { // ... });

JavaScript/WebAssembly でメモリーが共有できるため、余分なコピーが発生せず高速に処理することができました。今回は実装していませんが、Rust 側でサンプリングをフーリエ変換してフレームバッファメモリーにビジュアライズして書き込み、canvas に転送なんてこともできると思います。(Web Audio API にも FFT がありますが :D)

Web Audio API

WebAssembly だけの話ってわけでもないのですが、ブラウザの Web Audio API の使い方にちょっと困りました。

Rust 側としてはサンプリングを全て再生したタイミングで次のサンプリングを送り込みたいのですがそれを Rust 側で知るすべがなかったため、JavaScript 側の AudioContext の onaudioprocess イベントを使い(バッファを吐ききると発動する) Rust 側からサンプリングを渡す方式としています。

残念ながら現在 onaudioprocess イベントは JavaScript のメインスレッドを使いきる可能性があるということで非推奨となっており、Audio Workers を使えということのようです。

Audio Workers Audio workers を利用すると web worker のコンテキストで音声処理をおこなえます。Audio Workers は比較的新しいいくつかのインタフェース (2014 年 8 月 29 日に定義)によって定義されているため、これを実装したブラウザはまだありません。実装が完了すると、 ScriptProcessorNode, と JavaScript による音声処理 で述べた機能を置き換えることとなります。

が、まだ実装が進んでいないようです…。 とりあえず今のところ WebAssembly では、JavaScript のタイマーやイベントを契機に処理するという流れが良いかと感じました。

(追記:Audio Worker は仕様から消えて AudioWorklet になったようです)

というわけで、なんとなく WebAssembly でのプログラムの形がつかめてきました。 Rust も少しだけ分かってきましたのでまた何かつくってみようと思います。

WebAssembly はブラウザベンダー4社でつくってるだけあり、互換性もよく(今回 WebAssembly 的には何のトラブルもなく全てのブラウザで動作しました)、次のステップでは GC やスレッドも入ってくるということなので楽しみな技術です。

ついにブラウザーで好きなプログラムを動作させることができるようになって嬉しいす。 😀