undefined

bokuweb.me

ゲームボーイエミュレータをGo言語で書いた

概要

Goはこれまで量を書いたことがなかったので入門にゲームボーイエミュレータを書いてみることにした。ゲームボーイである理由はたまたまよくできたゲームボーイの資料(http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf)を見つけてしまったため。

成果物

github.com

まだ基本的なカートリッジタイプしか実装できていないがそこそこ動き始めたので公開することにした。直近は対応カートリッジを増やしながらWebAssemblyを吐けるようにしたい。

ゲームボーイの基本仕様

項目 概要
CPU LR35902 4.19MHz 8bit
RAM 8kB
VRAM 8KB
ROM 256k~32MBit
Display 4階調モノクロ、160×144ドット
スプライト 8×8 最大40個表示 / 1ライン上に 最大10個表示
背景 256×256ドット
ウィンドウ機能 後述
サウンド 矩形波2ch+波形メモリ音源1ch+ノイズ1ch
通信ポート シリアル通信ポート搭載
割込み機能 パッド入力割込み、シリアル通信割込み、タイマー割込み、LCDC割込み、Vblank割込み

CPUはシャープ製のLR35902でこの中には画像処理や音声の機能も含まれている。コアはカスタムZ80と聞くことが多いが、Intel8080Z80のハイブリッドとも聞いたことがあって、もうちょっと詳しく知りたいと思い調べていたら以下の記事に辿り着いた。

www.wizforest.com

推測も含んでいるようなので実際のところはわからないが技術面では 8080 カスタムと呼ぶべきで、政治面では Z80 カスタムと呼ぶべきらしく面白かった。

前述したように画像処理(ファミコンで言うところのPPU)はLR35902に含まれているため部品点数がとても少ない。大きな部品はメモリ2つとLCDだけだ。あのスペースに押し込めるのに苦労したんだろうなと思う。

ファミコンとの違い

こんなツイートしたところ反応があったので書いてみることにする。ただ、ファミコン開発時の技術面やコスト面での限界もあっただろうし改善というとすこし大げさな気もするので気になった違いを挙げてみたいと思う。いざまとめてみるとそんなに量も無い気がするけど。

タイマーペリフェラルがある

逆にファミコンに無いということに驚くかもしれませんがファミコンにはタイマーがなかった。ので1秒待つような処理が必要になった場合、各命令がどのくらい時間を食うのか計算してwhile文などで待つ必要あったと思う。辛い。 *1

*1 Vblank割り込みをカウントアップすれば簡易タイマーになるのでは。というコメントをいただきました。

ゲームボーイには簡素なものながらタイマーがついており周波数は4種類から選べるし、もちろん割り込みもついている。

タイマーは指定周期経過するごとにカウンタをインクリメントしていき1byteのレジスタがオーバーフローする際に割り込みがかかるようになっている。なのでこのカウンタを読むことでどのくらい時間が経過したか測定することができる。

この機能を使うことによりゲームボーイではエミュレータのCPUの実行タイミングが正しいか計測できる。そのためファミコンではなかったタイミングが正しいかどうかテストするROMがたくさんあった。(タイミングまで正確にエミュレートするのは難しくてこの手のテストROMは全然PASSできていない)

エミュレータとしては多少タイミングがずれていても動くのでどこまで頑張るかは実装者のやる気次第。

シリアル通信ができる

これもタイマー同様ファミコンに無いということに驚くが、ゲームボーイではシリアル通信ができる。とても原始的な作りになっていて制御すべきレジスタは2個だけ。0xFF01に送信データを書いたあと0xFF02に書くと送信されるっぽい。ぽい、というのはあまり真面目に実装していなくて0xFF01を標準出力に接続するだけでエミュレータとしては十分だからだ。

テストROMによってはテスト結果をシリアルに吐いてくれるので描画の実装がまだできていなくてもCPUの命令テストなどが行える。これはエミュレータを作る側としては非常に助かる。ただ自分は最後の最後までシリアルに出力される文字が化けていてこの恩恵に預かれなかったが。。。

ここまで書いて気づいたんだが、このシリアルポートはゲームボーイ同士の通信に使われているポートらしい。ゲームボーイで通信ケーブルを使った覚えがないのですっかり頭から抜け落ちていた。

なぜかテトリスは0x55をDr.マリオは0x60を連続して出力してくるのでバグっているのかと思っていたんだけど、多分通信相手を探してるんだそうな。

ここのプロトコルがわからないが解析してWebSocketにでもつなげばネット対戦ができるかもしれない。

Hblank割り込みや指定ラインでの割り込みがある

Hblankとはあるラインを描画してから次のラインの描画が開始するまでのブランク期間で、ゲームボーイはHblankでの割り込みや指定したラインが描画された際(正確にはラインバッファに展開された際かもしれない)に割り込みをかけることができる。

このタイミングを知ることで様々なことが可能になるが、代表的なものはやはりラスタスクロールじゃないかと思う。ラスタスクロールは画面描画の途中でスクロール量を調整することで部分的なスクロールなど様々な表現が可能となる。

たとえばこのようにスコアやタイムの表記だけ固定してゲーム部分のみスクロールさせることができる。

実際にこのカートリッジがどうやってるかまでは見てないが恐らく指定ラインで割り込みをかけてスクロール値を変更するなどすれば実現できると思う。

じゃあそれらのタイミングを取れないファミコンはどのようにラスタスクロールを実現しているかというと0爆弾という謎仕様がある。これはスプライト用RAMの先頭に格納されたスプライトがラインバッファ上に展開された際にある特定のレジスタにフラグが立つというものだ。

たとえばこれ。これは失敗例で意図しないとこまでスクロールしてるんだけど、そのおかげ(?)で0爆弾であるスプライトを目視することができる。本来コインが表示される位置にあるコインの影のような黒いスプライトだ。バグによりコインが流れていってしまってわかりにくいが。

このスプライト描画完了を検出してからスクロールを開始することによりスコアやタイムは画面上部に固定したままゲーム部分をスクロールすることができている。

このあたりのスクロールに関しては以下の記事も面白い。

gridbugs.org

ゼルダの伝説ではヘッダを固定したまま縦スクロールがありそれをどのように実装しているかという話。

0爆弾というトリッキーな仕様をシンプルな割り込みで解決できるようになったのは改善といっても良さそうだ。

ウィンドウという機能がある

これは最初説明を見てもなんのことかわからなかったが以下の記事を読んで氷解した。

wentwayup.tamaliver.jp

簡単に言うと背景の上にもう一枚背景をかぶせるようなことができる機能だ。ただ、透過処理ができるわけではないので8x8ピクセルの単位で完全上書きになってしまう。

使用例としては以下のようなものが挙げられる。

下から上がってくるGAME OVER の帯はまさにウィンドウ機能が使用されている。大した機能ではないように見えるが、ファミコンではこの挙動を実現できない*2んじゃないかと思っている。ファミコンではスプライトを並べて表現するか、背景を書き換えるかどちらかの手法になるが、スプライトは横方向最大8個しか並べられないし、背景をこのような速度で書き換えることはできないからだ。

*2 id:u_mid さんの指摘で GAME OVERの帯も不可能でない との指摘をいただきました。確かにタイミングの制御はかなりシビアだけどhblankのタイミングをうまく捉えてscrollを駆使したら行けるのかなーという気がしてきました。ただ少なからずグリッチが出るんじゃないかな...

で、話は戻ってゼルダの伝説のヘッダ固定上下スクロールもひょっとしてこのウィンドウ機能があればシュッと解決できるんじゃないかと思ったりしてる。なので地味だけど画期的な機能だと思う。

画像処理機能がCPUと同じパッケージに入ってる

これは半導体の集積度の向上やゲームボーイの筐体のサイズの都合上自然とこうなるべきという感じではあるが、ゲームボーイでは画像処理機能がCPUと同じパッケージに入ってる。

エミュレータ作成者から見て、何が嬉しいかと言うとCPUからVRAMに直接アクセスできることだろう。 ファミコンではVRAMPPU(画像処理IC)に接続されていたためCPUからは直接アクセスすることができない。(VRAMに画像を配置するのはCPUの仕事であるにも関わらず。)

どうするかと言うとPPU内のアドレスレジスタにアクセスするVRAMのアドレスを書いてからデータレジスタにアクセスすることでようやくVRAMを読んだり書いたりできる。

ここで重要な点はPPU内のデータレジスタは初回ゴミデータが読めるので読み捨てる必要がある点だ。これはファミコン開発サイトNESDEVにもハマりポイントして紹介されており幾多のエミュレータ作者を陥れた仕様だろう。これをちゃんと実装しないと漏れなくスーパーマリオブラザーズの空が黒くなる。

これはCPU側のバスとPPU側のバスが非同期だからで、非同期のバス間でやりとりするにはFIFOをつけたりDual port RAMを使ったりすることが多いと思う。が、当時Dual port RAMなんてものは無かったかもしれないし仕様面、コスト面からも使う必然性もないのでFIFOが入ったんだろう。なので初回はゴミデータになる。

ファミコンにはこんな事情があったのでやはり、VRAMへのアクセスがシンプルになるのは嬉しい。

実装過程

完全な理解

エミュレータ実装の第一歩はHello Worldまたはそれに相当するROMを探しコードを読むことだと思う。今回は以下のものを使用した。

github.com

ブートROM

これもファミコンとの違いの一つではあるのだけど、ゲームボーイはブートROMを持っている。0x0000~0x0100がブートROMの領域なんだけど一度起動後は0x000~0x0100はカートリッジのROM領域に再マッピングされるという仕様らしい。そういう挙動不安になる。

表示はできたもののスクロールが実装できていないので中央に居座っている。

スクロールが絡む座標計算は何度実装しても難しくてすんなりいった試しがない。y方向の座標計算をミスっていたためホラーっぽい仕上がりに。

完成。自分にとってゲームボーイは緑っぽいLCDの色のイメージなのでわざわざこの色に修正した。

CPUテスト

CPUテストROMはここにある。こいつはシリアルにも結果を出力してくれる便利なやつ。

github.com

動かすには苦労した。デフォルトのカートリッジタイプではなくRAMを持ったカートリッジタイプでRAMにプログラムをコピーしてから実行するような作りになっていたためすんなりとはいかなかった。

ただ、このROMは個別実行できたりかなり重宝した。難点としてはアセンブラが結構複雑で読んでもどこで落ちているのかわからないこともしばしば。

Opus5

謎のシューティング風ゲーム。敵もいなければ攻撃もできない主にスクロールとキー入力確認用ROMと言う感じ。またはじめてスプライトが登場したのでここで実装した。たしかスプライト用DMAも使用していてそれも合わせて実装した気がする。

ゲームボーイの解像度は160×144なので4kディスプレイで遊ぶとこうなる。早くスケール機能をつけないといけない。

テトリス

テトリスはなぜかすんなり動いて完成した気になってた。

スーパーマリオランド

これが全然だめだった。一番のミスはタイルIDの取り違い。昔のゲームはメモリ容量が少ないためVRAMにピクセルデータを直接持たせるのではなくスプライトデータを指し示すタイルIDを敷き詰めることになる。が、ゲームボーイはこれが負の値になる場合があるようでこれにハマッた。結局この値の持ち方にどのような利点があるのかさっぱりわからず。タイルIDがずれた分不思議な世界が描画されてた。

マリオがハエだしGが反転しながら襲ってくる。

マリオがたくさん。

反転しながら襲ってくるGを避け3を手にするとやっぱりハエになる。

2つに割れる。

これから

先にも書いたとおり、ひとまずはWebAssembly対応して遊んでみる。 あともう少し技術的詳細を書いた記事はどこかのタイミングで書こうかとは思ってる。けど腰は重そう。

そういえば以前ファミコンエミュレータを書くのをおすすめしたけど、ゲームボーイのほうがハマりポイントが少なくてもっとおすすめ。気になる方はぜひ。