[Astro] #156 SameBoy WASM — Game Boyエミュレーターコアの自前EmscriptenビルドとAstro組み込み

[Astro] #156 SameBoy WASM — Game Boyエミュレーターコアの自前EmscriptenビルドとAstro組み込み

はじめに

前回(#155)は、既存の WASM ビルド(Infinite Mac 版 DingusPPC)をお借りして、Web Worker や SharedArrayBuffer などの「ブラウザ側のつなぎ」を自前で実装する記事を書きました。

今回はその番外編です。普段は Z80 や 6502 といった CPU コア自体を自作していますが、今回はエミュレーター本体の自作ではなく、高精度 Game Boy / Game Boy Color エミュレーター「SameBoy」の C 言語ソースコードから WebAssembly(.wasm)を自分の手でビルドし、Astro ページへ組み込む プロジェクトに取り組みました。

構成

今回のプロジェクトの構成です。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 に引き渡すことで、完全クライアントサイド(ブラウザ内完結)で安全にゲームが起動します。

クレジットとライセンス

おわりに

普段行っている CPU コア自作とは異なり、既存のオープンソース C 言語プロジェクトを Emscripten で手動コンパイルし、ビルドされた WebAssembly モジュールを最新の Web フロントエンド(Astro)に組み込むという「WASM ポーティング」の知見を得ることができました。

C/C++ で書かれたコードを WASM に落とし込み、URL.createObjectURL や JS Bridge を通してブラウザ上でネイティブ同等のパフォーマンスで動作させる一連の流れは、今後の自作 CPU(6502 など)を WASM 化して Web 上で公開するプロジェクトへの大きな足がかりとなりそうです。