世界を動かす技術を、日本語で。

OCamlでゲームボーイエミュレーターを作成する

概要

  • CAMLBOY はOCamlで実装されたGame Boyエミュレータ
  • ブラウザ上で動作 し、スマートフォンでも60FPSを実現
  • 中規模プロジェクトの実践例 としてOCamlの高度な機能を活用
  • テスト容易性・保守性重視 の設計と実装
  • アーキテクチャやインターフェース設計 の具体例を紹介

CAMLBOY: OCaml製Game Boyエミュレータの開発記

  • CAMLBOY はOCamlで書かれたGame Boyエミュレータで、 ブラウザ上で動作 する実装
  • デモページで Bouncing ballRocket Man Demo など複数のROMを体験可能
  • スマートフォンでも60FPS で快適に動作
  • GitHubリポジトリhttps://github.com/linoscope/CAMLBOY
  • スクリーンショット やコード例も公開

なぜOCamlでGame Boyエミュレータを作るのか

  • 新しい言語学習時に感じる「 中規模以上のコードが書けない」「 高度な言語機能の実践的な使い方が分からない」という課題
  • OCamlの理解を深めるため、実践的なプロジェクトとしてエミュレータ開発を選択
  • プロジェクト選定理由
    • 仕様が明確で実装範囲がはっきりしている
    • 数日・数週間では終わらないが、数ヶ月で完了可能な規模
    • 子供時代の思い出 としてのGame Boy
  • 実装目標
    • 可読性・保守性重視のコード
    • js_of_ocaml でJavaScript化し、ブラウザで動作
    • スマホで遊べるFPS を実現
    • ベンチマーク を取り、コンパイラごとの比較

本記事の対象・内容

  • Game Boyエミュレータ実装中規模OCamlプロジェクト に興味がある方向け
  • OCamlの高度な機能活用例 (ファンクタ、GADT、ファーストクラスモジュール等)
  • Game Boyアーキテクチャ概要
  • テスト容易・再利用性の高いコード構造
  • ボトルネック発見やパフォーマンス改善
  • OCamlに関する所感
  • 基本文法や詳細なGame Boy仕様は割愛 (参考資料を別途案内)

アーキテクチャ設計

  • CAMLBOY全体構成図 を簡単に説明
    • CPU/タイマー/GPU はクロックに従い固定レートで動作
    • Bus が各ハードウェアモジュール間のデータ転送を制御
    • アドレスごとに適切なモジュールへデータをルーティング
    • Addressable_intf.S というインターフェースを各モジュールが実装
    • 割り込み要求 はタイマー・GPU・シリアルポート・ジョイパッドから可能

メインループ同期手法

  • CPU/タイマー/GPU の動作を同期させるための工夫
    • CPUが命令1つを実行し、消費サイクル数を計測
    • タイマー・GPUを同じサイクル分だけ進める
    • catch up手法」と呼ばれる同期方式
  • 実装例(抜粋)
    • let mcycles = Cpu.run_instruction t.cpu in
    • Timer.run t.timer ~mcycles;
    • Gpu.run t.gpu ~mcycles

データ読み書きインターフェース設計

8ビットデータ用インターフェース

  • Bus がGPUやRAMなど複数モジュールと8ビットデータやり取り
  • OCamlのモジュール型シグネチャ でインターフェースを共通化
    • Addressable_intf.Sとして定義
  • 各モジュール(RAM/GPU/Timer/Joypad等) でこのシグネチャをinclude

16ビットデータ用インターフェース

  • CPUとBus間では16ビットデータの読み書きも必要
  • 8ビット用シグネチャを拡張 し、Word_addressable_intf.Sを定義
    • 16ビット読み書き関数(read_word/write_word)を追加

Busモジュール実装

  • Bus は各モジュールをフィールドとして保持
  • アドレスに応じて適切なモジュールへread/writeをルーティング
  • 16ビット読み出しは8ビット2回で実現 (実機と同様)

レジスタ・CPU実装

レジスタ

  • Game Boy CPUの8ビットレジスタ(A/B/C/D/E/F/H/L)
  • 16ビットレジスタ(AF/BC/DE/HL) としても利用可能
  • read/write関数 でアクセス

CPU

  • 初期実装
    • BusやRegisters等をフィールドとして保持
    • 命令取得・デコード・実行の流れ
  • 課題:依存関係が多くテストしづらい
    • Busが他モジュールに依存するため、CPU単体テストが困難

ファンクタ活用によるテスト容易性向上

  • OCamlのファンクタ でBus実装を抽象化
    • module Make (Bus : Word_addressable_intf.S) : sig ... end
  • Mock実装を注入することでCPU単体テストが容易
    • 依存モジュール未完成でもテスト可能な設計

まとめ

  • OCamlによる中規模プロジェクト の実践例として、CAMLBOYは 設計・テスト容易性・高度な言語機能活用 の好例
  • モジュール型・ファンクタ・シグネチャ を活用し、 保守性・拡張性・テスト性 を両立
  • ブラウザ動作・スマホ対応 も実現し、 実用的なエミュレータ として完成度が高い

Hackerたちの意見

これはOCamlだけじゃなくて、ゲームボーイエミュレーターの実装についても素晴らしいまとめだね。すごい仕事だし、作者に感謝!余談だけど、アセンブラエディタとアセンブラ/リンカ/ローダーを使って、ブラウザでゲームボーイのホームブリューができるシングルページアプリを作るのってめっちゃ面白そうだと思ってる。これ、埋め込み開発の教育機会としても素晴らしいと思うんだ。

rgbds-liveは、RGBDSの埋め込み版みたいなもんだね。https://gbdev.io/rgbds-live/

あ、いいね!ファンクタやGADTの使い方が素晴らしい。CHIP 8やNESのエミュレーターと比べたいし、ocaml-wasmを使ってCAMLBOYをWASMに移植したいな。

js_of_ocamlの新しいWASMバックエンド(wasm_of_ocaml)のおかげで、もうCAMLBOYをWASM上で動かすことはできるはずだよ。

ちょっと難しいかもしれないけど、ゲームボーイエミュレーターの音に関するチュートリアル知ってる人いる?ほとんどのチュートリアルはその部分をカバーしてなくて、自分でやってみると実装するのも、参考資料を理解するのも難しいんだよね。

https://youtu.be/a52p6ji1WZs かも?

https://nightshade256.github.io/2021/03/27/gb-sound-emulatio... は結構いいよ。

厳密にはチュートリアルじゃないけど、私がどうやってやったかを説明してるスライドが2枚あるよ: https://www.slideshare.net/slideshow/emulating-game-boy-in-j... 基本的には、4つのチャンネルがあって、それぞれが毎ティックごとに0から15の数字を出すんだ。エミュレーターはそれを混ぜ合わせて(算術平均)、0から255にスケールアップしてサウンドバッファに送る。ティックレート(4.19MHz)をサウンド出力レート(例えば22kHz)に合わせて調整するのがポイントで、約190の値(4.19MHz / 22kHz)を取るのがいいスタートだよ。各チャンネルが生成する0..15の値はその特性に依存するけど、よく文書化されてるよ: https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware チャンネル1と2は矩形波を生成するから、低い値(0)と高い値(15)が交互に出て、オプションでボリュームエンベロープ(矩形波の「高い」部分で15から0に徐々に下がる)や周波数スイープ(0と15が交互に出るのが遅くなったり早くなったり)もある。チャンネル3はメモリから読み取った任意の波形を許可する。チャンネル4はLSFRによって生成されるランダムノイズだよ。参考にSoundModeX.javaを見てみてね: https://github.com/trekawek/coffee-gb/tree/master/src/main/j...

すごくいいまとめだね!シェアしてくれてありがとう。Rustでゲームボーイエミュレーターを書きたいと思ってて、あなたのブログ記事がそのきっかけになったよ。ブックマークしておくね。

素晴らしいまとめとクールなプロジェクトだね。2022年版が必要だね。

いいね。ただ、デモが速すぎる気がする。スロットルのチェックボックスを外してもあんまり変わらないし、むしろ遅くなる気がする。スロットルありだと240fps、なしだと180fpsで動いてる。スロットルが有効な状態だと、1秒がエミュレーター内ではもう4秒くらいになっちゃう。これって、画面のリフレッシュレートが関係してるんじゃないかな。私の場合は240Hzだし。

たぶん、requestAnimationFrame()を呼んでて、deltaTimeを考慮してないんじゃないかな?

ここにいる人たちの中で、エミュレーターや仮想マシン、バイトコードインタープリターを書くのに特に良いプログラミング言語があるって主張する人いる?「良い」って言うのは、効率的な結果を得るとか、実装ミスを減らすって意味じゃなくて、特定の言語でエミュレーターを実装する経験がもっと報われる、直感的で、エミュレーターやその言語についてもっと学べるってことなんだ。こういう言語は他の分野にはあるって知ってるから聞いてるんだけど。例えば、Erlangは「ソフトリアルタイムの99.9999999%稼働の分散システム」を実装するのに特にやりがいがあるんだ。この言語、その実行セマンティクス、ランタイム、コアライブラリは、特定の問題領域に対処するために共同設計されてるからね。Erlangを「その目的に使う」ことで、分散システムについてたくさん学べるし(言語やランタイムが分散システムの問題に対する独自の解決策に導いてくれるから)、Erlangの理解も深まると思う。もし他の分野の問題を解決するために使うよりも、言語デザイナーが「コードで機械を表現する」ことを目指してた場合、そんな言語はあるかな?

C89

sml、特にMLTon方言がいいね。ocamlが良い理由と同じ理由で、ML言語のもっと良いバージョンだと思う。ocamlにあってsmlにないのはアプリカティブファンクターだけだけど、結局それはちょっと違うモジュールスタイルに変わるだけだし。

まあ、いつでもhttps://pypi.org/project/rpython/があるよ。

Cは多分、これに最適な言語だね。

Verilog?…冗談だよ(多分)。純粋なインタプリタのことを話しているなら、バイトや配列を扱いやすくするものなら何でも大丈夫だと思う。Haskellはあまりおすすめしないかな。ほとんどの操作が機械の状態を命令的に変更することになるから、純粋なFPではあまりメリットがないと思う。解釈の基本的なプロセスは「オペコードを読み取って、それに基づいて処理する」って感じ。おそらく、メモリアドレス空間を維持する必要があるね。それだけかな?ほとんどの言語はそれをうまくこなせるよ。だから、選ぶ基準は他のことに基づくべきだね。使いやすさ、ホストプラットフォームとのインターフェースの能力、型チェックの好みとか、そういう感じ。

HaskellはDSLやコンパイラで必要なデータ操作に優れてるよ。OCamlやLisp、ADTsをサポートしている言語ならどれでもいけると思う。モダンなC++やバリアント型を使って頑張ることもできるけど、あまり綺麗にはならないかな。もちろん、エミュレーターでゲームを実行したいなら、CかC++が一番だね。Rustも使えると思うけど、低レベルのメモリ操作についてはあまり詳しくないな。

速い反復の選択肢の一つはForthだね。そっちの界隈では、ターゲット生成やアーキテクチャ間のクロスコンパイルで有名だよ。ネットで探せばたくさん見つかると思う。

OCamlはここでは悪くない選択肢に思えるね。あまり触ったことはないけど、Racketもいい選択肢かもしれないな?