[Astro] #156 SameBoy WASM — Game Boyエミュレーターコアの自前EmscriptenビルドとAstro組み込み
はじめに
前回(#155)は、既存の WASM ビルド(Infinite Mac 版 DingusPPC)をお借りして、Web Worker や SharedArrayBuffer などの「ブラウザ側のつなぎ」を自前で実装する記事を書きました。
[Astro] #155 Power Mac Emulator — DingusPPC WASMでMac OS 8.1をブラウザ起動(おまけ:Copland D11E4ランチャー) // PROTOCOL.LAIN
DingusPPC(mihaip版WASMビルド)を自前のWorkerで動かす実装記録。C++から呼ばれるworkerApiの口、SharedArrayBufferのロックによる入力受け渡し、OPFS SyncAccessHandleでのディスクI/O、素のHFSイメージにドライバ入りパーティションマップを付け足して起動する方法、ログの洪水対策、Copland版(mist64)との設計の違いまで。
lain-lab.com今回はその番外編です。普段は Z80 や 6502 といった CPU コア自体を自作していますが、今回はエミュレーター本体の自作ではなく、高精度 Game Boy / Game Boy Color エミュレーター「SameBoy」の C 言語ソースコードから WebAssembly(.wasm)を自分の手でビルドし、Astro ページへ組み込む プロジェクトに取り組みました。
GitHub - LIJI32/SameBoy: Game Boy and Game Boy Color emulator written in C
Game Boy and Game Boy Color emulator written in C. Contribute to LIJI32/SameBoy development by creating an account on GitHub.
github.com構成
今回のプロジェクトの構成です。WASM ファイルは自前でビルドしたものを public/libs/sameboy/ 配下に配置し、Astro ページから読み込みます。
my-astro-project/
├─ src/pages/
│ └─ sameboy.astro UI & 起動用スクリプト
└─ public/
└─ libs/
└─ sameboy/
└─ sameboy_libretro.wasm ← 自前でコンパイルした SameBoy WASM コア
SameBoy Libretro コアの WASM ビルド
SameBoy は Libretro API に対応しているため、Emscripten ツールチェーン(emcc)を使って WASM 化できます。
1. Makefile の修正
SameBoy のリポジトリを取得し、libretro/Makefile 内の Emscripten ターゲット記述部(160行目付近)を確認すると、未定義シンボルを許容しないフラグ -Wl,--no-undefined が入っており、Emscripten リンク時にエラーを引き起こします。
libretro/Makefile 修正箇所:
# 修正前
else ifeq ($(platform), emscripten)
TARGET := $(TARGET_NAME)_libretro_emscripten.bc
fpic := -fPIC
SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined
# 修正後(-Wl,--no-undefined を削除)
else ifeq ($(platform), emscripten)
TARGET := $(TARGET_NAME)_libretro_emscripten.bc
fpic := -fPIC
SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T
2. コンパイルの実行
emmake を通して make を実行します。
# クリーン処理
make clean
# Emscripten プラットフォームを指定してビルド
emmake make platform=emscripten
3. 生成された .bc ファイルの正体
ビルドが完了すると sameboy_libretro_emscripten.bc というファイルが生成されます。
拡張子は .bc(LLVM Bitcode)となっていますが、近年の Emscripten 環境で -shared オプションを付けて出力されたこのファイルは、実質的に完成された WebAssembly(SIDE_MODULE / WASM バイナリ) です。バイナリヘッダーを確認すると \0asm(WASM のマジックナンバー)が刻まれています。
そのため、追加で emcc で再コンパイル・再リンクしようとすると「入力フォーマットが不正」と弾かれてしまいます。単純に拡張子を .wasm に変更(コピー)すればそのまま使用可能です。
cp sameboy_libretro_emscripten.bc sameboy_libretro.wasm
生成された sameboy_libretro.wasm を Astro プロジェクトの public/libs/sameboy/ ディレクトリに配置します。
Astro ページへの組み込みと EmulatorJS の活用
Libretro API コア(.wasm)単体は計算エンジンであるため、画面描画(Canvas)、音声出力(Web Audio API)、キー入力を繋ぐフロントエンド(ラッパー)が必要です。
今回は軽量かつ互換性の高い EmulatorJS をフロントエンドの枠組みとして採用し、コアファイルだけを自前でビルドした sameboy_libretro.wasm に差し替える設計にしました。
src/pages/sameboy.astro の実装
---
// src/pages/sameboy.astro
---
<div id="emulator-container">
<!-- ROMファイル未選択時の UI -->
<div id="file-selector">
<p>手元のROMファイル (.gb / .gbc) を選択してください</p>
<input type="file" id="rom-input" accept=".gb,.gbc,.zip" />
</div>
<!-- エミュレータ画面 -->
<div id="game"></div>
</div>
<style>
#emulator-container {
width: 640px;
height: 480px;
max-width: 100%;
margin: 0 auto;
background: #111;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
overflow: hidden;
}
#file-selector {
text-align: center;
padding: 20px;
}
#game {
width: 100%;
height: 100%;
}
</style>
<script is:inline>
const romInput = document.getElementById('rom-input');
const fileSelector = document.getElementById('file-selector');
romInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
// 1. ローカルファイルをブラウザメモリ上の Blob URL に変換
const romUrl = URL.createObjectURL(file);
// 2. EmulatorJS のパラメータ設定
window.EJS_player = '#game';
window.EJS_gameName = file.name;
window.EJS_gameUrl = romUrl;
window.EJS_core = 'gb';
// 自前でビルドした SameBoy WASM を指定
window.EJS_coreUrl = '/libs/sameboy/sameboy_libretro.wasm';
window.EJS_pathtodata = '[https://cdn.emulatorjs.org/stable/data/](https://cdn.emulatorjs.org/stable/data/)';
window.EJS_language = 'en-US'; // 404 エラー対策
// 3. UI 切り替え
fileSelector.style.display = 'none';
// 4. ローダーを動的に呼び出して起動
const script = document.createElement('script');
script.src = '[https://cdn.emulatorjs.org/stable/data/loader.js](https://cdn.emulatorjs.org/stable/data/loader.js)';
document.body.appendChild(script);
});
</script>
組み込み時のハマりどころと解決策
1. 言語ファイル(ja.json)の 404 エラー
EmulatorJS 起動時にブラウザのコンソールで GET https://cdn.emulatorjs.org/stable/data/localization/ja.json 404 というエラーが発生していました。
これは、EmulatorJS がブラウザの言語設定(ja)を検知して日本語化ファイルを自動取得しようとするものの、CDN 上に該当ファイルが存在しないために起きる現象です。
設定に window.EJS_language = 'en-US'; を明示的に追加することで無用な 404 リクエストを回避できます。
2. サーバーに ROM を置かないアーキテクチャ
著作権保護とサーバー負荷削減のため、サーバー上には一切 ROM ファイルを置かない設計にしました。
HTML5 の File API と URL.createObjectURL(file) を組み合わせることで、ユーザーがローカルで選択した .gb や .gbc ファイルを即座にメモリ上の Blob URL に変換できます。これを window.EJS_gameUrl に引き渡すことで、完全クライアントサイド(ブラウザ内完結)で安全にゲームが起動します。
クレジットとライセンス
- SameBoy(MIT License)— LIARLIAR / SameBoy 開発チーム github.com/LIARLIAR/SameBoy
- EmulatorJS(GPL-3.0)— EmulatorJS チーム github.com/EmulatorJS/EmulatorJS
おわりに
普段行っている CPU コア自作とは異なり、既存のオープンソース C 言語プロジェクトを Emscripten で手動コンパイルし、ビルドされた WebAssembly モジュールを最新の Web フロントエンド(Astro)に組み込むという「WASM ポーティング」の知見を得ることができました。
C/C++ で書かれたコードを WASM に落とし込み、URL.createObjectURL や JS Bridge を通してブラウザ上でネイティブ同等のパフォーマンスで動作させる一連の流れは、今後の自作 CPU(6502 など)を WASM 化して Web 上で公開するプロジェクトへの大きな足がかりとなりそうです。