https://github.com/pishiko/gones
日記みたいなものです. Twitter モーメントはこちら
任天堂レトロハードが好きなんですが,せっかくなら遊ぶだけじゃなくて中身にも詳しくなりたいと思っていました. 一番好きなのはスーファミなんですが,仕様が公開されておらず難易度が高そうなので,とりあえずシンプルそうなファミコンの動作の理解をしようと. で,前に Qiita でファミコンエミュレータの Hello world 解説記事があったのを思い出したので,今ならできる!と思って始めました.
偉大なる先駆者様 ファミコンエミュレータの創り方 - Hello, World!編 - - Qiita
最初は他の人がまだやっていないものを作りたくて,Python で書いて matplot のグラフ出力画面で動いたらオツだよな~と思って書いていたのですが,普通に CPU が 60fps でないので泣く泣く諦めました.
これは果たされなかった夢の跡です.
こちらがファミコンエミュレータ開発におけるHello, world!です pic.twitter.com/mh6WyX4cFb
— pishiko (@pishitaro_) October 29, 2020
実装の流れは,ROM reader->CPU(Adressing -> OP -> WRAM read/write)-> PPU( VRAM read/write ->Line 生成->描画 )でした.
ということで,妥協して golang を採用しました. python は 8bit で uint として数値を扱えないので書きにくかったのですが,Go は圧倒的に楽でした.
Python のソースコードをほぼ移植する形で,HelloWorld を実行したところ CPU は 60fps 十分に出ました.最高!
NESエミュ開発進捗メモ
— pishiko (@pishitaro_) November 9, 2020
・60fpsでないのでPythonをやめてGoで実装した
・Hello worldで60fpsが出るようにした pic.twitter.com/eCUZ37p2tg
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 は公式ドキュメントが充実しすぎてて,それで記事を書いてもらえずイマイチ盛り上がってないような…
ギコ猫でもわかるさんの解説で機能の概要を理解して,細かい仕様は Nesdev wiki で仕様を把握しました.
ギコ猫でもわかるファミコンプログラミング
NES reference guide - Nesdev wiki
以下は実装した順の流れです.
この辺の仕様は図があると分かりやすいです. ファミコンエミュレータの創り方 - Hello, World!編 - - Qiita
ギコ猫さん第9章.
(この時のスクロールは仕様を勘違いしていてダメダメです.)
NESエミュ開発進捗メモ
— pishiko (@pishitaro_) November 11, 2020
・PAD入力,BGスクロール対応
nestestでひたすらCPUのトラップ実装とバグ取りやったので,次は非公式OP実装 pic.twitter.com/EpNYvIvLbQ
CPU がうまく動いていないと PPU のデバッグもつらいので,先にバグ取りをすることにしました.
やることは,nestestROM を 0xC000 から動かして,logと照らし合わせるだけです.diff 用の PC だけのログはここに置いておきます.
バグではなく仕様で引っかかったのは以下です.
ちなみに,非公式 OPcode は実装しませんでした.nestest の半分くらいからは非公式 OPcode なので無視していいと思います.
ギコ猫さん第15章
スプライト RAM の 0 個目のスプライトが特定の状態で描画されると PPU レジスタの 0x2002 の 6bit が立つというものです.
PPU のミラーリング,スクロールインデックスを実装します. この辺が一番難しかったです.
これはうまくいってないけど絵的に面白いマリオ.
マリオ,世紀末だけどなんとなく遊べるとこまできた pic.twitter.com/urr7qwrx7s
— pishiko (@pishitaro_) November 12, 2020
まだノイズ入りですが giko016 が形になってるのがこれ
NESエミュ開発進捗メモ
— pishiko (@pishitaro_) November 14, 2020
・PPU描画部を変更,スクロールほぼ対応(0爆弾含む)
・VRAMのミラーリングを実装
PPUのバグ取りがかなりしんどかったです.なおgiko016はまだノイズが pic.twitter.com/I9GXLTSbEJ
PPU パレットのミラーリングの実装が必要です. アドレス$ 3F10 / $ 3F14 / $ 3F18 / $ 3F1C は、$ 3F00 / $ 3F04 / $ 3F08 / $ 3F0C のミラーです. https://wiki.nesdev.com/w/index.php/PPU_palettes
スプライトの優先順位などはまだ実装してないからいいとして,
というバグが残っています.
ToBeContinued…
APU 実装編
オーディオをちょっとだけ実装した(酷い) pic.twitter.com/SvqHA8PwQN
— pishiko (@pishitaro_) November 16, 2020