today   2019-09-17

access_time  3 mins

RustでNESエミュレータを作っている(備忘録)

進捗ツイートまとめ

preview

rust-nes-emulator GitHub

ここ一ヶ月ぐらいRust入門を兼ねて、ファミコンエミュレータを自作している。変遷についてまとめたかったのだがTwitterのモーメントがもう使わせる気がなさそうなのでブログにまとめておく。

誰がなんというとこれはモーメント。

Rustを使うモチベーション

筆者の背景を紹介するが、単純に書いた量と馴染みで言えばC#, C/C++, JavaScript/TypeScript, Scalaの4つだと思う。

特定の言語を持ち上げたりすることはあまり得意ではないが、2015年頃から漠然と安全側に傾けた言語に興味が湧くようになっていた。書くと長くなるがscalaを書き始めたことと、LLVMについて調べだしたあたりが強く影響している。

その中でも組み込み開発での可用性が高そうで、パフォーマンスも既存の言語に引けを取らないという点でRustが私の中で候補に上がっている。(golangはOSレスno_stdな組込み向けではTinyGo次第なところがある)

自作にあたり

NesDev wiki が圧倒的な情報量を取り揃えている。幸いファミコンエミュレータについては日本でも作っている先輩方が多数おられるので、英語が苦手でも日本語情報がかなり得られるので照らしながら作ると良い。

細かい実装の話などは後日別の媒体にまとめようと思っているので、残りは実装の変遷を。

CPU実装

はじめはwikiにあるCPU命令をすべて実装する作業を行った。きっと合っているだろうという気持ちで…。

すぐに言語マクロを試したくなるのは趣味。

カセット読み込みと割り込み実装

CPU命令と.nes形式ファイルの展開を実装した頃で、実機起動時(もしくはResetボタン相当)の動きができるようになった。

Reset割り込みは0xfffc, 0xfffdに記されたアドレスに飛ぶような挙動なので、エントリポイントに飛ばせるようになったことに等しい。ここがスタート地点だ…?

CPUのみでHello, World!挑戦

NES研究所 様が公開しているHello, Worldを表示するサンプルを動かすことにした。対戦よろしくお願いします。

バイナリエディタとオペコード一覧を見て、命令のフェッチがうまく行っているか見ながらステップ実行を繰り返している様子を眺めていた。

一部修正してPPUADDR, PPUDATA経由でCPUからBGの情報と”Hello, World!”の文字列をを流しているのがおおよそ確認できたので次に進むことにした。

rust-nes-emulator Issue #3: Hello Worldの実行結果の正当性を確認する

PPUでHello, World!

ファミコンにはCPUの他にPPU(Picture Processing Unit), APU(Audio Processing Unit)というサブシステムが乗っかっており、画面表示はこのPPUが担っている。

表示できるものにはいわゆる背景のBGと、マリオとかクリボーみたいに動かせるSpriteがある。Hello, WorldのサンプルではSpriteは使っていなかったのでBGを描画するコードの実装した。

頭がオカシイのか何故かここだけは一発で動いてしまった、多分ここで苦戦してたらエミュ自作にこれほどはまらなかったかもしれない…。

最初はbitmap画像にしていたが不便だったので、piston_windowというライブラリを使って表示制御することにした。

残念ながらこの時点では他のROMはほぼ動かなかった。nestestでさえこの有様。Hello, Worldがいかに簡潔なサンプルなのか思い知らされる。

えー!なるっちのCPU実装がバグだらけ!?

画像は省略します。nestest.nesという命令一式を試せる素晴らしいROMがあるのですが先の通り表示すらできない有様。

nestestの作者はこれはもう素晴らしくて、画面が表示できなくてもPCを0xC000に飛ばせば画面なくてもテストが進むよモードを実装していた…天才か。

ただし確認方法は正しく動くROMのログとCompareすることだったため(8000行ぐらいある)、まずはログ出力を強化した。

がんばれかみ子、バグをしばいて立派なまぞくになるんだ。

rust-nes-emulator Issue #25: NESTESTのメニュー画面が正しく表示されるようにする に大体書いてあるが、ひたすらlogを見ていってミスった実装を直しての繰り返しをしていた。

だんだん直って

起動するようになった。(下にCompareしてるスクショが残ってた)

ここで初めて気がついたがunofficial opcodeらしきものがあるらしい。それもすべて実装した。複数の命令の組み合わせか通常存在しないアドレッシングモードか、NOPかのいずれかな気がする。

ボタンが押せなくてテストが実行できなかったのでボタン入力を実装。

ここもまた一発で動いてしまった。ここまで死ぬほど動かなかったので、嬉しさのあまりまぞくになるところだった。

リグレッションテスト導入

せっかく動いたのに、後の実装でぶっ壊れると嫌なのでテスト環境を整備した。

顔色が悪いPPU

nestestしか動かしていなかったので興味本位で動かしたら顔色が悪い。顔色が悪いけど嬉しかった。

ファミコンには実際の表示領域4面分の描画空間(NameTable)が合って、その任意位置を変更できるスクロールレジスタが存在する。

実装したらこの有様…。

スクロールがない1面だけのゲームに焦点を絞って先にSpriteを実装した。体がばらばらになってしまった…

スクロールのバグと合わせて新しいゲームが完成した。

Spriteのバラバラ事件は表示反転の論理が逆になっていただけだった。

ここまで来るとスクロールのないドンキーはそれっぽいゲームになっていた。

背景色の判定をきちんと実装したのでそれっぽくなってきた。

属性テーブルの参照アドレスをミスっていたので修正したら、顔色はかなり良くなった。

透明色処理が正しく実装されていないと、タイトル表示がこうなるのは正しい。

ドンキーコングはほぼ完動。

マリオ完動に向けて修正

ここまで来たら自分の理解度が深まったこともあり、画面出力と実装をにらめっこしてどこがおかしいか順に潰していった。結構エスパーっぽいデバッグだった気もする。

風を感じていた。これはスクロールの値が8倍に適用されている。

これは属性テーブルがスクロールに追従していないので、背景と色がずれている。

背景色処理をしていないので、隠しブロックが見えている。あとスクロールがカクカクしている。これはスクロール値の計算間違い。

動画の最後で変死しているが、これはパックンフラワーが見えていない。

これはわかりやすい。

かなり現物に近づいた。あとは黒い部分だけ。これは[背景色, 背景Sprite, BG, 全面Sprite]とあって 透明色の黒色が選ばれていたら後ろの色(e.g.空の色)を出してあげるのが正解。

死ぬほど嬉しかった。

所感

いかがでしたか? とりあえずマリオが遊べるまでのツイートをまとめた。最近WebAssemblyに吐いてブラウザでも遊べるようにしたので興味あれば遊んでみていただけると嬉しい限り。

rust-nes-emulator.netlify.com

Rustを使ってみて

WebAssembly版のポーティングするjavascriptを少し書いていただけでバグまみれになったので、私はRustがすごい良い言語だと感じる。以下紹介する

意図しない型変換、Overflow/Underflowなどはすぐに原因がわかった

異なる型の演算はそもそもコンパイルエラー、Overflow/Underflowは実装者が明示しないと実行時エラー。 なんかおかしい→調べたらOverflowだった。みたいなのがなかった。(すぐに場所がわかった)

書き換え可能な参照をばらまけないので、怪しい設計がコンパイル通らない

別の場所からフラグを書き換えるような行儀の悪いことはそもそもコンパイルエラー。

ありがとうRust…。私はか弱い凡才なのでコンパイラに守られたい。

速い、wasmにしても速い

流石にここまで早くなると思ってなかった。3.6msか…。

終わりに