2 ポイント 投稿者 GN⁺ 2025-07-06 | 1件のコメント | WhatsAppで共有
  • CAMLBOYはOCamlで開発され、ブラウザで動作するGame Boyエミュレータ
  • OCamlの中規模〜大規模プロジェクト開発と高度な機能の使い方を実践的に学ぶために選ばれたプロジェクト
  • 基本構造、抽象化、GADT、ファンクタ、ランタイムモジュール切り替えなど、さまざまなOCamlの言語機能を実用的に活用
  • ブラウザで60FPSで動作し、性能改善の過程とボトルネック分析、最適化の経験を共有
  • OCamlエコシステム、テスト自動化、そしてエミュレータ開発が実務能力向上に与える影響を整理

プロジェクト概要

  • 数か月にわたりCAMLBOYプロジェクトを進め、OCamlでGame Boyエミュレータを制作
  • デモページで実行可能で、さまざまなhomebrew ROMを含む
  • リポジトリはGitHubで公開

OCaml学習の動機とプロジェクト選定の背景

  • 新しい言語を学ぶ際、中規模・大規模なコードを書く方法高度な機能の実際の活用法に限界を感じる
  • こうした問題を解決するため、実践的なプロジェクト経験の必要性を感じ、Game Boyエミュレータ開発を選択
  • 理由
    • 仕様が明確で実装範囲が定まっている
    • 十分に複雑だが数か月以内に完了可能な規模
    • 個人的な動機が強い

エミュレータの目標

  • 可読性と保守性を重視したコードを書く
  • js_of_ocamlJavaScriptにコンパイルしてブラウザで実行
  • モバイルブラウザでもプレイ可能なFPSを達成
  • さまざまなコンパイラバックエンドの性能ベンチマークを実装

本文の目的と主な内容

この記事の目的は、OCamlでGame Boyエミュレータを制作する旅を共有すること
扱う内容:

  • Game Boyのアーキテクチャ概要
  • テストしやすく再利用性の高いコードの構造化方法
  • functor、GADT、一級モジュールなど高度なOCaml機能の実践活用
  • 性能ボトルネックの発見、最適化と改善の経験
  • OCaml全般に対する考え

全体構造と主要インターフェース

  • CPU、Timer、GPUなど主要ハードウェアは同期されたクロックに従って動作
  • バスはアドレスに応じて各ハードウェアモジュールへのデータアクセス・転送を担当
  • 各ハードウェアモジュールはAddressable_intf.Sインターフェースを実装
  • バス全体はWord_addressable_intf.Sインターフェースに従う

メインループの動作方式

  • ハードウェアの同期のため、メインループで以下の循環段階を実行
    1. CPU命令を1つ実行し、消費したサイクル数を記録
    2. 同じサイクル数だけTimer、GPUを進める
  • この方法で実機ハードウェアの同期状態を模倣
  • 実装コード例とともに説明を提供

8ビット・16ビットのデータ読み書き抽象化

  • 多数のモジュールが8ビットデータ入出力インターフェース (Addressable_intf.S) を実装
  • 16ビット読み書きへの拡張はWord_addressable_intf.Sを通じて継承し、追加機能を拡張
  • OCamlのシグネチャ (signature)モジュール型のinclude方式で抽象化レイヤーを構成

バス、レジスタ、CPU実装

  • バス: 各ハードウェアモジュールに対するアドレスベースのルーティングを担当し、メモリマップ基準で分岐処理
  • レジスタ: 8ビット、16ビットレジスタの読み書きインターフェースを提供
  • CPU: 当初はバス依存が強くテストが難しかった
    • ファンクタ(functor)を適用して依存性を抽象化し、モック注入を可能にした
    • これにより単体テストの作成が大幅に容易になった

命令セットの表現 (GADT活用)

  • Game Boyには8/16ビット命令が両方存在し、命令定義の型安全性が必要
  • 単純なvariant方式では、複雑なパターンマッチングで戻り値型が衝突する問題が発生
  • GADT(Generalized Algebraic Data Type) を適用し、入力型と出力型の両方を安全にマッチング可能にした
  • GADT適用時には各命令の引数型、戻り値型をともに正確に型推論可能
  • 複雑な命令パターンやパラメータにも安全に対応

カートリッジとランタイムモジュール選択

  • Game Boyカートリッジには、単純なROM以外に追加ハードウェア(MBC、タイマーなど)が含まれる場合がある
  • 各タイプごとに別モジュールとして実装し、ランタイムに適切なモジュール選択が必要
  • 一級モジュールによってランタイムでのモジュール切り替えと拡張性を実現

テストと探索的開発

  • test ROMおよびppx_expectを活用
    • 機能別のテストROM: 算術演算、MBC対応など具体的な領域を検証
    • 失敗時は画面出力などで明確な診断が可能
  • 統合テストにより、大規模リファクタリングや新機能追加時の信頼性を確保
  • 探索的開発方式を適用し、テストROMで反復的に実装と検証を実施

ブラウザUIと性能最適化

  • js_of_ocaml簡単にJSビルド
  • Brrライブラリにより、JavaScript DOM APIへOCaml流に安全にアクセス
  • 初期性能(20FPS)は低かったが、ChromeプロファイラでGPU、タイマー、Bigstringafなどのボトルネックを分析
  • 各モジュールごとに最適化コミットを進め、JSビルドで非効率なインライン化を無効化して最終的に60FPS(PC/モバイル)を達成
  • ネイティブビルドでは1000FPSまで性能を発揮

ベンチマークとハードウェア比較

  • ヘッドレスベンチマークモードを実装し、各環境ごとのFPS測定が可能

エミュレータ開発と実務能力

  • 競技プログラミングに似ており、明確な仕様の解釈 → 実装 → 検証のループを反復
  • 仕様ベースの開発・テストを進めるうえで実践的に役立つ経験

最新のOCamlエコシステムとツールの進展

  • duneにより手軽なビルドシステムを体験可能
  • MerlinOCamlformatなどで自動補完、コードナビゲーション、フォーマットが容易
  • setup-ocamlでGitHub Actionsにも簡単に適用可能

関数型言語についての考察

  • 関数型言語とは副作用を最小化するものという説明に疑問を抱く
  • 抽象化の下に隠れたmutable状態は性能のために積極的に利用
  • 筆者は静的型付け、パターンマッチ、モジュールシステム、型推論などを好む

不便な点と抽象化依存のコスト

  • 依存関係管理の標準化はなお複雑で、説明も不足している (opamなど)
  • モジュール-ファンクタ構造で抽象化を加えると、依存層全体の構造まで修正が必要
  • OOPと異なり、抽象化を導入すると上位依存モジュールの書き方まで変更が必要

おすすめ学習資料

結論

  • CAMLBOYプロジェクトを通じてOCamlの高度な機能、テスト、抽象化、ブラウザ互換性などを実践的に経験
  • エコシステムの進展と現実的な開発経験から得た利点と限界を明確に認識
  • エミュレータ開発は中級以上の開発者の実力向上に実質的に役立つ

1件のコメント

 
GN⁺ 2025-07-06
Hacker Newsのコメント
  • エミュレータ、仮想マシン、バイトコードインタプリタを書くのに、どの特定のプログラミング言語がより向いていると自信を持って言える人がいるのか気になる。ここでいう「より良い」とは、性能や実装ミスの減少ではなく、自分で実装して探究するときにより直感的で、何かをより学べて、実装経験そのものがやりがいがあって楽しい、という観点のこと。たとえば Erlang は分散システムの領域で明確な目的があり、その領域のドメイン知識と言語設計が一致しているので、使ってみると分散システムと Erlang 自体について深い理解が得られる。このように「機械の動作をコードで表現すること」を対象にした言語があるのか気になっている

    • システムプログラミング言語である C、C++、Rust、Zig が、個人的には最も「満足度の高い」選択であることを強調したい。これらの言語ではデータ型(例: uint8)がメモリ上のバイトにそのまま対応し、memcpy のような操作はそのまま blit 作業と同じになる。JavaScript のような言語で Number 型をビット演算用のバイトとして使い回して苦労することはほとんどない。JavaScript でエミュレータを作ると、こうした問題にすぐ直面する。もちろん、どんな言語でもグラフィック表示と十分なメモリさえサポートされていれば似たように動かせるし、結局は自分が最も慣れている言語を選ぶときに最大の楽しさを感じられる

    • Haskell は DSL とコンパイラに必要なデータ変換に優れた性能を示す。OCaml、Lisp、パターンマッチングと ADT をサポートするモダンな言語もどれも適している。Modern C++ でも variant 型などで似たことは試せるが、すっきりはしない。実際にエミュレータでゲームを動かすつもりなら C か C++ が標準的な選択。Rust もそこそこいけそうではあるが、低レベルのメモリ操作についてはよく分からない

    • エミュレータ、仮想マシン、バイトコードインタプリタを作るのに、特別により優れた言語はないという立場。配列(任意のインデックスへの定数時間アクセス)とビット演算さえあれば、実装はとても簡単になる。JIT まで考えないレベルなら、関数型言語でも配列とビット演算をサポートしている

    • sml、なかでも MLTon 方言を勧めたい。OCaml が良いとされるほとんどすべての理由を共有しているが、個人的には ML 系言語の中でより完成度の高い完成形だと評価している。OCaml で恋しくなるのは applicative functor くらいだが、これはモジュール構造が少し違うだけで大きな差ではない

    • ブラウザ内で楽しさと実験を重視するなら、Elm も良い選択肢。似たプロジェクトとして elmboy を参照するとよい

  • この記事は Ocaml だけでなく、Game Boy エミュレータの実装過程まで充実して整理していて、本当に素晴らしい資料。著者に感謝を伝えたい。ついでに、ブラウザ内でアセンブラエディタと、アセンブラ/リンカ/ローダーまで一体化した SPA によって、誰でも簡単に Gameboy ホームブリュー開発を体験できるようにするなら、組み込み開発教育によさそうだというアイデアをずっと前から持っていた

    • rgbds-live プロジェクトはこのアイデアに近く、RGBDS が組み込まれている。rgbds-live
  • もしかすると、Game Boy エミュレータでのサウンド実装に関するチュートリアルを探している人がいるかもしれない。たいていのチュートリアルはサウンドを説明しておらず、自分で実装しようとしても資料だけでは理解も実装も難しかった

    • 公式チュートリアルではないが、自分が実装した方法をまとめた 2 枚のスライド資料を共有する: スライド資料 Game Boy サウンドには 4 つのチャンネルがあり、各チャンネルは毎ティックごとに 0〜15 の値を出力する。エミュレータはそれらを足し合わせ(算術平均)、0〜255 の範囲にスケーリングしてサウンドバッファに出力する必要がある。ティックレート(4.19MHz)と音声出力(22kHz など)に合わせて、約 190 ティックごとに 1 つの値を出力する。チャンネルごとの特徴は この資料 によく整理されている。1 番、2 番チャンネルは矩形波(0/15 の繰り返し)、3 番チャンネルは任意波形(メモリ読み出し)、4 番チャンネルはノイズで、LSFR ベース。サンプルコード SoundModeX.java の参照を勧める

    • この資料 もかなり良い

    • この YouTube 動画 も参考になる

  • 本当に素晴らしい記事とクールなプロジェクトだという印象

  • デモがあまりにも速く動いているのが目につく。Throttle チェックボックスがあまり効いていない。むしろチェックを外すとさらに遅くなる感じ。Throttle を有効にすると 240fps、無効にすると 180fps。Throttle を有効にすると 1 秒が実際のエミュレータでは約 4 秒に感じられる。おそらくモニターのリフレッシュレートが 240Hz であることと関係していそう

    • おそらく requestAnimationFrame() だけを呼んでいて、deltaTime の計算が抜けているのだろう
  • 本当に美しい記事だと思う。こういう資料を共有してくれてありがとう。Rust で Game Boy エミュレータを自分で作ってみたくなったし、このブログ記事に大きく刺激を受けたのでブックマークした

  • functor と GADT の使い方が本当に見事な例。CHIP 8 や NES エミュレータと比較してみたいし、CAMLBOY を ocaml-wasm で WASM に移植してみるのも面白そう

    • js_of_ocaml の新しい WASM バックエンド(wasm_of_ocaml)があるので、すでに CAMLBOY を WASM 上で動かせるはず