Go ファミコンエミュレータ開発記録 その1

2020-11-22

https://github.com/pishiko/gones
日記みたいなものです. Twitter モーメントはこちら


動機

任天堂レトロハードが好きなんですが,せっかくなら遊ぶだけじゃなくて中身にも詳しくなりたいと思っていました. 一番好きなのはスーファミなんですが,仕様が公開されておらず難易度が高そうなので,とりあえずシンプルそうなファミコンの動作の理解をしようと. で,前に Qiita でファミコンエミュレータの Hello world 解説記事があったのを思い出したので,今ならできる!と思って始めました.
偉大なる先駆者様 ファミコンエミュレータの創り方 - Hello, World!編 - - Qiita



Python で実装したい!

最初は他の人がまだやっていないものを作りたくて,Python で書いて matplot のグラフ出力画面で動いたらオツだよな~と思って書いていたのですが,普通に CPU が 60fps でないので泣く泣く諦めました.
これは果たされなかった夢の跡です.


実装の流れは,ROM reader->CPU(Adressing -> OP -> WRAM read/write)-> PPU( VRAM read/write ->Line 生成->描画 )でした.


Go 編

ということで,妥協して golang を採用しました. python は 8bit で uint として数値を扱えないので書きにくかったのですが,Go は圧倒的に楽でした.
Python のソースコードをほぼ移植する形で,HelloWorld を実行したところ CPU は 60fps 十分に出ました.最高!



PPU 60fps 計画

PPU を実装する前にどうやって出力するかを考えました.その中で,golang 製 2D ゲームエンジンの「Ebiten」がよさげだったので採用しました.
Ebiten - A dead simple 2D game library for Go

採用理由

  • 名前がかわいい
  • ゲームライブラリなので低レイヤは触る必要が無さげ
  • コライダーの機能などは実装されていないため,無駄がなく早そう
  • 日本製

で,実装してみたところ Hello world は出力できたんですが,8fps くらいでした.
何故かというと以下の様に,タイル 1 枚のパレット情報(00~11)のスライスを用意しておいて,それを毎回 NewImage していたからです.

p.background.DrawImage(
            ebiten.NewImageFromImage(&image.RGBA{
                Pix:    tile,
                Stride: 8 * 4,
                Rect:   image.Rect(0, 0, 8, 8),
            }),
            op,
        )

どうも NewImageFromImage 及び Image の初期化が遅いみたいでした.
ということで,早くします. ebiten.Image を色なしで,パレットのインデックス(00,01,10,11)ごとにマスクになるような形であらかじめ 4 枚作っておきます. 描画時は pallet の色を取得して,DrawImageOptions で指定します.1 色ごとに 4 枚の Image を貼り付けます.

for i := 0; i < 4; i++ {
            op := &ebiten.DrawImageOptions{}
            op.GeoM.Translate(float64(tilex*8), float64(tiley*8))
            c := nesColor[p.vRAM[pHead+i]]
            op.ColorM.Scale(float64(c[0]), float64(c[1]), float64(c[2]), 1)
            p.background.DrawImage(p.tiles[nameTable[tilex]+patternOffset][i], op)
        }

これで,晴れて 60fps になりました.
関係ないですが,Ebiten は公式ドキュメントが充実しすぎてて,それで記事を書いてもらえずイマイチ盛り上がってないような…



Hello world の後

ギコ猫でもわかるさんの解説で機能の概要を理解して,細かい仕様は Nesdev wiki で仕様を把握しました.
ギコ猫でもわかるファミコンプログラミング
NES reference guide - Nesdev wiki
以下は実装した順の流れです.


スプライト描画

この辺の仕様は図があると分かりやすいです. ファミコンエミュレータの創り方 - Hello, World!編 - - Qiita


スプライト DMA

ギコ猫さん第9章


入力実装

(この時のスクロールは仕様を勘違いしていてダメダメです.)



CPU のバグ

CPU がうまく動いていないと PPU のデバッグもつらいので,先にバグ取りをすることにしました.
やることは,nestestROM を 0xC000 から動かして,logと照らし合わせるだけです.diff 用の PC だけのログはここに置いておきます.
バグではなく仕様で引っかかったのは以下です.

ちなみに,非公式 OPcode は実装しませんでした.nestest の半分くらいからは非公式 OPcode なので無視していいと思います.


0 爆弾

ギコ猫さん第15章
スプライト RAM の 0 個目のスプライトが特定の状態で描画されると PPU レジスタの 0x2002 の 6bit が立つというものです.


スクロール

PPU のミラーリング,スクロールインデックスを実装します. この辺が一番難しかったです.
これはうまくいってないけど絵的に面白いマリオ.


まだノイズ入りですが giko016 が形になってるのがこれ



マリオの空を青くする

PPU パレットのミラーリングの実装が必要です. アドレス$ 3F10 / $ 3F14 / $ 3F18 / $ 3F1C は、$ 3F00 / $ 3F04 / $ 3F08 / $ 3F0C のミラーです. https://wiki.nesdev.com/w/index.php/PPU_palettes
スプライトの優先順位などはまだ実装してないからいいとして,

  • マリオや敵キャラが左に移動するとすぐ画面左端に瞬間移動する
  • 雲の背景が黒くなる

というバグが残っています.


ToBeContinued…


次回予告

APU 実装編