WATaBoy:WASM でゲームボーイの命令を JIT コンパイルしてネイティブインタプリタに勝利する

2026/06/30 0:02

WATaBoy:WASM でゲームボーイの命令を JIT コンパイルしてネイティブインタプリタに勝利する

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

WATaBoy プロジェクトは、Rust で記述された高性能なゲームボーイエミュレータを導入し、「JIT-to-Wasm」アプローチを採用しています:ランタイムで WebAssembly バイトコードを生成し、ブラウザの JavaScript エンジン(例:JavaScriptCore)が頻繁な呼び出しに応じてネイティブ機械語へコンパイルする方式です。本システムは

wasm-encoder
クレートを使用してバイトコードを出力し、標準的な bindgen ツールではなく C ABI を介して Rust-JS 境界間でのデータ転送を実行します。動的な関数成長をサポートするため、ニghtly Rust 環境内で非公式なリンクフラグ(例:
--growable-table
)を使用し、
asm_experimental_arch
などのあるinline WebAssembly 機能を利用しています。Pokémon Blue などのゲームのタイトル画面をループ再生したベンチマーク結果では、M2 MacBook Air 上でこの手法は標準的なインタープリタ方式よりも約1.2倍高速、生の WebAssembly エグゼッションよりも約1.5倍高速であることが示されました。Safari が最も高い性能を示し、iOS の WebKit に制限が存在してもこの手法を阻害しないことを検証した一方で、本アプローチは現在 nightly Rust に依存しており、他のプロジェクトに見られるような一部の低レイヤーの最適化や、エルゴノミクスなコード生成ツールが不足しています。将来の取り組みとしては、PPU インターラプト予測、オーディオエミュレーション、Game Boy Color サポートの実装を目指し、既存のブラウザコンパイラとプロプライエタリツールを使用せずに高性能なエミュレータを構築できることを証明するものです。

  • Changes made:
    • Key Points List からの特定の技術詳細(
      wasm-encoder
      、C ABI、nightly Rust 機能)を追加しました。
    • ベンチマーク手法の説明を明確化(ループ再生されたタイトル画面を使用)。
    • iOS の制限に関する表現を「克服」するという強い主張から、「...を検証した」というより正確な表現へ調整しました。
    • コード生成ツールの不足および Dolphin との比較について欠けていた制約事項を含めました。

本文

JIT-to-Wasm:Wasm ランタイム内の即時コンパイルを活用した Game Boy エミュレータ「WATaBoy」の実装と評価

本記事では、iOS の JIT(即時展開)制限を回避し、WebAssembly 上で最適化されたコード実行を実現する手法について解説します。具体的には、「Rust から Wasm バイトコードを生成し、それを Web ブラウザ上でコンパイル・リンキングする」アプローチ(以下 JIT-to-Wasm と呼称)を用いた Game Boy エミュレータ「WATaBoy」の構築と性能検証を行います。


1. 背景と技術的課題

1.1 iPhone における JIT の制限と例外

Dolphin エミュレータが iOS App Store で提供できない主な理由は、iOS が JIT 展開を許可していないためです。しかし、Apple は Web ブラウザに対しては例外扱いを行っており、以下のような仕組みが存在します。

  • WebKit の活用: JavaScriptCore (JSC) や WebAssembly (Wasm) エンジンも内部で JIT 展開を利用しています。
  • 仕組み:
    • JavaScript/Wasm が呼び出されると、やがて最適化されネイティブマシンコードへコンパイルされます。

1.2 アプローチの転換:JIT-to-Wasm

既存プロジェクト(The Jitterpreter や v86)は Wasm を JIT 生成する技術を採用していますが、ゲームコンソールエミュレータへの応用例やネイティブインタプリタとの比較は希少でした。

  • 本研究の目的:
    • Game Boy エミュレータを「インタプリタ方式」と「JIT-to-Wasm 方式」の 2 パターンで実装し、性能差を検証する。
    • 「ブラウザが Wasm をマシンコードへ再コンパイルする」と「Wasm の JIT 展開」を混同しないよう注意します。

1.3 なぜ Game Boy に JIT が必要なのか?

一般的に 64bit/128bit 演算が多い第六世代以降のコンソールとは異なり、Game Boy エミュレーションへの恩恵は小さいように思われますが、以下の手法により性能向上が可能になります。

  • 割り込みタイミングの予測: JIT ブロック内で割り込みが入る場合のみインタプリタへフォールバックする。
  • 遅延評価(Lazy Evaluation): MMIO アクセスなどのコンポーネントは必要になった時点で計算を行う。
    • 参考:GameRoy のブログ「Game Boy Emulator JIT Implementation」における技術詳細は、最適化技術の多くが通用する良質なリソースです。

2. 実装:Rust 内での Wasm コード生成と後期リンキング

本稿では、汎用的な部分である**「Rust 内での Wasm コード生成と後期リンキング(late-linking)」**に焦点を絞って解説します。

2.1 環境準備と依存関係

wasm-bindgen
など通常のバインドツールは低レベルの Wasm 操作においてエルゴノミクス面で問題があるため、C ABI を経由する手動アプローチを採用しました。Nightly Rust が必須となります。

コマンド例

# Nightly Rust をデフォルトに設定
rustup default nightly

# 新たなライブラリを作成(--lib flag)
cargo new --lib jit-to-wasm

依存関係は

wasm-encoder
クレートのみです(ビルダーパターンとして使用)。

[package]
name = "jit-to-wasm"
version = "0.1.0"
edition = "2024"

[lib]
# .wasm ファイルを生成するために必要。
crate-type = ["cdylib"]

[dependencies]
wasm-encoder = "0.252.0"

2.2 Wasm コード生成の例

単純な加算関数(

add
)を含む Wasm モジュールを
make_add_module
関数で生成します。型定義、関数登録、エクスポート、コードセクションの手動エンコーディングが必要です。

use wasm_encoder::*;

fn make_add_module() -> Vec<u8> {
    let mut module = Module::new();

    // 1. 型セクション:引数 2 つ (i32), 戻り値 1 つ (i32)
    let mut types = TypeSection::new();
    let params = vec![ValType::I32, ValType::I32];
    let results = vec![ValType::I32];
    types.ty().function(params, results);
    module.section(&types);

    // 2. 関数セクション:型インデックス 0 を参照
    let mut functions = FunctionSection::new();
    let type_index = 0;
    functions.function(type_index);
    module.section(&functions);

    // 3. エキスポートセクション:"my_add_func" で公開
    let mut exports = ExportSection::new();
    exports.export("my_add_func", ExportKind::Func, 0);
    module.section(&exports);

    // 4. コードセクション:実装
    let mut codes = CodeSection::new();
    let locals = vec![];
    let mut my_add_func = Function::new(locals);
    my_add_func
        .instructions()
        // スタックに左辺を取得
        .local_get(0)
        // スタックに右辺を取得
        .local_get(1)
        // 加算
        .i32_add()
        // エンド
        .end();
    codes.function(&my_add_func);
    module.section(&codes);

    // 完了してバイトコードを抽出
    module.finish()
}

2.3 コンパイル・リンキング・ディスパッチの連携

生成した Wasm バイトコードを直接実行できないため、WebAssembly ハーバード構造(別メモリ空間)を利用して、JavaScript 側でコンパイル・インスタンス化し、メインプログラムへ関数を登録します。

Rust 側の実装ポイント

  • Nightly 機能:
    asm_experimental_arch
    を使用して Wasm インストラクションを直接組み立てます。
  • リンキー関数: JavaScript からの呼び出しで新しいモジュールを読み込み、間接関数テーブル(Indirect Function Table)に追加します。
#![feature(asm_experimental_arch)] // 必須:Nightly 機能

use std::arch::asm;

// Wasm の関数テーブルの index にある関数を間接的に呼ぶディスパッチ関数
fn dispatch(index: i32, left: i32, right: i32) -> i32 {
    let mut result: i32;
    unsafe {
        // call_indirect を内联 Wasm として展開
        asm!(
            "local.get {right}",
            "local.get {left}",
            "local.get {index}",
            "call_indirect (i32, i32) -> (i32)",
            "local.set {result}",
            index = in(local) index,
            left = in(local) left,
            right = in(local) right,
            result = lateout(local) result,
        );
    }
    result
}

#[unsafe(no_mangle)]
pub extern "C" fn make_and_execute_add(left: i32, right: i32) -> i32 {
    let add_bytecode = make_add_module();
    
    // JavaScript 側で呼ばれるリンキング関数へのポインタ
    let func_idx = unsafe {
        link_new_module(add_bytecode.as_ptr(), add_bytecode.len())
    };

    dispatch(func_idx, left, right)
}

ビルドスクリプト (
build.rs
)

Lld リンカーに追加フラグを渡して、間接関数テーブルのエクスポートと拡張性を確保します。

fn main() {
    println!("cargo:rustc-link-arg=--export-table");   // 間接関数テーブルをエクスポート
    println!("cargo:rustc-link-arg=--growable-table"); // テーブルの成長許可(未完全ドキュメントだが機能)
}

ビルドコマンド:

cargo build --release --target wasm32-unknown-unknown

3. 埋め込み側(JavaScript)の実装

Wasm バイトコードを読み込み、ブラウザ上の WebAssembly インスタンスとしてコンパイルし、間接関数テーブルへ追加する処理を行います。

3.1 リンキング関数の実装例

// メイン Wasm モジュールをインスタンス化する
const source = fetch("target/wasm32-unknown-unknown/release/jit_to_wasm.wasm");
const {instance} = await WebAssembly.instantiateStreaming(source);

// 後期リンキング(Late-linking)を実装する関数
const linkNewModule = (bufferPtr, bufferLen) => {
  // メインインスタンスのメモリから Wasm バイトコードを読み出す
  const bytecode = new Uint8Array(
    instance.exports.memory.buffer,
    bufferPtr,
    bufferLen
  );

  // バイトコードを新しいインスタンスとしてコンパイル
  const newModule = new WebAssembly.Module(bytecode);
  const newInstance = new WebAssembly.Instance(newModule);

  // 新しい関数をメインの"間接関数テーブル"に追加(grow)
  instance.exports.__indirect_function_table.grow(
    1,
    newInstance.exports.my_add_func
  );

  // リンキングされた関数のインデックスを返す
  return instance.exports.__indirect_function_table.length - 1;
};

3.2 実行確認

このコードを実行すると、コンソール上に

5
が出力されます。

注意: この技法により生み出された WATaBoy のデモは非常に高速ですが、光感覚過敏性てんかんの患者さんにおいて発作を引き起こす可能性があります⚠️(「スタート」押下にご注意ください)。


4. 性能ベンチマーク結果

WATaBoy の JIT コンパイラ、Wasm 内インタプリタ、ネイティブインタプリタの 3 つを比較しました。 測定条件: Game Boy ROM のタイトル画面をループさせて 10 秒(壁時計時間)エミュレートし、フレーム総数をカウント。

4.1 ベンチマーク環境

項目詳細
OSmacOS 26.5 (25F71)
CPUMacBook Air (13 インチ, M2, 2022)
メモリ16 GB
ブラウザSafari 26.5, Chrome 148.0, Firefox 150.0
Rust 版1.97.0-nightly (2026-05-09)
WATaBoy 版Commit c06850a
検証回数各構成 5 回反復(平均値)

4.2 主要な結果:『ポケットモンスター ブルー』

  • JIT-to-Wasm: ネイティブインタプリタの 約 1.2 倍 の速度に達しました。
    • ネイティブマシンコードから一段間接的なレイヤがあるにもかかわらず、JIT の恩恵が確認されました。
  • Wasm インタプリタ: JIT-to-Wasm よりも約 1.5 倍遅い です。
  • ブラウザ依存性:
    • Safari: 先行する性能を示しました。
    • Firefox / Chrome: も同様の恩恵を得ていましたが、iOS が WebKit 限定であるため、Web ブラウザでの検証が重要です。

※他の ROM(『ゼルダの伝説 リンクの冒険』、『トブトブガール』)でも同様の傾向が確認されました。


5. 今後の課題と展望

5.1 WATaBoy の現状課題

  • 機能: オーディオおよび GBC(カラーモード)サポートは未実装です。
  • ボトルネック: PPU(ピクセルプロセッサ)のエミュレーションが実行時間の大部分を占めており、JIT ブロック内の非分岐命令の再コンパイルがまだ十分に進んでいません。
  • 最適化の余地: 基本的なブロック JIT はインタプリタを上回っていますが、分岐命令を再コンパイルすることでフォールバック頻度を減らし、さらに性能向上が見込めます。

5.2 JIT-to-Wasm 技術的な制限と課題

  • コード生成(Codegen)の工数:
    • 既存プロジェクトは独自のツールを使用しており、DynASM や Cranelift に匹敵する使いやすさ・頑健さが不足しています。
    • 将来的には、ヒューマンリーダブルな文字列を入力してコンパイル時にバイトコードを生成するような専用ツールの開発が必要です。
  • 最適化の限界:
    • Dolphin などが行うようなハードウェア依存の深い最適化(例:fastmem)は、Wasm ランタイムの制限により適用できません(無効なメモリアクセスは回復不可能)。

5.3 結論と意義

この手法では GameCube エミュレータをフルスピードで動作させるものではありませんが、**「インタプリタよりも速い基本的なブロック JIT が存在し、Wasm をクロスプラットフォームのターゲットとして活用できる」**ことを実証しました。

特に iOS 上での高速化において、Web ブラウザを利用した WebAssembly(JIT-to-Wasm)は大きなポテンシャルを秘めています。よりマチュアなコード生成ツールが成熟すれば、クロスプラットフォームエミュレーションの標準的な選択肢になるでしょう。


WATaBoy の全バージョンや詳細なコードは GitHub で公開されています。興味のある方は是非ご検討ください。

同じ日のほかのニュース

一覧に戻る →

2026/06/30 4:49

/.self: ホスト環境を構築することを支援する新しいトップレベルドメイン

## 日本語訳: 本件の核心となるメッセージは、ユーザーのデータや注意を搾取する既存のモデルを捨て、倫理的な新アーキテクチャへとインターネットを変革する呼びかけです。Human-Centered Computing Foundation は、ICANN の Applicant Support Program を通じてこのイニシアチブを正式に開始し、その主な目標として、倫理的技術にのみ専属 reserved されるトップレベルドメイン(TLD)の確保を目指しています。この動きは、人間の行動から価値を抽出するという業界の確立されたダイナミクスに直接挑戦し、代わりに人間中心の価値に基づいたシステムを提案しています。 もしこの新しいドメイン拡張を取得することに成功すれば、同財団はユーザーエシクティクスをデータマイニングよりも優先するプロジェクトのみがホストされる特定のデジタル空間を作成します。この転換は大きな利益をもたらすと約束しており、個人は企業の監視ではなく自らの道徳的原則を中心に設計された Web 環境を航行することができます。企業にとっては、持続的な成功には単に注意を採取するのではなく、真の人間のニーズを満たすアーキテクチャが不可欠になる、避けられない未来を示しています。最終的に、このキャンペーンは、技術が人々を利用するために操作するのではなく、人々をサービスするためのセクターとして、誠実さを定義されたインターネットの別個の分野を確立することを目指しています。

2026/06/30 2:05

Qwen 3.6 27B はローカル開発のsweet spot(最適解)です。

## Japanese Translation: 本文は、ローカルコード生成のために Qwen 3.6 27B デンスモデルを優先すること advises(推奨)しています。これは、指示追従の精度と効率的なパフォーマンスのバランスが取れており、Node パッケージの作成といった特定のタスクで失敗する可能性があるように 35B の A3B mixture-of-experts などのより大きなバリエーションを上回る場合があるためです。ベンチマークによると、このモデルは消費者向けハードウェア上で効率的に動作しながら、2025 年の中盤の GPT-5 程度の知能レベルに達します。Apple M5 チップ(共有 RAM を最大 48 GB 使用)では約 30 トokens/秒、量子化された状態で高級な Nvidia RTX 5090 カードでは 50 トokens/秒 にスケールします。重要なのは、著者が倫理的かつ技術的な理由から、Ollama ではなく `llama-server` または `llama-cli` を使用して Hugging Face の量子化版(例:`unsloth/Qwen3.6-27B-MTP-GGUF:Q8_0`)でモデルを実行することを推奨している点です。この構成により、開発者は OpenCode エージェントなどのツールと互換性のあるセキュアな「vibe coding」環境を構築できます。ローカルでモデルを実行することは、データのプライバシーを維持し、機密情報が外部の米中クラウドプロバイダーに漏洩することなく、オフラインでの作業をサポートするために不可欠です。将来的にはツールの呼び出しを通じて事実知識と生粋の知能を分ける傾向があるかもしれませんが、この即席のソリューションは品質を損なうことなく、個人および小規模チームの開発者にとってアクセス可能な入門点を提供します。より大きなモデルが将来的にはエンタープライズレベルのハードウェアを必要とするでしょうが、27B バリエーションは現在、標準的な消費者向けハードウェア上で DeepSeek-V4 Flash などのフロンティア代替案と比較できる堅牢でプライベートな AI 機能を 제공합니다(提供しています)

2026/06/28 0:05

アイコンを解放せよ

## Japanese Translation: 2026 年 6 月 26 日付の投稿で、Paul Kafasis は、macOS 26「Tahoe」がすべてのアプリアイコンに対して義務付けられた統一された「squircle」形状を導入し、ファーストパーティアイコンをボヤけた「Liquid Glass」 appearances に変更したと報告している。多くの人にとってこれはデザインと使いやすさにおける重大な後退だと見られている。サードパーティ製アイコンをこの指定された squircle 形状に強制することで、ユーザーが迅速な識別のために頼りにしていた多様な形状はなくなり、色が主な識別基準になった——特に色覚障害を持つユーザーや類似の色を持つアプリを区別する際には深刻な問題となった。コンプライアンスに反するサードパーティ製アイコンは縮小され、魅力的でない灰色の背景上に表示され、「icon jail」シナリオが引き起こされたほか、Apple の新しい「Clear」と「Tinted」アイコンスタイルは採用率が低かった。これは統一された squircle により識別がほぼ不可能になりつつあったためである。内部的なフィードバックチケット(FB23388490)でこれらの制限への異議が申し立てられたにもかかわらず、macOS 27「Golden Gate」の初期ベータ版では余計な「Liquid Glass」を取り除き、シャープなデザインを復活させ、Automator などのファーストパーティアイコンを見直し、部分的な改善が見られる。Kafasis は、Apple がサードパーティ製アプリに対して単一の squircle 形状を強制することをやめ、多様なアイコン形状を許可してアクセシビリティ、創造性、および総合的な使いやすさを向上させることを求めつつある。

WATaBoy:WASM でゲームボーイの命令を JIT コンパイルしてネイティブインタプリタに勝利する | そっか~ニュース