前置き
1か月ほど前にbokuwebさんのゲームボーイエミュレータをGo言語で書いたというブログ記事を見ました。
ゲームボーイ世代(正確にはGBC)であったのとGOで何かしたいと思っていたのもあって、僕もゲームボーイのエミュレータに取り掛かってみることにしました。
追記(2020/1/24)
記事を執筆したときから機能面の大幅な向上があるため加筆を加えました
できあがったもの
GoでGameBoyエミュレータを自作しました
— アカツキ (@akatsuki_py) November 11, 2019
いつぞやのファミコンエミュレータと違って今度は60fps出てます
将来的にGBCに拡張予定です よかったら遊んでみてくださいhttps://t.co/MHwjCmA3PX pic.twitter.com/PvS89Qkvrn
ゲームボーイカラーにも対応しました
ゲームボーイカラーのソフトに対応しました!!https://t.co/MHwjCmA3PX pic.twitter.com/mug5wRQLID
— アカツキ (@akatsuki_py) November 20, 2019
Github
ポケモンクリスタル
夢をみる島
このエミュレータの特徴
60fpsで動作
実機と同じ60fpsで動作します CPU使用率も頑張って抑えているため低スペックなノートパソコンでも60fpsでプレイが可能です
サウンドのサポート
ここだけgoboyを参考に(というかほぼほぼ移植)してサウンド付きでプレイすることが可能です
マルチプラットフォーム
Go言語の利点を生かしてマルチプラットフォームで動作します
Windows、MacOS、Linuxで動作確認しています(ラズパイにはOpenGLの関係で非対応)
ゲームボーイカラーを含むほぼすべてのROMがプレイ可能
通常のゲームボーイのROMだけでなくゲームボーイカラーのROMにも対応しています。
MBC1、MBC2、MBC3、MBC5のサポートに加え、RTCもサポートしているためポケモン金銀のようなゲームもリアルタイムにプレイすることが可能です。
セーブ機能のサポート
メモリをすべてダンプするクイックセーブ機能と、0xa000-0xbfffをダンプする実機と同じ形式のセーブ機能の両方に対応しています。
後者の形式のセーブデータは実機に書き込んでそのままプレイできます
自作のGBエミュレータのセーブデータが実機で動いた! pic.twitter.com/vzFuK12Qe8
— アカツキ (@akatsuki_py) January 15, 2020
一部通信対戦機能のサポート
ゲームボーイの通信機能と一部ゲームボーイカラーの通信機能をネットワークプロトコルを用いて再現しました。
つまりネットワーク対戦などが可能になっています。(まだ同一ネットワーク内限定ですが。。。)
まだ同一ネットワーク内ですが、自作のゲームボーイエミュレータでポケモンの通信対戦に成功しました!うれしい! pic.twitter.com/N23vVekrMA
— アカツキ (@akatsuki_py) December 14, 2019
作成過程~ファミコンエミュレータ~
まずエミュレータ開発の経験も、任天堂のハードに対する知識もなかったのでまずシンプルなファミコンエミュレータから作ってみることにしました。
個人的に作ったことがないものを作るときは最初の一歩(設計とか何から作るとか、最初の段階ではどこまで作るのかなど)を考えるのが一番しんどいのですが、幸いにもbokuwebさんのファミコンエミュレータの創り方 - Hello, World!編 -のおかげで割とすんなり最初の一歩を踏み出すことができました。
キタ━━━━(゚∀゚)━━━━!! pic.twitter.com/TqXxnYFGiP
— アカツキ (@akatsuki_py) October 1, 2019
その後はギコ猫のテストROMを参考に実装を進めていき、fpsが残念ですがなんとかマリオが遊べる程度のファミコンエミュレータが完成しました。
このときCPUとPPUの同期を適当にとっていたため、ラスタースクロールの実装のところで結構つまりました。
Go言語でファミコンエミュレータを自作しました
— アカツキ (@akatsuki_py) October 15, 2019
平均fpsが30前後ですがSuper Mario Brosが遊べます
時間があったら記事書くかも。楽しかったーhttps://t.co/0ZmOdPhyr4 pic.twitter.com/rVDG9Y29eL
レポジトリはこちら
作成過程~ゲームボーイ~
サンプルROM
ファミコンエミュレータを作ることにより、エミュレータを作るという経験とゲーム機に対する知識が身についたため、本命のゲームボーイエミュレータ開発にとりかかりました。
まずはファミコンエミュレータ同様にHello worldを動かし、、、
おれは いま! GBCエミュレータへの だいいっぽを ふみだした! pic.twitter.com/7D7DViPvIe
— アカツキ (@akatsuki_py) October 25, 2019
そのあとほかのサンプルROMを動かしていたのですが、、、
— アカツキ (@akatsuki_py) October 27, 2019
ここでサンプルのように作ることがなかなかできませんでした。
ゲームボーイはファミコンよりも割り込みの種類が多く、割り込みの生じるタイミングもファミコンと違って様々なのでそこら辺を実装するのに結構苦労しました。(上の場合だと、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もクリアしたので今度はテトリスにトライ
ここでは以下のようなバグですさまじく簡単なテトリスになってしまいました。
開発中のGBエミュレータで遊んでいるテトリス、四角ブロックしか降ってこなくてワロタ pic.twitter.com/fGZOlaFLdD
— アカツキ (@akatsuki_py) November 8, 2019
これは僕のゲームボーイエミュで疑似乱数が0のままだったため初期値の四角ブロックばかりが降ってくることになっていました。
テトリスの疑似乱数はDIVレジスタという一定時間ごとにインクリメントされるレジスタの値を使って作っていたのですが、ここのインクリメントを忘れていました。
きた! pic.twitter.com/OijzuN4Cpa
— アカツキ (@akatsuki_py) November 9, 2019
完成
GoでGameBoyエミュレータを自作しました
— アカツキ (@akatsuki_py) November 11, 2019
いつぞやのファミコンエミュレータと違って今度は60fps出てます
将来的にGBCに拡張予定です よかったら遊んでみてくださいhttps://t.co/MHwjCmA3PX pic.twitter.com/PvS89Qkvrn
DIVレジスタのバグを修正してからはMBC1のカードリッジのバグに対応するだけだったので意外とすんなりいけました。
ポケモンが遊べるぜ
実装の詳細
画面描画やVsync、ジョイパッドの入力を処理するのにはGolangのpixelというフレームワークを使いました。 pixelは直感的なAPIや親切なチュートリアルなどと使っていてあまり障壁を感じる部分がなかったのがよかったです。
あとCPUを作るときに、昔暇つぶしにやった自作エミュレータで学ぶx86アーキテクチャ コンピュータが動く仕組みを徹底理解!で作ったCPUがファミコンやゲームボーイのCPUを設計する際に結構参考になりました。 いい本なので暇があるかたはやってみるといいと思います。
WebAssembly
Goではwasmビルド機能がサポートされているため、エミュレータの描画部分以外をwasmでビルドしそれをJavascriptでCanvasに描画することでWebアプリケーションとしてゲームボーイエミュレータを遊べるようにしました(wasmすごい)
スマホだと60fpsでないのが難点ですが誰でも気軽に遊べるためぜひ遊んでみてください。
あとがき
とりあえずまともに遊べるエミュレータができてよかったです。
頑張って作ったのでぜひこのエミュレータで遊んでいただけると嬉しいです。