ichirin2501's diary

いっちりーん。

ファミコンエミュレータ実装の感想

とりあえずスーパーマリオが動いて一段落したので覚えているうちに感想書いていく。

(この記事の情報量は、デバッグは大変、以上)

動機

単に好奇心。ただ、ファミコンエミュレータに着手したのはこれで3回目になる。
1度目は10年前の身内ハッカソンのとき。このときはC言語で実装してて強引にHELLO, WORLD!を表示するだけで終わった。 実装の続きをしたかったけど、この後は忙しくなってしまって挫折している。
2度目は2年前で、過去の心残りを精算するためにGo言語で着手したのだけど、CPUの実装が終わった後ぐらいからまた忙しくなって挫折している。
今回は2年前のGoコードの続きからコミットを積んでここまで来たので、一応リベンジ成功....と言って良いんじゃないかな、たぶん。

過程

PPUの実装は最初からinternal register(v,t,x,w)を使う方法にした(PPU scrolling)。
hello world表示するだけでも地味にデバッグが大変になってしまって、最初は少し後悔した。

これは見切れているhello world


見切れるbugは修正したけど、次は左寄りになってるhello world
これはppu shift registersのbufferが1tile分足りなくて、1tileずれて表示されていた。

https://www.nesdev.org/wiki/PPU_rendering#Cycles_1-256
Note: At the beginning of each scanline, the data for the first two tiles is already loaded into the shift registers (and ready to be rendered), so the first tile that gets fetched is Tile 3.


hello world!!


正常なhello worldを確認して意気揚々とnestest.nesを読み込ませたらメモリアクセス違反でクラッシュしたんだけど、無視して強引に表示させた様子。 めっちゃバグってて草。画面表示なしだとnestest.nesのCPUテストはクリアするので、ここで少しデバッグに苦しんだ。


画面表示ありでnestest.nesが正常に動いた様子。
原因としては以下の一文を見落としており、メモリアクセス違反が発生していた。

https://www.nesdev.org/wiki/PPU_scrolling#PPU_internal_registers
Note that while the v register has 15 bits, the PPU memory space is only 14 bits wide. The highest bit is unused for access through $2007.


sprite実装に着手したがきもい色になった。
ここからギコ猫でもわかるファミコンプログラミングの各種ROMにお世話になる。


正常なgiko猫さん


背景動いている


sprite 0 hit の実装も(bugがあるが)比較的にすんなり進んでしまってgiko猫のラスタスクロールは動いた。ただ、この後の実装でこのgiko猫ラスタスクロールは動かなくなる、下記に従った実装すると動かなくなってしまった(今も)。

これと、

https://www.nesdev.org/wiki/PPU_registers#OAM_data($2004)%3C%3E_read/write
Writes to OAMDATA during rendering (on the pre-render line and the visible lines 0-239, provided either sprite or background rendering is enabled) do not modify values in OAM

これ

https://www.nesdev.org/wiki/PPU_registers#OAM_address($2003)%3E_write
Values during rendering
OAMADDR is set to 0 during each of ticks 257-320 (the sprite tile loading interval) of the pre-render and visible Scanlines.


公開されている2048.nes。このときはまだコントローラ実装が雑だったので操作感が微妙だったが、ぎり遊べた。

ここからは公開されているPPU系のテストROMと格闘することになる。
bug取り作業で印象に残っているのは3つ。

1. CPUとPPUのタイミング実装を変えたこと

まず、実機だとCPUとPPUは独立して動作し、clock差は1:3の関係になっている。対してエミュ実装の基本的な方針としては並列ではなくCPUを動かしてその後にPPUを動かす。 自分の実装でも方針は同じだけど、CPU命令を1つ実行したらその命令にかかったclock数に応じて最後にPPUを動かしていた。
各命令毎のclock数の表はこことか: https://www.nesdev.org/wiki/6502_cycle_times
例えばLDA ABSなら4clockなので、その命令の処理が全部終わってからPPU側を12clock動かす。 この実装のエミュだと、実機の挙動と比較してCPUとPPUの状態に差があるのでrace conditionの再現度がいまいち。それが原因でどうにもvbl flag timing系のテストが通らなかった。 https://www.nesdev.org/wiki/PPU_frame_timing#VBL_Flag_Timing

再現度を上げるためにも各命令の実行途中で良い感じにPPUを進ませたい (良い感じとは?)。 なんとかしてえ〜って思って各命令にかかるclock数に着目していたら、最低でもメモリの読み/書きする回数以上であることが分かった。 CPUから1回メモリを読み/書きするときにPPU 3clock進ませる方針にして、余ったclock数は最後に調節する設計に切り替えた。 これが良い感じにハマって多くのvbl flag timingu系のテストを通すことが出来た。 その後になって命令毎にどのタイミングでCPU 1clock相当の処理になるのか詳細に書かれた資料を見つけてさらにハッピー。
https://www.nesdev.org/6502_cpu.txt
dummy read/write はこの辺から来てるのか〜となった。

2. CPUから見たときのPPUの状態の定義

メモリの読み書き時にPPUを動かすようにしても一部のtiming testが通らなかった。デバッグするとテストROMが期待する状態に対してPPU 1clock分ずれていた。 CPU 1clock分ずれるのは分かるけどPPU 1clockずれることに心当たりがなくて???だった。 自分の最初の実装だと、「PPUのscanline=241,cycle=1のときにvblankが発生する」というのを、cycle 1 ~ 2 の間で発生すると解釈して実装していた。これだとCPUから見てPPUの状態がちょうどscanline=241,cycle=1のときはまだvblankが発生していないことになっていた。対してテストROMが期待していたのはvblankが発生済み、というものだったので、自分の解釈が間違っていた。 cycle 1 ~ 2 の間で発生する〜実装をやめて、CPUから見てPPU cycle=1状態だとcycle=1で発生するPPU側のイベントは全部終わっているように修正した。 これで当初こけてたテストも通った。

3. sprite 0 hitのbug


めっちゃハマった。上記はこのbugにハマっていたときのもの。 このbugに向き合う前は散々cpuとppuのタイミングのずれと格闘していたので、絶対それ関連だろうと憶測を立ててしまったのが誤り。 不幸なことにこの時点でsprite_hit_tests_2005.10.05のテストは全部OKだったので、既存のテストROMで検知出来ない部分のsprite 0 hit処理がバグってると考えるのにかなり時間がかかってしまった。 wikiの通りにoamはprimaryとsecondaryを用意する実装にしていたんだけど、"0 index"の対象をsecondary oam側にしてしまっていたのが原因だった。普通にbugである。 RasterDemoの画面の動きを観察したときに、背景を1本の軸で回転させてるなぁ〜 => ラスタスクロールはタイミングを捉えるのが重要だからこの1本の軸のところがsprite 0 hitだろう => 常に固定だろう => あれ、なんでsprite 0 hitが固定なのに画面がちらつくんだ? という思考による気付きだった。観察は重要。 primary oam側の0 indexを見るように修正して解決。

他にも色んなデバッグ苦労話があるがだいたいこんなところ

テストROMがある程度通したところで、FCダンパー購入


メルカリで購入したスーパーマリオをダンパーに指した様子

そしていざROM吸い出し....


これはカセットとROM吸い出しの接触不良によって生み出された謎のゲーム。
めっちゃわくわくして自作エミュに読み込ませたらこれだったので「えっ、自作エミュばぐってる?... 念入りにテスト通してきたのに?....」ってなった。 急遽別のエミュを落としてきて同ROMを読み込ませたら同じ現象になって安堵した。

カセットを抜き差しして何度か試すものの、毎回ハッシュ値が違ってて笑ってしまった。
念願のハッシュ値で吸い出し成功したら平和が訪れた。

だめだと分かってても懐かしさに負けてカセットをふーふーしてしまった。反省してます。

その他

実装の骨格はfogleman/nesを参考にさせていただいた。シンプルに書かれててすごい。自分のエミュではAPUは未実装、サウンドは...仕組みにあまり興味がないのでたぶん実装しない。vbl_nmi_timing/7.nmi_timing.nesppu_vbl_nmi/05-nmi_timing.nesはNMI発生をppu 2clock delayさせるhackをしてしまった。今の自作エミュだとcpuを動かした後にppuを動かす実装なので、ppuの動作が実際より遅れることはあっても早くなることはない、という理解なのだけど、テストROMが期待するよりもNMI発生が早い...という状態っぽくて ??? 状態。よくわかってない。あと、decay ppu openbusがよくわからず未実装。ppu openbusのテスト通しておきたい気持ちはある。ぽよよ

github.com

現時点で自分が主に使ったテストROMたち

Test SingleRom Result
blargg_ppu_tests_2005.09.15b palette_ram.nes OK
blargg_ppu_tests_2005.09.15b sprite_ram.nes OK
blargg_ppu_tests_2005.09.15b vbl_clear_time.nes OK
blargg_ppu_tests_2005.09.15b vram_access.nes OK
branch_timing_tests 1.Branch_Basics.nes OK
branch_timing_tests 2.Backward_Branch.nes OK
branch_timing_tests 3.Forward_Branch.nes OK
cpu_dummy_reads cpu_dummy_reads.nes OK
cpu_dummy_writes cpu_dummy_writes_oam.nes OK
cpu_dummy_writes cpu_dummy_writes_ppumem.nes OK
cpu_exec_space test_cpu_exec_space_apu.nes Failed
cpu_exec_space test_cpu_exec_space_ppuio.nes OK
cpu_timing_test6 cpu_timing_test.nes OK
instr_misc 01-abs_x_wrap.nes OK
instr_misc 02-branch_wrap.nes OK
instr_misc 03-dummy_reads.nes OK
instr_misc 04-dummy_reads_apu.nes Failed
instr_test-v5 01-basics.nes OK
instr_test-v5 02-implied.nes OK
instr_test-v5 03-immediate.nes OK
instr_test-v5 04-zero_page.nes OK
instr_test-v5 05-zp_xy.nes OK
instr_test-v5 06-absolute.nes OK
instr_test-v5 07-abs_xy.nes OK
instr_test-v5 08-ind_x.nes OK
instr_test-v5 09-ind_y.nes OK
instr_test-v5 10-branches.nes OK
instr_test-v5 11-stack.nes OK
instr_test-v5 12-jmp_jsr.nes OK
instr_test-v5 13-rts.nes OK
instr_test-v5 14-rti.nes OK
instr_test-v5 15-brk.nes OK
instr_test-v5 16-special.nes OK
nestest nestest.nes OK
oam_read oam_read.nes OK
oam_stress oam_stress.nes OK
ppu_open_bus ppu_open_bus.nes Failed
ppu_read_buffer test_ppu_read_buffer.nes OK
ppu_vbl_nmi 01-vbl_basics.nes OK
ppu_vbl_nmi 02-vbl_set_time.nes OK
ppu_vbl_nmi 03-vbl_clear_time.nes OK
ppu_vbl_nmi 04-nmi_control.nes OK
ppu_vbl_nmi 05-nmi_timing.nes OK
ppu_vbl_nmi 06-suppression.nes OK
ppu_vbl_nmi 07-nmi_on_timing.nes OK
ppu_vbl_nmi 08-nmi_off_timing.nes OK
ppu_vbl_nmi 09-even_odd_frames.nes OK
ppu_vbl_nmi 10-even_odd_timing.nes Failed
sprite_hit_tests_2005.10.05 01.basics.nes OK
sprite_hit_tests_2005.10.05 02.alignment.nes OK
sprite_hit_tests_2005.10.05 03.corners.nes OK
sprite_hit_tests_2005.10.05 04.flip.nes OK
sprite_hit_tests_2005.10.05 05.left_clip.nes OK
sprite_hit_tests_2005.10.05 06.right_edge.nes OK
sprite_hit_tests_2005.10.05 07.screen_bottom.nes OK
sprite_hit_tests_2005.10.05 08.double_height.nes OK
sprite_hit_tests_2005.10.05 09.timing_basics.nes OK
sprite_hit_tests_2005.10.05 10.timing_order.nes OK
sprite_hit_tests_2005.10.05 11.edge_timing.nes OK
sprite_overflow_tests 1.Basics.nes OK
sprite_overflow_tests 2.Details.nes OK
sprite_overflow_tests 3.Timing.nes Failed
sprite_overflow_tests 4.Obscure.nes Failed
sprite_overflow_tests 5.Emulator.nes OK
vbl_nmi_timing 1.frame_basics.nes OK
vbl_nmi_timing 2.vbl_timing.nes OK
vbl_nmi_timing 3.even_odd_frames.nes OK
vbl_nmi_timing 4.vbl_clear_timing.nes OK
vbl_nmi_timing 5.nmi_suppression.nes OK
vbl_nmi_timing 6.nmi_disable.nes OK
vbl_nmi_timing 7.nmi_timing.nes OK

References