Go言語を使ってゲームボーイ(GB)のエミュレータ AQBoyを書いて、Tobu Tobu Girlを動かしました。 実装はいつも通りGitHubに上がっています。
昨年末のアドベントカレンダーで、linoscopeさんが書いた「OCaml でゲームボーイエミュレータを書いた話」を読んだのが直接のきっかけです。 ちょうど年末の大掃除で、昔遊んでいたポケットモンスター ピカチュウが実家から出てきたので、 これを動かすべくGBエミュレータの開発を始めました。
先人に習って、まず "The Ultimate Game Boy Talk" を見て、設計の検討をつけます。 この講演は1時間でGBの仕組みについてあらかた話してしまうというもので、 トークそのものもユーモアにとんでいて面白いのでおすすめです。特にCPUアーキテクチャの くだりが秀逸です。
この動画だけではエミュレータを書くには不足なので、続いて詳細な資料にあたります。
これらを見ながら、まずCPUエミュレーションの部分から作り始めます。
基本的には、ROMから次に実行する命令のopcodeを読み込み、
その値をもとに巨大switch
文で分岐して対応する命令を実行するだけでうまく動きます。
以前、Cコンパイラやプロセッサを書いた経験が幸いして、この部分ではそれほど苦労しませんでした。
ただ、現代のCPUではあまり見ない命令(DAA命令など)もあって、
そのような命令は実装に手間取りました。
巨大switch
文を書く過程で、
Goではswitch
の各case
にbreak
が必要ない(フォールスルーさせたいときは
fallthrough
キーワードを使う)といった仕様を知って驚いたりもしました。
CPUエミュレーションの実装には、以下が役に立ちました。
cpu_instrs
とinstr_timing
が通ることを確認しました。まだこの段階では画面表示が無いので、実行したあとのレジスタの値がエミュレータと一致するか、そもそも実行が最後まで走り切るかなどを確認しました。CPUの処理中には、ROM・RAMとの入出力や、Memory-Mapped IO(MMIO)の実装のために、 Memory Management Unit(MMU)が必要になります。 そこでGoのpackage機能を使ってCPUとMMUでパッケージを分けて構成したいところですが、 CPUからMMUを参照し、MMUからCPUを参照すると、パッケージ間で循環参照に陥ってしまって、 Goプログラムがコンパイルできなくなってしまいます。そこでAQBoyでは、 CPUやMMUのインタフェースだけを保持するようなBusという新しいパッケージを作成し、 このパッケージを間に挟んで各パッケージを参照することによって解決しました。
続いてPixel Processing Unit(PPU)を作ります。PPUは画面表示を司っている部分で、
走査線(scanline)が画面の上部から下部に進みながら描画を行います。
実際の画面表示にはveandco/go-sdl2を使用しました。
画面が出るようになると、上記のcpu_instrs
やinstr_timing
がどの命令が正しく実装されていないかが分かるので便利です。
cpu_instrs
やinstr_timing
での画面表示例。Passed
はテストがパスしたことを意味し、パスしないと何が間違っているのかを表示してくれる。PPUの詳細な動作確認にはmattcurrie/dmg-acid2を用いました。 このテスト用ROMは顔の表示によってPPUの挙動が正しいかどうかを判断できるように構成されており、 例えば鼻が欠けていればobject paletteの実装が間違っていることが分かります。
このあたりで、Tobu Tobu Girlという GBゲームのROMを読み込ませてデバッグを始めました。いくつかバグを潰すと、 次のように画面表示が比較的まともに行われるようになりました。
ゲームで遊ぶためにJoypadを実装します。GBのJoypadは単純で、 A, B, Start, Select, Up, Right, Down, Leftを適当なキーに割り当てて 入力を検知した上で、Pan Docsに書かれている通りにMMIOに追加すると動作します。
ちなみにJoypadのデバッグのためにTobu Tobu Girlで遊んでみるとむちゃくちゃ難しくて (あるいは私の操作が下手過ぎて)、いまだに一面をクリアできていません。
最後に、Audio Processing Unit(APU)を作り音を出します。 The Ultimate TalkやPan DocsにはGBのAPUの構造について詳細な説明がありますが、 エミュレータを開発する上で一番問題になるのは、これをゲームエンジン(私の場合はSDL2)と どのように接続するかという部分で、ここについてはThe Ultimate Talk・Pan Docsのいずれも 示唆を与えてくれません。 このあたりはGBエミュレータ自作界隈でも知られた難関ポイントのようで、過去の個人作GBエミュレータでもAPUが搭載されていないものが多くあります。
今回の実装では、2021年3月にNight Shadeさんが公開した Game Boy Sound Emulation | NightShade's Blog という記事を参考にしました。このサイトではGBのAPUの構造と、それをエミュレーションするための疑似コードが掲載されており、そのままコードに書き起こすことができます。 さらに、APUとゲームエンジンとを接続する方法についても説明があり、参考になりました。
最終的に、冒頭に貼ったようなエミュレータが完成しました。
GBエミュレータの開発は下記のような様々な方のコードを参考にしました。
Bus
で各コンポーネントをまとめるデザインはこの実装を参考にしました。OCamlではファンクタがこの構造にうまくハマっています。linoscopeさんの「OCaml でゲームボーイエミュレータを書いた話」では、 json_of_ocamlを使用してOCamlをWASMに変換し、ブラウザ上でGBエミュレータを動作させることに成功していました。 AQBoyでも、hajimehoshi/ebitenを使用することでブラウザで動作させることができるはずなのですが、この記事の執筆時点ではまだ成功していません。 一応、Ebitenへの移植はすでに成功していて、WASMへの変換もできているのですが、 それをブラウザで動作させようとするとFPSが8程度しか出ないような状況です。 いろいろと調べた限りでは単純にAQBoyの動作が非効率的であるためのようなので、 プロファイリングを真面目に行えば動作する可能性があるのかなと思っています。
それから、冒頭で述べたように元々はポケットモンスター ピカチュウを動かすべく AQBoyの開発を始めたのですが、残念ながらまだ動かせていません。 これは理由がはっきりしていて、カセットからROMデータを吸い出すための ROM吸出し器が思ったよりも高価だったためです(Amazonで7,000円とか)。 知り合いに相談したところ自作を勧められましたが、 電子工作をほとんどやったことがなく家には半田ごても無いので、 自作するにしても色々と準備してからになるかなと思っています。
Twitterのタイムラインにポケットモンスター アルセウスのプレイ動画とか漫画とかが めっちゃ流れてきておもしろそうだなーって言っています。Nintendo Switchほしい。
本文に入れられなかった話をランダムにいくつか。本文に出てこない話で、Pan Docsとかを見るとすぐに分かる内容を説明せずに書いています。
F
レジスタを書き換える場合(POP AF
とか)、下位4bitが常に0であることに注意。DAA
命令は2進化10進を普通にADD
やSUB
で加算・減算した後の値を2進化10進に戻すための命令。例えば0x99 + 0x99 -> 0x98
(i.e., 0x99 + 0x99 = 0x32
をADD
で計算したあとにDAA
命令を使う。この場合DAA
命令は(half-)carry flagを見ながらこれを0x98に補正するために0x66を足す。本文にも貼ったこのスレッドにはDAA
命令を実装するための疑似コードもある。JP (HL)
命令は(HL)
ではなくHL
に飛ぶ