KDOC 24: ゲームボーイエミュレータを作る

ゲームボーイエミュレータの実装は数多くある。参考にして作る。

memo

  • bokuweb/gopher-boyGoで読める。最初のコミットをたどる。
  • pkgconfigの問題で実行できず
    • ググった方法で実行できた
    • 機能ごとにさまざまなミニマルROMがある
    • 各ROMの画像回帰テストで動作チェックしているよう。うまい
    • go run cmd/go-boy/main.go roms/helloworld/hello.gb
  • 比較的コードが少なく、ファイルが分かれているので読みやすそう
  • 命令には引数(オペランド)が存在することがある
    • 引数を指定する方法は13種類ある。アドレッシングモードという
    • プログラムに埋め込んだ定数を引数にするモードやメモリアドレスを渡してそのアドレスに格納されている値を引数にするモードがある
  • memory mapped IO
    • IOレジスタがCPUのメモリ空間上にマップされている。該当するアドレスを読み書きすることで、CPUはコントローラの入力を読み取ったり、PPUに出力する画像の情報を送ったり、APUから出力する音の波形を設定したりできる
    • アドレスに対応するモジュールにアクセス
    • CPUからはメモリアクセスに見える
    • このアドレスを読み書きすれば各モジュールを使える、くらいの感じ
  • カセットのMemory Bank Controller
    • カセットにはメモリが積まれていて、メモリの拡張として用いる
    • セーブデータとか保存している領域っぽい
    • 確かにカセットごとに保存されてたよな
    • コントローラには種類がある
    • RAMのサイズはカセットによって異なる
    • コントローラは複数のbankで構成されていて、スイッチできる
    • コントローラにはモードがある
      • RAM優先/ROM優先
  • ビット演算を復習しないといけないな。こういうとき使うんだ
    • 命令を眺める。意味はわかるが、どうしてそうなのかはよくわからない

Tasks

TODO GPUのヘルパー関数のテストを書く

まだあまり理解できてないところがある。

TODO 割込のテストを書く

難しそうな部分。

TODO CPUのテストを書く

  • フラグあたりがよくわかってない

Archives

DONE バスのテストを書く

完全に理解するためにテストを書く。

  • リトルエンディアンだから逆に格納されているわけか
    • DEAD => AD DE
  • 0x0000 に 0xA5 が格納されている
    • 0x0000は配列のインデックスで、0xA5が中に入っている値と考えられる。もちろん ハードウェア的には違うんだろうが、同じことだ
    • 中身は0〜255が入る。これらは16進数で2文字で表せる
  • 書き込みしてるのだが、反映されないな
    • バンクコントローラが0の場合は、書き込み不可。ROMしかないから。コントローラによってはRAMがあったりするので、そこでは書き込める
    • まだあまりバンクコントローラについてよくわかっていない
  • カートリッジのROMと、バンクコントローラのROMは違うのだろうか
  • メモリマッピングを理解した。バスから0x8001番を読み込むと、VRAMの0x0001番を参照するようなことだ。実際のRead, Writeの処理はデバイスに移譲する

DONE 動かしてわかる CPUの作り方10講 | Gihyo Digital Publishing

CPUの知識が足りないので読む。

  • この本で設計するシンプルなRISC型CPUの仕様
    • すべての命令は固定長で15bit
    • 機械語を格納するインストラクション・レジスタやメインメモリのプログラムにおけるデータ長も、命令と同じ長さになる
    • 2バイト(16bit)のデータ表現
      • CPUが扱うデータは2バイト(16bit)
      • 汎用レジスタやメインメモリのデータ領域のデータ長も16bitになる
      • 算術論理演算を行うALUも16bitが基本サイズ
    • 8個の汎用レジスタ
      • 命令長が15bitの制約の中で、命令の種類を表すオペレーションと、最大2つのオペランドを指定する必要がある
      • オペレーションを表す命令コードに4bit、汎用レジスタを指定するオペランドに3bitを割り当てることにする
      • なので命令の数は16個、汎用レジスタの数は8個になる
    • プログラム領域とデータ領域を分離するハーバードアーキテクチャ
      • メインメモリとレジスタ間のデータ転送は、
        • A. プログラム領域とインストラクション・レジスタ間
        • B. データ領域と汎用レジスタ間
      • プログラム領域におけるデータ長が15bit、データ領域におけるデータ長は16bitとなり、1bitのずれがある。そのため独立したデータバスとして扱うハーバードアーキテクチャを採用する
      • メモリマップドI/O
        • 回路構成をシンプルにするため、メインメモリのデータ領域に独立したアドレスを与えてI/Oを配置するメモリマップドI/Oを採用する
    • シンプルさに徹している。命令長を増やせば命令数を増やしたり、オペランド等に割り当てるビット数に余裕が生まれ、汎用レジスタの数やメモリサイズを大きくできるがやらない
  • mov
    • 汎用レジスタのデータを移動(コピー)する
    • [命令コード][第1オペランド…to][第2オペランド…from]
    • toの汎用レジスタの内容を、fromの汎用レジスタにコピーする
  • add
    • 汎用レジスタのデータを加算
    • [命令コード][第1オペランド…target][第2オペランド…from]
    • target + fromして、計算結果をtargetの汎用レジスタに上書きする

DONE 命令セットを網羅する

追加していく。

CLOSE オペランドを取る関数が何かおかしい

第2引数のレジスタとデータが混じっているように見える。

いや、第2引数のレジスタとデータは一部共用だから、混じっていていいんだ。

DONE 画像によるテスト方法を確認する

テストでそれぞれのROM実行結果を画像出力しているので、手動確認しなくていい。これはどうやっているのだろうか。ほかのゲーム開発にも応用できそうだ。

  • そもそも画像をどうやって出しているのだろう
    • OpenGLのラッパーライブラリを使うのだが、これはエミュレータとどう絡んでいるか
    • WASMで出せたりするか
    • GPUが色情報の配列を持っている。それを画像サイズに合わせて長さと幅を設定して配置すれば画像になる
    • 3種類のレイヤがある。背景、ウィンドウ、スプライト。それらの重なり具合をうまくやって、1つの色情報の配列になる
    • skipFrameが返す画像
      • emu.next()が返す画像
        • gpu.Step()して、gpu.GetImageData()する
        • フィールドのg.imageDataのゲッター
    • 色情報の配列を描画ライブラリで書き出す
      • それだけのことだから、別に四角い白黒の文字列に置き換えても画面は表示できる

DONE GPUのテストで画像を出力してみる

  • なんだかわからない。進まない
    • Readでは、命令を解釈している。渡されたオペコードから、返す情報を決めている。CPUと同じ感じ
    • build系はレイヤごとに色情報を作成する処理をしている
    • build系はStepから呼び出される。step上で分岐してどのbuildを使うか決めている。基本的に1つのセルに対して複数のbuildは使わない
    • 実体はOAMアドレスにあって、そこから設定だのを取り出す
    • lycは何に使ってるんだろう。ly compareのよう。割り込み判定に使ってる
  • GBには画像に特殊効果を与えるラスタエフェクトをやるハードウェアはない
    • レジスタで特殊効果を与えている
  • 特にテストにするような項目はなさそう
    • 画像を出してみる
    • imageDataには、何らか処理したあとの色の構造体が入っている(buildWindowTileを実行した結果とか)
    • 処理する前のデータはどこにあるのだろう
    • GPUの中にあるlcdcって何
      • GBの画面を制御するLCDC(LCDコントローラ)で、タイル単位で画面を描画するタイルは8x8のビットマップで、専用のメモリ領域に定義する
      • LCDの知識が必要
      • タイルデータはVRAM(8000から97FF)に配置されている
        • 前半はスプライトと背景、後半はオブジェクト
      • タイルマップは9800-9BFFにある
      • つまり8x8の1つ1つと、それらを使って画面を構成する2階層あるということか
    • 背景は出せた。単に行の g.ly それぞれでbuildBG()しただけ。あの緑色がついた
    • スプライトが出ない
    • タイル指定もできない
    • タイルIDが全部同じになっている
      • タイルID
      • パレットIDを固定すると、出力する色が変わった。パレットにプリセットの色が設定されていて、IDで選択するだけで色を変えられる
      • 2アドレスで1行分を担当している。2アドレスを重ね合わせて濃さを決定する。1x1は濃、1x0はやや濃、0x1はやや薄、0x0は薄
      • アドレス8000に0b1111_1110, アドレス8001に0b1111_1100の場合、1行は░░░░░░▒▓になる。
    • タイルの集合がタイルマップ

DONE テストでスプライトを出力する

スプライトを出してみる。

  • paletteIDが0になっているな
    • paletteIDが0だと背景色を優先して描画されない
    • タイルIDはスプライトごとにあり、
    • パレットIDはピクセルごとにあり、実際の色を指定している
    • objPaletteが設定されていなくて、paletteIDが常に背景色のIDになっていた

Reference

ギコ猫でもわかるファミコンプログラミング

定番のエミュレータ解説サイト。一般的な仕組みはこっちで学んでおくのがよさそう。

Foreword - Pan Docs

ゲームボーイの仕様。

GameBoy CPU Manual

ゲームボーイのCPUマニュアル。

Projects | gbdev.io

開発コミュニティ。

Resources | gbdev.io

非常に詳しい解説集。

Game Boy: Complete Technical Reference

詳しいリファレンス。