Help us understand the problem. What is going on with this article?

ゲームボーイのエミュレータをGoで作った話

前置き

1か月ほど前にbokuwebさんのゲームボーイエミュレータをGo言語で書いたというブログ記事を見ました。

ゲームボーイ世代(正確にはGBC)であったのとGOで何かしたいと思っていたのもあって、僕もゲームボーイのエミュレータに取り掛かってみることにしました。

できあがったもの

Github

よかったらスターお願いします

ポケモン赤

ガンギマリゼニガメかわいい
pokemon.png

テトリス

tetris.png

たまに描画やサウンドがバグるときがあります。ぼちぼち修正していく予定です。

特徴としては

  • 60fps(core i5-4310Uで確認)
  • サウンドのサポート
  • マルチプラットフォーム(Windows10とUbuntu18.04で動作確認)
  • キーボードに加えてジョイスティックでも操作可能
  • MBC1カードリッジに対応
  • コアダンプによる『どこでもセーブ』が可能

などの特徴があります。

割り込みもシリアル通信以外はサポートできているはずです。

2019 11/20
ゲームボーイカラーのソフトにも対応しました!ポケモン金が遊べます
pokemon_gold.gif

作成過程~ファミコンエミュレータ~

まずエミュレータ開発の経験も、任天堂のハードに対する知識もなかったのでまずシンプルなファミコンエミュレータから作ってみることにしました。

個人的に作ったことがないものを作るときは最初の一歩(設計とか何から作るとか、最初の段階ではどこまで作るのかなど)を考えるのが一番しんどいのですが、幸いにも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でやろうと思ったのですが、、、

fps.go
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や親切なチュートリアルなどと使っていてあまり障壁を感じる部分がなかったのがよかったです。

ただ60fpsを出すのには結構苦労してタイルデータをこっそりキャッシュしたりしてやっと60fps出せたりしたのでメモリがふんだんに使える現代ならではのエミュレータって感じになってしまいました(笑)

サウンドだけは少しズルをしてgoboyというゲームボーイエミュレータのサウンド機能を移植させてもらいました。正直僕のエミュレータより優れていると思います。

あとCPUを作るときに、昔暇つぶしにやった自作エミュレータで学ぶx86アーキテクチャ コンピュータが動く仕組みを徹底理解!で作ったCPUがファミコンやゲームボーイのCPUを設計する際に結構参考になりました。 いい本なので暇があるかたはやってみるといいと思います。

あとがき

とりあえずまともに遊べるエミュレータができてよかったです。

とりあえずはポケモン赤を遊んでそのあとはGBCに拡張、ネット対戦機能の追加などと色々やっていきたいです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away