Try   HackMD

AQBoy: Yet Another Game Boy Emulator 開発記

Go言語を使ってゲームボーイ(GB)のエミュレータ AQBoyを書いて、Tobu Tobu Girlを動かしました。 実装はいつも通りGitHubに上がっています。

きっかけ

昨年末のアドベントカレンダーで、linoscopeさんが書いた「OCaml でゲームボーイエミュレータを書いた話」を読んだのが直接のきっかけです。 ちょうど年末の大掃除で、昔遊んでいたポケットモンスター ピカチュウが実家から出てきたので、 これを動かすべくGBエミュレータの開発を始めました。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
実家の大掃除で見つけたゲームボーイアドバンスとゲームカセットと周辺機器。

開発の流れ

先人に習って、まず "The Ultimate Game Boy Talk" を見て、設計の検討をつけます。 この講演は1時間でGBの仕組みについてあらかた話してしまうというもので、 トークそのものもユーモアにとんでいて面白いのでおすすめです。特にCPUアーキテクチャの くだりが秀逸です。

この動画だけではエミュレータを書くには不足なので、続いて詳細な資料にあたります。

  • Pan Docs:GBに関する技術的詳細がまとめられています。基本的にはこれを読みながらエミュレータを書くことになります。ちなみに元々はWikiでしたが、移行したようです。
  • Gameboy CPU (LR35902) instruction set:オペコード一覧表。CPUの実装はここを見ながらすることになりますが、たまに間違っています。
  • Game Boy Programming Manual:Nintendo in Americaが出した(?)公式の(?)仕様書。細かい処理などを確認するときに便利です。

これらを見ながら、まずCPUエミュレーションの部分から作り始めます。 基本的には、ROMから次に実行する命令のopcodeを読み込み、 その値をもとに巨大switch文で分岐して対応する命令を実行するだけでうまく動きます。 以前、Cコンパイラプロセッサを書いた経験が幸いして、この部分ではそれほど苦労しませんでした。 ただ、現代のCPUではあまり見ない命令(DAA命令など)もあって、 そのような命令は実装に手間取りました。 巨大switch文を書く過程で、 Goではswitchの各casebreakが必要ない(フォールスルーさせたいときは fallthroughキーワードを使う)といった仕様を知って驚いたりもしました。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
CPUエミュレーションの開発風景。各実行命令ごとにレジスタの値を出力させている。

CPUエミュレーションの実装には、以下が役に立ちました。

  • BGB GameBoy Emulator:有名なGBエミュレータの一つで、デバッガがかなり充実しています。自作エミュレータの挙動が怪しいときはBGBでの挙動を参考にしました。
  • Blargg's test roms:CPU emulationが正しく実装されているかを確認するためのROMで、cpu_instrsinstr_timingが通ることを確認しました。まだこの段階では画面表示が無いので、実行したあとのレジスタの値がエミュレータと一致するか、そもそも実行が最後まで走り切るかなどを確認しました。
  • GameBoy - Help With DAA instruction - nesdev.org:GBにはDAAという特殊な命令があり、2進化10進数を扱うことを可能にしています。この命令の挙動はPan Docsにもあまり情報が無いのですが、このnesdev.orgの議論では、DAA命令をどのように実装するかについて記載があります。

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_instrsinstr_timingがどの命令が正しく実装されていないかが分かるので便利です。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cpu_instrsinstr_timingでの画面表示例。Passedはテストがパスしたことを意味し、パスしないと何が間違っているのかを表示してくれる。

PPUの詳細な動作確認にはmattcurrie/dmg-acid2を用いました。 このテスト用ROMは顔の表示によってPPUの挙動が正しいかどうかを判断できるように構成されており、 例えば鼻が欠けていればobject paletteの実装が間違っていることが分かります。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
dmg-acid2を用いたPPU開発の経過。左がPPU開発初期のもので、一番右はPPUの全ての要素を実装したあとのもの。

このあたりで、Tobu Tobu Girlという GBゲームのROMを読み込ませてデバッグを始めました。いくつかバグを潰すと、 次のように画面表示が比較的まともに行われるようになりました。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Tobu Tobu Girlが動作。

ゲームで遊ぶためにJoypadを実装します。GBのJoypadは単純で、 A, B, Start, Select, Up, Right, Down, Leftを適当なキーに割り当てて 入力を検知した上で、Pan Docsに書かれている通りにMMIOに追加すると動作します。

ちなみにJoypadのデバッグのためにTobu Tobu Girlで遊んでみるとむちゃくちゃ難しくて (あるいは私の操作が下手過ぎて)、いまだに一面をクリアできていません。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
実装した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エミュレータの開発は下記のような様々な方のコードを参考にしました。

  • linoscope/CAMLBOY
    • linoscopeさんのOCamlでの実装。
    • Busで各コンポーネントをまとめるデザインはこの実装を参考にしました。OCamlではファンクタがこの構造にうまくハマっています。
  • keichi/gbr
    • Keichi TakahashiさんのRustでの実装。
    • 対応する記事である「ゲームボーイのエミュレータを自作した話」も参考にしました。特にBlargg's test romsのうちcpu_instrsとinstr_timingをパスできるようにするという方針を参考にしました。

やり残したこと

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ほしい。


GBエミュレータ開発こぼれ話

本文に入れられなかった話をランダムにいくつか。本文に出てこない話で、Pan Docsとかを見るとすぐに分かる内容を説明せずに書いています。

  • GBに関わる開発の話(エミュレータだけではなくゲーム開発とか)はAwesome Game Boy Developmentにある。
  • opcode表の間違いについて、自分が気づいたもの:
    • 0xe2, 0xf2のopcode sizeは1 byte
    • BITの(HL)のサイクルは16ではなくて12
    • ZNHC flagが間違っている命令があったはず(どれか忘れた……)
  • Fレジスタを書き換える場合(POP AFとか)、下位4bitが常に0であることに注意。
  • DAA命令は2進化10進を普通にADDSUBで加算・減算した後の値を2進化10進に戻すための命令。例えば0x99 + 0x99 -> 0x98 (i.e., 99+99=100+98)を計算したいときに、0x99 + 0x99 = 0x32ADDで計算したあとにDAA命令を使う。この場合DAA命令は(half-)carry flagを見ながらこれを0x98に補正するために0x66を足す。本文にも貼ったこのスレッドにはDAA命令を実装するための疑似コードもある。
  • JP (HL)命令は(HL)ではなくHLに飛ぶ
  • PPUのH/W実装はFIFOバッファを用意する複雑なものだが、software emulationの場合はOAM Searchが終わった段階で一行分全て描画してしまえばよい。VRAMにアクセスできないという制約があるので、これで動作は一貫する。
  • 通常のinput, update, drawというループに合わせてemulationを行うために、updateは画面描画が一回終わるまでCPU emulationを実行すると良い。具体的には(456×154)サイクルCPUを実行する。
  • Objectのtransparentなpixelはcolor paletteのindexが0。colorが0ではない。
  • IME=0の場合でもhalt状態が解除される場合がある。割り込みのrequest flagとenable flagが立てばHALTが終わる。ただし割り込みハンドラは動かないで、HALT命令の次のPCから実行が再開する。
  • ROMに書き込みの命令を送ると、それがMBCへの通信として扱われてROM/RAMのbank switchができる。
  • go-sdl2でaudioを扱う際には、bufferに波形データを詰めるようなcallback関数を登録する必要がある。このcallback関数にuserdataを渡す場合、Cgoの「Goのポインタを指すGoのポインタを関数の引数に渡してはならない」というルールに抵触しうるが、mattn/go-pointerを利用することで回避できる。詳細はmattnさんの記事を参照のこと。
  • PPUがdisableされたときはPPUの内部バッファの更新をしてはいけないし、enableされたときにはlyを0に戻さなければならない。このあたりの挙動はCAMLBOYが参考になる。ただしAQBoyはここまで詳細に実装していない。
  • Ebitenのオーディオ処理は内部バッファがsampling rateの半分のサイズあるので、先頭から0.5秒くらい遅延してしまう。回避策は見つけられていない。
  • Tobu Tobu Girlの一面は未だにクリアできていない。知り合いにライブ配信をしながらTobu Tobu Girlの一面を30分プレイしたが、かなり良いところまで行くも結局クリアできなかった。