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 でエミュレーターが動かせる…!

関連

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

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 すごい。

Maixduino K210/RISC-V マイコンでメガドライブエミュレーターを動作させる

twitter を眺めていましたらスイッチサイエンスさんから、かねてより興味があった RISC-V SoC のマイコンボードが発売されていましたので買ってみました。 LCD 付きで 4000円くらいなり。 🙂

CPU クロック 400MHz(600MHz)・メモリー 6MB(8MB) とかなり高性能なマイコンですので、ESP32 では諦めていたメガドライブのエミュレーターを動作させることに hello world がてら挑戦し、まずは起動できました。やった〜。

まだ起動するだけのものですがソースコードを github にコミットしています。

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

Genesis-Plus-GX をエミュレーターのコアとして使わせてもらい、YM2612/SN76489 もエミュレーションされていますので、I2S にサンプリングバッファを流せば音も発声すると思います。

ちょっと原因不明ですが、まだ特定の条件でプログラムがダウンしてしまうことがあるようです。要調査。 アライメント関係の不具合で修正することができました。

また ROM のサイズは 128K 〜 640K バイトのものであれば最初の malloc には成功して起動します。大きなものはスタックオーバーフローするかもです。1Mバイト(ソニック2 などの 8M ROM) 以上は残念ながらメモリー不足で初期の malloc で落ちてしまいます。

さて、この記事では Maixduino 及び K210/RISC-V の開発環境の構成と、製作中に気がついた事をメモがてらまとめてみます。

開発環境の構成

Maixduino は SoC として K210 を使った開発ボードです。 Maixduino が提供する開発環境及びライブラリーは Arduino コアとして提供されています。

インストールガイドとソースコード

PlatformIO で Sipeed MAIXDUINO の設定がありますので、おそらく導入はこれを使うのが一番簡単です。

Maixduino Arduino コアは、K210 SoC 提供元の Kendryte が提供する K210 SDK とツールチェインに依存しています。K210 の SDK は OS なしの standalone 版と FreeRTOS 版があります。 Arduino コアでは standalone 版を使っています。

Maixduino Arduino コアのドキュメントはありますが現在は書きかけっぽいです。ライブラリの example のソースコードを参照するのがよいと思います。

この記事のメガドライブのエミュレータープロジェクトでは、勉強がてら Maixduino の Arduino コアを使わずに直接 kendyte-standalone-sdk を使ってプログラミングしました。

SDK のサンプルが以下から参照できます。

また K210 プログラミングガイドやペリフェラルなどの資料は次からダウンロードできます。

環境の構築はツールチェイン類を PC 上に配置して、

SDK をダウンロードしてきて、src ディレクトリの下に自分のプロジェクトを作成し、cmake して make すればOKです。

詳しい手順が上記のサイトの Usage にあります。

メガドライブエミュレータープロジェクトでは、ビルドに使う SDK のバージョンを固定したかったため、SDK をおなかに抱える構成としています。ビルド手順なども以下に記載していますのでご参考まで。 🙂

製作中に気がついた点

箇条書きにて。

  • Maixduino は K210 SDK Demo で使われているボードとペリフェラルの構成が異なり、LCD と SD カードはそれぞれ SPI0 と SPI1 に割り当てられている。また LCD コントローラーも異なる。 Arduino コアを参考に移植しました
  • K210 は RISC-V * 2 と AI コアと呼ばれる 3コア構成。このうち AI コアを有効にすると 2MB SRAM が追加で使えるようになる。ここここを参考
  • static 領域をおく .bss 領域が大きくないため、エミュレーターの移植では大きな static を動的に malloc するように修正
  • それでもエミュコアがメモリー不足でしたので、使わない機能を disable できるようにコアのソースを修正してなんとか起動。
  • CPU のクロックは 400MHz が標準設定だが、600MHz まで上げることができる
  • いろいろやっているも先人の方がおられることに気がつく。 Quake や DOOM、MMD が動いてる!すごい!

ボード付属のカメラで画像認識なんかもできるようなので、引き続き遊んでみようと思います。 🙂

関連