YDiary

メモ的な

Rustでファミコンエミュレータを作った その1

はじめに

前回の記事の終わりに新年の抱負として書いていた通り、Rustを書けるようになりたいなーと思っていたので勉強も兼ねてファミコンエミュレータを作ってみました。

新しい技術を身に着けるには実際に使ってみるのが一番、ということで別に作るものは何でもよかったのですが、なんかRustというと皆ファミコンエミュレータとかGBエミュレータとかを作ってるイメージがあったので自分も真似してみました。

身の回りでいうと、少し前にkamiyaさんがRustでファミコンエミュレータを作っている様子がTLに流れてたのを見て、楽しそうだなーと感じたのも大きいです。

確かにCPUやらメモリやらといったハードウェアの低レベルな処理をエミュレートするというのは、基礎的な処理やらデータ構造やらを扱えるという意味でプログラミング言語を学ぶのに向いているように思います。 それに、実際に絵が出て音が出るのは楽しいのでモチベーションの維持にも繋がりますしね。

取っ掛かり→Hello, world!ロムを動かすまで

いきなりファミコンエミュレータを作るといっても何も分からないので、まずは調べて出てきた次の記事を参考にしました。

qiita.com

基本的な部分のみですが、Hello, world!ロムを動かすまでに必要なことが一通りまとまっていて、とても良い記事です。

記事にもある通り、Hello, world!ロムはNES研究室のサイトから入手可能です。

↑の記事を読めばファミコンアーキテクチャやスプライトのデータ構造などは何となく理解できます。 が、それだけでは肝心のプログラムが動かないのでまずはCPUの実装が必要になります。

CPU実装

CPUの命令セットについてはNES on FPGAのサイトに日本語で詳しくまとめられているので、その資料を基にひたすら実装していきました。

何も動かない状態でひたすら実装をし続けないといけないので、このCPU実装フェーズが最初の難関になるでしょう。 命令セットはトータルで百数十種類ありますが、命令自体はそれほど種類があるわけではなく、豊富なアドレッシングモード×命令の組み合わせでトータルの種類が増えている感じです。 非公式命令もかなりの数が存在しますが、普通に動作させる分には必要ないと思うので自分は実装していません。

PPU実装

CPU実装が完了したら、今度は出力する画像を生成するPPUの実装に着手します。 PPUはスクロール周りも実装するとそこそこ大変ですが、Hello, world!ロムを動かす分には必要ないのでいったん無視してしまって問題ありません。PPUの基本的な部分は、↑のQiitaの記事を見れば実装できると思います。 PPUの実装はCPUに比べれば大分楽で、実際に出力される絵を見ながら作業できるので楽しみながら進められると思います。

Hello, world!ロムの動作!

CPUとPPUが実装できればいよいよHello, world!ロムが動きます。 初めて動かした際の様子が次の通りです。

f:id:YDKK:20220307001509p:plain

この時点ではまだファミコン内部の色表現と実際に出力される色との変換を行っていませんが、まぁそのまま適当に出力するだけでもそれっぽく見えます。 HELLO, WORLD!が2行にわたって出力されているのはVRAMアドレス計算のミスによるバグです。

とまぁ本当に最低限の処理で表示もバグっていますが、この段階で自分の実装したCPUとPPUでプログラムが動いて絵が出ている訳で、めちゃくちゃテンションが上がります。

そのまま勢いで実装を修正しながら色変換を実装して、ロムを書き換えて好きなメッセージを出力してみます。

f:id:YDKK:20220307001538p:plain

ここまでくればチュートリアルは完了といって良いでしょう。

自分の場合はここまで来るのに、Rustの勉強と合わせて0から始めて大体2, 3日程度でした。

本当のCPU実装

さて、Hello, world!ロムが動いたら次に行うべきはnestestを完走させることです。

nestestはファミコンのCPU命令が正しく実装されているかどうかをテストできるロムのことで、これが本当に良く出来ています。

エントリーポイントを変更することでPPUやPAD入力が未実装でも動作し、またその動作を記録したリファレンスとなる有名なCPUログが存在するため自分のCPU実装と比べながらデバッグを進めることが可能です。

nestestやリファレンスとなるCPUログはNesdev wikiのページにあるリンクから入手可能です。

まずはCPUにリファレンスのログと同じ形式のログ出力機能を実装し、リファレンスのログとのdiffを取ることで徹底的にCPUのバグを洗い出していきます。

f:id:YDKK:20220307001606p:plain
実装したCPUログ出力

f:id:YDKK:20220307003146p:plain
リファレンスログとの比較

普通ならここでCPUのバグがそれはもうボロボロと出てくると思うので、地道にデバッグしていきます。

nestestは公式命令→非公式命令の順でテストが走るので、非公式命令を実装していない場合は非公式命令に当たる部分までのCPUログが一致していれば問題ないです。

f:id:YDKK:20220307003222p:plain
デバッグの結果、非公式命令に当たるまでログが一致するようになる

ちなみにnestestはPAD入力とPPUのVBlankでのフラグセットとNMIを実装すれば実際に操作できる形でも動かせます。が、↑でCPUの修正が済んでいれば当然ここでも問題なく動作するのでまぁおまけみたいなものです。*1

次回予告

長くなるので今回の記事はここまで。
次回はいよいよスーパーマリオブラザーズを動作させてみます、が……。

f:id:YDKK:20220307001220p:plain:w256

その2を書きました。

ydkk.hateblo.jp

*1:ちなみに、APUも実装すると効果音も鳴るようになっていて意外と凝っています。