ゲームボーイのエミュレータをGoで作った話
前置き
この記事は Qiita から移行したものです。
1か月ほど前(2019年11月時点)にbokuwebさんのゲームボーイエミュレータをGo言語で書いたというブログ記事を見ました。
ゲームボーイ世代(正確にはGBC)であったのとGOで何かしたいと思っていたのもあって、僕もゲームボーイのエミュレータに取り掛かってみることにしました。
できあがったもの
ゲームボーイカラーにも対応しました
Github
よかったらスターお願いします!
ポケモンクリスタル
夢をみる島
このエミュレータの特徴
60fpsで動作
実機と同じ60fpsで動作します CPU使用率も頑張って抑えているため低スペックなノートパソコンでも60fpsでプレイが可能です
サウンドのサポート
ここだけgoboyを参考に(というかほぼほぼ移植)してサウンド付きでプレイすることが可能です
マルチプラットフォーム
Go言語の利点を生かしてマルチプラットフォームで動作します
Windows、MacOS、Linuxで動作確認しています(ラズパイにはOpenGLの関係で非対応)
ゲームボーイカラーを含むほぼすべてのROMがプレイ可能
通常のゲームボーイのROMだけでなくゲームボーイカラーのROMにも対応しています。
MBC1、MBC2、MBC3、MBC5のサポートに加え、RTCもサポートしているためポケモン金銀のようなゲームもリアルタイムにプレイすることが可能です。
セーブ機能のサポート
メモリをすべてダンプするクイックセーブ機能と、0xa000-0xbfffをダンプする実機と同じ形式のセーブ機能の両方に対応しています。
後者の形式のセーブデータは実機に書き込んでそのままプレイできます
一部通信対戦機能のサポート
ゲームボーイの通信機能と一部ゲームボーイカラーの通信機能をネットワークプロトコルを用いて再現しました。
つまりネットワーク対戦などが可能になっています。(まだ同一ネットワーク内限定ですが。。。)
ハイレゾ化
HQ2xという画像拡大アルゴリズムを使って高解像度化を実現しています。
通常画質
ハイレゾ化
作成過程~ファミコンエミュレータ~
まずエミュレータ開発の経験も、任天堂のハードに対する知識もなかったのでまずシンプルなファミコンエミュレータから作ってみることにしました。
個人的に作ったことがないものを作るときは最初の一歩(設計とか何から作るとか、最初の段階ではどこまで作るのかなど)を考えるのが一番しんどいのですが、幸いにもbokuwebさんのファミコンエミュレータの創り方 - Hello, World!編 -のおかげで割とすんなり最初の一歩を踏み出すことができました。
その後はギコ猫のテストROMを参考に実装を進めていき、fpsが残念ですがなんとかマリオが遊べる程度のファミコンエミュレータが完成しました。
このときCPUとPPUの同期を適当にとっていたため、ラスタースクロールの実装のところで結構つまりました。
レポジトリはこちら
作成過程~ゲームボーイ~
サンプルROM
ファミコンエミュレータを作ることにより、エミュレータを作るという経験とゲーム機に対する知識が身についたため、本命のゲームボーイエミュレータ開発にとりかかりました。
まずはファミコンエミュレータ同様にHello worldを動かし、、、
そのあとほかのサンプルROMを動かしていたのですが、、、
ここでサンプルのように作ることがなかなかできませんでした。
ゲームボーイはファミコンよりも割り込みの種類が多く、割り込みの生じるタイミングもファミコンと違って様々なのでそこら辺を実装するのに結構苦労しました。(上の場合だと、LY=104の時点でLY=LYCのLCD割り込みが起きている)
またHALT命令の実装ミスなどによるバグなどもあり、とにかく割り込みには苦戦した覚えがあります。
テストROM
色々苦労しましたが、サンプルROMを一通り終えたので、今度は有名なテストROMをクリアするためにCPUの未実装部分を作ることにしました。
個人的にはCPUを一気に作るとバグがあったときに候補の箇所が特定しづらいので、テストROMの実行に必要な部分だけをちょっとずつ作っていくことをおすすめします。(ファミコンエミュレータの教訓)
CPUはファミコンエミュレータと同じ調子でいけると思ったのですが、オペランド指定の形式が違ったり、4bit(12bit)部分のキャリーが生じたときにセットするハーフキャリーフラグを実装したりするのが地味に面倒くさかったです。(しかもこのハーフキャリーフラグ、ほとんど使わない。。。)
Goの罠
ゲームボーイのタイマー(一定時間ごとに割り込みをかける機能)を作るときにGoの time.Tickを使おうと思ったのですが、ここで問題が生じました。
ゲームボーイのタイマーは一秒間に何千回、何万回と割り込みを生じさせることもあるのでそのエミュレーションをGoのtime.Tickでやろうと思ったのですが、、、
package main
import (
"fmt"
"time"
)
func main() {
var (
frames = 0
second = time.Tick(time.Second)
)
for range time.Tick(time.Microsecond) {
frames++
select {
case <-second:
fmt.Printf("FPS: %d\n", frames)
frames = 0
default:
}
}
}
上記のコードは1マイクロ秒ごとにframesをインクリメントしていき1秒ごとにframesの数を表示する、つまりforループ(1マイクロ秒間隔)のループ回数を計測するものなのですが、、、
FPS: 674
FPS: 665
...
あれ、おかしいですね。。。
1マイクロ秒ごとのループなので、FPSは 1000000くらいの値になりそうですが、、、
time.Tick(time.Nanosecond) で試してみましたが、マイクロ秒同様700くらいになりました。
これはどうやらWindowsのGo特有のバグ?らしいようでtime.Tickでは少なくとも1.9ミリ秒ほどの間隔(つまり数百fps)になってしまうようです
https://github.com/golang/go/issues/29485 にissueが立っています。 ちなみにUbuntuでも試しましたが普通に1000000くらいになりました。
なんにせよ(Windowsの)time.Tickではタイマーを再現できないことが判明しました。
これは結構悩んだのですが、結局CPUのクロックが4.194304 MHzであることを利用しました。 つまり1クロックかかる命令が終わったときに、タイマーの時間を1/4.194304 M秒だけインクリメントする方式です。(しかもこっちのほうがよさそう)
追記
どうやらWindowsがnano秒単位のsleep機能を提供していないのが根本の問題のようです
テトリス
色々ありましたが、テストROMもクリアしたので今度はテトリスにトライ
ここでは以下のようなバグですさまじく簡単なテトリスになってしまいました。
これは僕のゲームボーイエミュで疑似乱数が0のままだったため初期値の四角ブロックばかりが降ってくることになっていました。
テトリスの疑似乱数はDIVレジスタという一定時間ごとにインクリメントされるレジスタの値を使って作っていたのですが、ここのインクリメントを忘れていました。
完成
DIVレジスタのバグを修正してからはMBC1のカードリッジのバグに対応するだけだったので意外とすんなりいけました。
ポケモンが遊べるぜ
実装の詳細
画面描画やVsync、ジョイパッドの入力を処理するのにはGolangのpixelというフレームワークを使いました。 pixelは直感的なAPIや親切なチュートリアルなどと使っていてあまり障壁を感じる部分がなかったのがよかったです。
あとCPUを作るときに、昔暇つぶしにやった自作エミュレータで学ぶx86アーキテクチャ コンピュータが動く仕組みを徹底理解!で作ったCPUがファミコンやゲームボーイのCPUを設計する際に結構参考になりました。 いい本なので暇があるかたはやってみるといいと思います。
あとがき
とりあえずまともに遊べるエミュレータができてよかったです。
頑張って作ったのでぜひこのエミュレータで遊んでいただけると嬉しいです。
Discussion