概要
Go
はこれまで量を書いたことがなかったので入門にゲームボーイエミュレータを書いてみることにした。ゲームボーイである理由はたまたまよくできたゲームボーイの資料(http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf)を見つけてしまったため。
成果物
まだ基本的なカートリッジタイプしか実装できていないがそこそこ動き始めたので公開することにした。直近は対応カートリッジを増やしながら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
と聞くことが多いが、Intel8080
とZ80
のハイブリッドとも聞いたことがあって、もうちょっと詳しく知りたいと思い調べていたら以下の記事に辿り着いた。
推測も含んでいるようなので実際のところはわからないが技術面では 8080 カスタムと呼ぶべきで、政治面では Z80 カスタムと呼ぶべき
らしく面白かった。
前述したように画像処理(ファミコンで言うところのPPU
)はLR35902
に含まれているため部品点数がとても少ない。大きな部品はメモリ2つとLCDだけだ。あのスペースに押し込めるのに苦労したんだろうなと思う。
ファミコンとの違い
ゲームボーイ、ファミコンのイケてない点がいろいろ改善されているように見えて、そういう観点で見ると面白かった
— bokuweb (@bokuweb17) July 20, 2019
こんなツイートしたところ反応があったので書いてみることにする。ただ、ファミコン開発時の技術面やコスト面での限界もあっただろうし改善
というとすこし大げさな気もするので気になった違い
を挙げてみたいと思う。いざまとめてみるとそんなに量も無い気がするけど。
タイマーペリフェラルがある
逆にファミコンに無いということに驚くかもしれませんがファミコンにはタイマーがなかった。ので1秒待つ
ような処理が必要になった場合、各命令がどのくらい時間を食うのか計算してwhile
文などで待つ必要あったと思う。辛い。 *1
*1 Vblank割り込みをカウントアップすれば簡易タイマーになるのでは。というコメントをいただきました。
ゲームボーイには簡素なものながらタイマーがついており周波数は4種類から選べるし、もちろん割り込みもついている。
タイマーは指定周期経過するごとにカウンタをインクリメントしていき1byteのレジスタがオーバーフローする際に割り込みがかかるようになっている。なのでこのカウンタを読むことでどのくらい時間が経過したか測定することができる。
この機能を使うことによりゲームボーイではエミュレータのCPU
の実行タイミングが正しいか計測できる。そのためファミコンではなかったタイミングが正しいかどうかテストするROM
がたくさんあった。(タイミングまで正確にエミュレートするのは難しくてこの手のテストROM
は全然PASSできていない)
エミュレータとしては多少タイミングがずれていても動くのでどこまで頑張るかは実装者のやる気次第。
シリアル通信ができる
これもタイマー同様ファミコンに無いということに驚くが、ゲームボーイではシリアル通信ができる。とても原始的な作りになっていて制御すべきレジスタは2個だけ。0xFF01
に送信データを書いたあと0xFF02
に書くと送信されるっぽい。ぽい、というのはあまり真面目に実装していなくて0xFF01
を標準出力に接続するだけでエミュレータとしては十分だからだ。
テストROM
によってはテスト結果をシリアルに吐いてくれるので描画の実装がまだできていなくてもCPU
の命令テストなどが行える。これはエミュレータを作る側としては非常に助かる。ただ自分は最後の最後までシリアルに出力される文字が化けていてこの恩恵に預かれなかったが。。。
ここまで書いて気づいたんだが、このシリアルポートはゲームボーイ同士の通信に使われているポートらしい。ゲームボーイで通信ケーブルを使った覚えがないのですっかり頭から抜け落ちていた。
なぜかテトリスは0x55
をDr.マリオは0x60
を連続して出力してくるのでバグっているのかと思っていたんだけど、多分通信相手を探してるんだそうな。
ここのプロトコルがわからないが解析してWebSocket
にでもつなげばネット対戦ができるかもしれない。
Hblank割り込みや指定ラインでの割り込みがある
Hblank
とはあるラインを描画してから次のラインの描画が開始するまでのブランク期間で、ゲームボーイはHblank
での割り込みや指定したラインが描画された際(正確にはラインバッファに展開された際かもしれない)に割り込みをかけることができる。
このタイミングを知ることで様々なことが可能になるが、代表的なものはやはりラスタスクロール
じゃないかと思う。ラスタスクロール
は画面描画の途中でスクロール量を調整することで部分的なスクロールなど様々な表現が可能となる。
まだおかしいがだいたい動いた pic.twitter.com/Mk5Z00j0Cu
— bokuweb (@bokuweb17) August 1, 2019
たとえばこのようにスコアやタイムの表記だけ固定してゲーム部分のみスクロールさせることができる。
実際にこのカートリッジがどうやってるかまでは見てないが恐らく指定ラインで割り込みをかけてスクロール値を変更するなどすれば実現できると思う。
じゃあそれらのタイミングを取れないファミコンはどのようにラスタスクロール
を実現しているかというと0爆弾
という謎仕様がある。これはスプライト用RAM
の先頭に格納されたスプライトがラインバッファ上に展開された際にある特定のレジスタにフラグが立つというものだ。
ちくちょう。。。スコアがスクロールしやがる。。。。 pic.twitter.com/LgJ80Bpmnd
— bokuweb (@bokuweb17) January 15, 2018
たとえばこれ。これは失敗例で意図しないとこまでスクロールしてるんだけど、そのおかげ(?)で0爆弾
であるスプライトを目視することができる。本来コインが表示される位置にあるコインの影のような黒いスプライトだ。バグによりコインが流れていってしまってわかりにくいが。
このスプライト描画完了を検出してからスクロールを開始することによりスコアやタイムは画面上部に固定したままゲーム部分をスクロールすることができている。
このあたりのスクロールに関しては以下の記事も面白い。
ゼルダの伝説ではヘッダを固定したまま縦スクロールがありそれをどのように実装しているかという話。
0爆弾
というトリッキーな仕様をシンプルな割り込みで解決できるようになったのは改善といっても良さそうだ。
ウィンドウという機能がある
これは最初説明を見てもなんのことかわからなかったが以下の記事を読んで氷解した。
簡単に言うと背景の上にもう一枚背景をかぶせるようなことができる機能だ。ただ、透過処理ができるわけではないので8x8ピクセルの単位で完全上書きになってしまう。
使用例としては以下のようなものが挙げられる。
GAME OVERの帯が下から上がってくるのはwindowという機能らしい。なんでもない機能に見えるけどファミコンでこれは実現できない気がする pic.twitter.com/KmsbmX99sx
— bokuweb (@bokuweb17) August 4, 2019
下から上がってくるGAME OVER
の帯はまさにウィンドウ機能が使用されている。大した機能ではないように見えるが、ファミコンではこの挙動を実現できない*2んじゃないかと思っている。ファミコンではスプライトを並べて表現するか、背景を書き換えるかどちらかの手法になるが、スプライトは横方向最大8個しか並べられないし、背景をこのような速度で書き換えることはできないからだ。
*2 id:u_mid さんの指摘で GAME OVERの帯も不可能でない
との指摘をいただきました。確かにタイミングの制御はかなりシビアだけどhblank
のタイミングをうまく捉えてscrollを駆使したら行けるのかなーという気がしてきました。ただ少なからずグリッチが出るんじゃないかな...
で、話は戻ってゼルダの伝説のヘッダ固定上下スクロールもひょっとしてこのウィンドウ機能があればシュッと解決できるんじゃないかと思ったりしてる。なので地味だけど画期的な機能だと思う。
画像処理機能がCPUと同じパッケージに入ってる
これは半導体の集積度の向上やゲームボーイの筐体のサイズの都合上自然とこうなるべきという感じではあるが、ゲームボーイでは画像処理機能がCPU
と同じパッケージに入ってる。
エミュレータ作成者から見て、何が嬉しいかと言うとCPU
からVRAM
に直接アクセスできることだろう。
ファミコンではVRAM
はPPU
(画像処理IC)に接続されていたためCPU
からは直接アクセスすることができない。(VRAM
に画像を配置するのはCPU
の仕事であるにも関わらず。)
どうするかと言うとPPU
内のアドレスレジスタ
にアクセスするVRAM
のアドレスを書いてからデータレジスタ
にアクセスすることでようやくVRAM
を読んだり書いたりできる。
ここで重要な点はPPU
内のデータレジスタは初回ゴミデータが読めるので読み捨てる必要がある点だ。これはファミコン開発サイトNESDEV
にもハマりポイントして紹介されており幾多のエミュレータ作者を陥れた仕様だろう。これをちゃんと実装しないと漏れなくスーパーマリオブラザーズの空が黒くなる。
これはCPU
側のバスとPPU
側のバスが非同期だからで、非同期のバス間でやりとりするにはFIFO
をつけたりDual port RAM
を使ったりすることが多いと思う。が、当時Dual port RAM
なんてものは無かったかもしれないし仕様面、コスト面からも使う必然性もないのでFIFO
が入ったんだろう。なので初回はゴミデータになる。
ファミコンにはこんな事情があったのでやはり、VRAM
へのアクセスがシンプルになるのは嬉しい。
実装過程
完全な理解
ゲームボーイ完全に理解した #bokuwebnes pic.twitter.com/X0idXw7rze
— bokuweb (@bokuweb17) June 21, 2019
エミュレータ実装の第一歩はHello World
またはそれに相当するROM
を探しコードを読むことだと思う。今回は以下のものを使用した。
ブートROM
これもファミコンとの違いの一つではあるのだけど、ゲームボーイはブートROM
を持っている。0x0000~0x0100
がブートROM
の領域なんだけど一度起動後は0x000~0x0100
はカートリッジのROM
領域に再マッピングされるという仕様らしい。そういう挙動不安になる。
ロゴおかしい #bokuwebnes pic.twitter.com/BWSgSRq20r
— bokuweb (@bokuweb17) June 22, 2019
ロゴはでたけどなんかぎざぎざしてるのと上から落ちてこない... #bokuwebnes pic.twitter.com/XqjBmSiTaE
— bokuweb (@bokuweb17) June 22, 2019
表示はできたもののスクロールが実装できていないので中央に居座っている。
降ってきた!けどなんかホラーっぽい.... #bokuwebnes pic.twitter.com/noSVVbJv5s
— bokuweb (@bokuweb17) June 22, 2019
スクロールが絡む座標計算は何度実装しても難しくてすんなりいった試しがない。y方向の座標計算をミスっていたためホラーっぽい仕上がりに。
直った。色をLCDっぽく修正。 pic.twitter.com/2F1PNr1lKo
— bokuweb (@bokuweb17) June 24, 2019
完成。自分にとってゲームボーイは緑っぽいLCDの色のイメージなのでわざわざこの色に修正した。
CPUテスト
CPU
テストROM
はここにある。こいつはシリアルにも結果を出力してくれる便利なやつ。
CPU test romがようやく動くようになった pic.twitter.com/Xxzfz4iFoX
— bokuweb (@bokuweb17) July 1, 2019
ようやく全部通った pic.twitter.com/qOkR7kuGXe
— bokuweb (@bokuweb17) July 13, 2019
動かすには苦労した。デフォルトのカートリッジタイプではなくRAM
を持ったカートリッジタイプでRAM
にプログラムをコピーしてから実行するような作りになっていたためすんなりとはいかなかった。
ただ、このROM
は個別実行できたりかなり重宝した。難点としてはアセンブラが結構複雑で読んでもどこで落ちているのかわからないこともしばしば。
Opus5
謎のシューティング風ゲーム。敵もいなければ攻撃もできない主にスクロールとキー入力確認用ROM
と言う感じ。またはじめてスプライトが登場したのでここで実装した。たしかスプライト用DMA
も使用していてそれも合わせて実装した気がする。
シューティングっぽいなにか pic.twitter.com/Uw09pSlehY
— bokuweb (@bokuweb17) July 16, 2019
ugoita pic.twitter.com/MwRY6rZQen
— bokuweb (@bokuweb17) July 19, 2019
ゲームボーイの解像度は160×144なので4kディスプレイで遊ぶとこうなる。早くスケール機能をつけないといけない。
4kで遊ぶとこうなる pic.twitter.com/FsxTS8bWx4
— bokuweb (@bokuweb17) July 19, 2019
テトリス
テトリスはなぜかすんなり動いて完成した気になってた。
テトリス動いた。大体完成では。wasmにするぞ! pic.twitter.com/eHtxRWKmlt
— bokuweb (@bokuweb17) July 19, 2019
スーパーマリオランド
これが全然だめだった。一番のミスはタイルIDの取り違い。昔のゲームはメモリ容量が少ないためVRAM
にピクセルデータを直接持たせるのではなくスプライトデータを指し示すタイルIDを敷き詰めることになる。が、ゲームボーイはこれが負の値になる場合があるようでこれにハマッた。結局この値の持ち方にどのような利点があるのかさっぱりわからず。タイルIDがずれた分不思議な世界が描画されてた。
本来マリオであるべき場所がハエだしG反転しながら襲い掛かってくるすばらしい世界観 pic.twitter.com/Ri1smO7Ya0
— bokuweb (@bokuweb17) July 30, 2019
マリオがハエだしG
が反転しながら襲ってくる。
すごい pic.twitter.com/8h9g7HlQw0
— bokuweb (@bokuweb17) August 5, 2019
マリオがたくさん。
謎すぎるの撮れた pic.twitter.com/lwC0HN3is6
— bokuweb (@bokuweb17) July 30, 2019
反転しながら襲ってくるG
を避け3
を手にするとやっぱりハエになる。
できたと思ったけど死ぬ瞬間2つに割れる pic.twitter.com/ujQD1iTxjD
— bokuweb (@bokuweb17) July 30, 2019
2つに割れる。
まだおかしいがだいたい動いた pic.twitter.com/Mk5Z00j0Cu
— bokuweb (@bokuweb17) August 1, 2019
これから
先にも書いたとおり、ひとまずはWebAssembly
対応して遊んでみる。
あともう少し技術的詳細を書いた記事はどこかのタイミングで書こうかとは思ってる。けど腰は重そう。
そういえば以前ファミコンエミュレータを書くのをおすすめしたけど、ゲームボーイのほうがハマりポイントが少なくてもっとおすすめ。気になる方はぜひ。