【Rust】ひどく回りくどい方法によるHello, World!

f:id:s_ktmy:20211222222056p:plain この記事は TUT Advent Calendar 2021 22日目、福島高専 Advent Calender 2021 22日目の記事です。

adventar.org qiita.com

はじめに

はてなブログからこんにちは。福島高専OB、TUT 3系 B3所属のさぎのやです。*1最近寒さに一段と磨きがかかってきましたね、皆様いかがお過ごしでしょうか。福島高専だと寮の風呂のシャワーが冷たすぎて風邪をひく季節になりましたが、ここTUTの寮ではそんなことがなくてとても精神衛生上良いです。
ところで、みなさんは好きなプログラミング言語はありますでしょうか。 C++C#Pythonなどいろいろな言語が挙がると思いますが、僕が最近触っている言語にRustというものがありまして、これがとても良いんですよね。
Rustの言語仕様とか特徴についての記事はGoogleに聞けば星の数ほど出てくるのでここでは触れませんが、速くて安全なコードを書けるRustは僕の好みにぴったりなんですよ。
敷居が高いのが原因なのかわからないんですが、あんまり身の回りでRustを触っている人が少ない印象を持ったので、布教もかねて新しい言語を触るときに必ず出力する"アレ"をRustを使ってやっていこうと思います。

実装物について

ということで、Rustでファミコンエミュレータを作りました。

(右下のマークは録画ソフトの動作通知アイコンです)
余りにも時間がなかったがゆえに実装する命令などを大幅に省いているため、サンプルプログラム"しか"動かないものになっています。
RustでHello, World!をするという目標は達成できているので許してください。

ファミコンの仕様について

ファミコンエミュレータを実装するにあたり、ファミコンの仕様について少しだけ知っておく必要があります。

ファミコンの仕様については以下のサイトが詳しいですが、ここではHello, World!をするにあたって知っておくべき仕様を列挙していくだけにします。 pgate1.at-ninja.jp

  • 画面解像度
    • 256x240らしいです。縦横8pxの画像が32x30で並べられるサイズ。
  • CPUのほか、PPU、APUがある
    • それぞれ画面操作、オーディオ操作に相当するものだと考えてくれればいいです。
    • CPUとAPUは約1.79MHz、PPUは約5.37MHzで動作します。制御部より描画部の方が短い周期で動作していくみたいです。面白いですね。
  • CPUの持つレジスタについて
    • 演算結果を保存しておくA(アキュムレータ)レジスタ、インデックスレジスタとして使われるX Yレジスタ、プログラムカウンタ PC、演算結果によるフラグを管理しておくプロセッサステータスレジスタ Pなどがあります。
      • Pレジスタには「直前の演算結果が0になったかどうか」を表すZフラグ、「直前の演算結果がオーバーフローしたかどうか」を表すVフラグなどがあります。詳しいことは先述のサイトを参照してください。
  • カートリッジについて
    • ヘッダデータ、プログラムデータ、キャラクタデータが含まれています。
    • それぞれがどのくらいのサイズなのかはヘッダデータを見ると確認できます。
  • メモリ空間
    • WRAMとかVRAMとかメモリマップドされたI/Oとかいろいろありますが、全部ひっくるめると64KBあります。
    • 0x8000以降にカートリッジのプログラムが読み込まれます。
    • どこがどのように使われてるかは先述のサイトを参照してください。
  • PPUとCPU間のデータやり取り方法について
    • RAMの0x20000x2007がPPUのI/Oレジスタになっています。
    • つまるところ、0x20000x2007へCPUが情報を書き込むと、PPUがそれを読み取って描画内容に反映させるような仕組みになっています。
  • カラーパレットとキャラクタ(画像)について
    • カートリッジのキャラクタデータにはゲームで使われる画像データが入っているんですが、RGBなど色のデータは含まれておらず、形のみが画像データとして入っています。
    • 色を付ける際に用いるのが「カラーパレット」というものであり、カラーパレットを変えるだけで画像の色合いを変えることができます。
  • 命令セット
    • たくさんあります。非公式の命令とかもあるらしい。
    • 命令実行対象を決める複数のアドレッシングモードが存在します。つまりオペランドの数が一定ではありません。

実装

実装の前に、動かす"Hello, World!"のプログラムを確認しましょう。
ここに載せるのは権利的にアレなので各自ダウンロードしてみてください。該当のプログラムはここのサイトの「サンプル」からダウンロードできます。 hp.vector.co.jp

sample1.asmファイルがHello, World!のソースコードsample1.nesファイルがカートリッジのデータの本体になっています。
少なくともこのアセンブリコードで用いられている命令を実装すればこのプログラムは動くので、それだけを実装していきます。
具体的には、以下に示す命令を実装していきます。

  • ジャンプ命令
    • JMP
      • PCを飛ばすだけ
  • レジスタのインクリメント・デクリメント
  • フラグ操作
    • SEI
      • Iフラグをセット
  • レジスタ間転送
  • ロード命令
  • ストア
    • STA
      • Aレジスタの内容を指定されたメモリ番地に書き込み
  • 分岐命令
    • BNE
      • Zフラグがセットされているとき(直前の演算結果が0であるとき)に分岐

また、各命令にはアドレッシングモードというものがあり、命令に続くバイト列で動作の対象を変えることができます。これもあわせて実装していきます。

さらに、「どこにどの画像を表示するか」の情報が0x2007を介してCPUからPPUに送られているため、PPUはこれを受け取って情報を更新する必要があります。

次に時間がなかったため最小限の構成にするため、様々な機能をオミットしていきます。 音を出す必要はないのでAPUは完全無視、別に凝った画面を出すわけでもないのでカラーパレットも完全無視していきます。 さらに、CPUとPPUのクロック周波数の違いも完全無視し、CPUの処理が終わった後にPPUの処理を入れるような動作をさせることにします。
さらにさらに、各命令には動作に必要なサイクル数というものがありますが、これも無視して1固定にします。

それでは実装が必要な部品がそろったところで、実装していきます。 この記事では主要な部分を示すにとどめますが、実装したものはGitHubに上げてあるので、もっと見たいときはそちらを参照してください。

PPU

先述した通り、CPUとのデータのやり取りは0x20000x2007を通して行われるので、クロックが入るたびにその領域をチェック、更新があればデータを受け取ってVRAMを更新、という動作をさせています。 今回のHello, World!では、データを書き込むためのアドレス指定に使われる0x2006、データ転送に使われる0x2007のみを監視し、変更があればVRAMを更新させています。
また、ファミコンを起動したときに行われるであろう「カートリッジからキャラクタデータを読み込む」処理も書いておきます。 キャラクタデータの各ドットがどのようにカートリッジ内で表現されているかはここでは触れませんが、めちゃくちゃ面白いので興味のある人は先ほどのサイトで確認してみてください。
ウィンドウの作成や描画にはpistonクレートを使いました。

gist.github.com

CPU

まず、オペコードからニーモニックコードやアドレッシングモードを識別する辞書を作成します。 ニーモニックコードとアドレッシングモードはenumでまとめておくことにより、コードの可読性が上がります。上がってるといいなあ。 gist.github.com

次に、CPU本体の実装をしていきます。 与えられたカートリッジのファイルを読み込み、プログラムデータとキャラクタデータに分割、WRAMとVRAMにそれぞれ読み込ませます。 その後、クロックが入るたびにPCレジスタが指すアドレスから値を読み取り、辞書からオペコードとアドレッシングモードを引っ張ってきて、それらに応じた処理をするような動作をさせます。 命令実行後、PCの値をオペランドの数だけ進め、次の命令の位置に合わせます。

gist.github.com

まとめ

最後に、Nesインスタンスを作ってプログラムをfetch()で読み取り、run()することによってエミュレータを起動することができます。 gist.github.com

以上で、Rustで(Hello, World!専用)ファミコンエミュレータが完成しました。

反省点

  • 描画があまりにも遅い!
    • 参照を持つといろいろと面倒なので変数をコピーするなどいろいろやってやりくりしていましたが、やはり遅くなってしまいました。Rustを勉強して効率よく変数を使っていきたいです。
    • 現在はVRAMのデータから1x1の四角形を大量に描画することで描画部を実装していますが、Imageクレートを使って画像データを作成、それを画面いっぱいに表示とした方がもっと効率よく描画できるかもしれません。そのうちやってみます。
  • レポート課題などを考慮したスケジュールや作業工数の見積もりが甘い!
    • 12/22の21:51にこの部分を書いています。遅刻ギリギリ。*3もっと頑張りましょう。

さいごに

いかがでしたでしょうか(テンプレート)。
12月に入っても案が思いつかず、結局この案を思いついたのが予定日の約1週間前でしたが、急ピッチで作業を行い、いろいろなものを犠牲にして完成にこぎつけることができました。褒めて。
前々からファミコンエミュレータを作りたいと思っていたので、頑張ってコイツを完成まで持っていきたいと思います。来年のAdCでは完成した姿をお披露目できたらいいなあ。

また、今回TUT AdCの主催を(勝手に)やらせていただきました。結果として25枠すべてが埋まったことをとても嬉しく思います。ありがとうございます。
AdCに参加・寄稿、AdCを拡散していただいた人、またこの記事を最後まで読んでくれたあなたに最大限の感謝を込め、この駄文の結びとしたいと思います。

GitHubリポジトリ

github.com

余談ですが

こんなことをしなくてもコレでHello, World!できます。

fn main() {
   println!("Hello, World!");
}

内容に間違いがあった場合は @s_ktmy まで連絡ください。


次回の記事は
TUT AdC : にゃん さんの「美少女が技科大生を落とす方法について語る」(12/23)
福島高専 AdC : りゅうちゃん くんの「AWSロボコンのお話(たぶん)」(12/24) です。おたのしみに。

*1:Q: 去年はQiitaでしたよね? A: マサカリが怖いのでこっちにしました

*2:現状スタックポインタを使用する命令が実装されていないため、スタックポインタはただの値を入れる箱と化しています。

*3:みんな午前0時ぴったりとかに記事出しててひえ~ってなってました。それ基準だと遅刻です。