NES
はじめに
NES(Nintendo Entertainment System)とはいわゆるファミコンのことである。このエミュレータを自分の手で書いてみたい。過去何度か挑戦したが途中で飽きたり実力不足が多分にあってROMを動かすことができなかった。今なら以前よりは技術力が上がっている気もするのでチャレンジしてみる。
NESに関する情報はすでにweb上に大量にある。例えばファミコンエミュレータの創り方 - Hello, World!編 - - Qiitaや先人の実装(fogleman/nes: NES emulator written in Go.)も色々ある。日本語情報も多くて詰まったとしてもなんとかなりそうだ。つまりあとは実装者の気合い次第という感じ。
とりあえず何の言語で書くかだが今回はRustを使う。そういえば以前はGolangとかTypeScriptなんかで書こうとしていたが力不足で頓挫した記憶がある。
実装の流れとしてはWriting NES Emulator in Rustという初心者に最適なサイトがあるのでこれの流れで進めていく。ある種写経に近い感じになりそうだけどまぁ頓挫させるよりは良い。まずは最後まで書き切ることを目標にしたい。
Writing NES Emulator in Rust
https://scrapbox.io/files/6371aa60fdf6fe001f91b53f.png
参照: Emulating NMI Interrupt - Writing NES Emulator in Rust
Introduction / NES Platform
とりあえずIntroduction - Writing NES Emulator in Rustから読んでいく。
Luckily for us, NES doesn't have an Operating System. That means that the Application layer (Gamezzz) communicates with hardware directly using machine language.
OSが無いのでNESはハードウェアと機械語を通じて直接やりとりする。
https://scrapbox.io/files/635a2ec8a0e36d001d27e535.png
NESの構成要素
CPU: 2A03。6502 Chip。8bitマイクロプロセッサ。プログラムの命令を実行する役目。
Easy 6502 by skilldrick
64種類の命令があり、アドレッシングモードの分も加味すると全部で150命令ある。
PPU: 2C02。ゲームの現在の状態をスクリーンに書き出す役目。
RAM: CPUとPPUともに2KiB(2048 byte)のメモリ。
APU: サウンドを生成する2A03の一部となるモジュール。5チャネル。
Cartridge: カセット。Character ROMとProgram ROMがある。 前者はグラフィック、主に背景やキャラの画像とか。後者はCPU用の命令プログラム。初期化時に前者はPPUに接続され、後者はCPUに接続される。
ゲームパッド: コントローラ。ユーザー入力をゲームに伝える。8bit。
実装の流れ
CPUの実装
BUSの実装
NES ROMの実装
PPUの実装
GamePadの実装
APUの実装
Emulating CPU
Getting Started
このあたりから実際にコーディングを始める。まずはa9 c0 aa e8 00というプログラムを動かすところから。a9 c0 aa e8 00はアセンブリに直すと下記。
code:txt
LDA #$c0; a9 c0
TAX ; aa
INX ; e8
BRK ; 00
CPUの基本的な動作の流れ
命令メモリから次の実行命令を取り出す
命令をデコードする
命令の実行
サイクルを繰り返す
CPUの構成
https://scrapbox.io/files/636de42f1ce896001d3b2480.png
参照: Emulating CPU - Writing NES Emulator in Rust
レジスタ
PCカウンタ
スタックポインタ
Accumulator
Xレジスタ
Yレジスタ
ステータスレジスタ
外部メモリ
WRAM: 2 KB
オペコードの処理は理解できたがフラグやステータスの処理がよくわからない
6502のステータスコードは8bitなので例えば0xA9(LDA)の命令に関しては0b0000_0000の1bit(ZeroFlag)と7bit(NegativeFlag)のフラグを立てるか否かをAレジスタの値から決める感じ。
6502のステータスコード表
https://scrapbox.io/files/635b90459e40bb001de7aa39.png
例えば6502ではステータスレジスタは8bitなので0000 0000の各桁それぞれが0 or 1でtrue or falseを表す。
各桁が何を表しているかは6502 Registersに書かれている。0桁目はCarray Flag、1桁目はZero Flag、2桁目はInterrupt Disable...といった感じ。
キャリーとは
carry flagは演算結果がoverflowした時(u8なら0xFFを超えた時)に立てる
該当コミット
CPUの原型 · YuheiNakasaka/nes-rs@6d59e50
最初のopcode実装 · YuheiNakasaka/nes-rs@60a8b45
0xaaの実装 · YuheiNakasaka/nes-rs@bad71b6
opcodeの処理を別メソッドに切り出す · YuheiNakasaka/nes-rs@cfd6671
0xe8の実装 · YuheiNakasaka/nes-rs@9eb800c
Memory Addressing modes
プログラムをメモリにロードする処理を書く。0x8000から0xFFFFまでがProgram ROM。カートリッジを差し込むとCPUはリセット割り込みを受けてフラグとレジスタの初期化とPCカウンタを0xFFFCにセットする。
NES CPUのアドレス空間は2byte(65536)。リトルエンディアン。つまり最下位8bitが最上位8bitよりも前に格納されることになる。例えば0x8000なら00 80になる。0x8000のアドレスのメモリからAレジスタにロードする命令でいうと、LDA $8000はad 00 80となる。
リトルエンディアンのデータをメモリに読み書きする実装がよくわからない
アドレスは2byte。2byteをまずは上位bitと下位bitに分ける
書き込み
上位の場合はu16_data >> 8 as u8で8bit右シフトしてu8のデータとして扱う・・・(1)
下位の場合は(u16_data & 0xFF) as u8と下位8bitだけが保存されるように論理積を取る・・・(2)
あとは(2)を先に書き込み、(1)をその後に書き込めばリトルエンディアンになる
読み込み
書き込みの逆をやれば良い
リセット割り込みの実装。リセットがかかるとNES CPUは0xFFFCと0xFFFDからそれぞれ読み込みを行う。なので0xFFFCにProgram ROMのPCカウンタである0x8000をアドレスとして書き込んでおく。
アドレッシングモードの実装。例えばLDAのImmediateのモードの場合、PCが0x8000でその値はLDAなので0xA9。このDocsより0xA9ではImmediateモードだとわかるので、PCは0x8000の次の0x8001の値を読み込む。LDAはAレジスタにロード命令なので0x8001の値をAレジスタに読み込む。
PCカウンタをインクリメントするタイミングがよくわからなくなってきたが、実装的にオペコードの読み込みは共通で+1してるから、あとはオペランドを1つ取るならさらに+1、オペランドを2つ取るならば+2すれば良い。
STAみたいなメモリに書き込む命令をどうやってテストすれば良いのか今は謎。
現状のOPECODEの実装だとmatchがかなり肥大化しそうでやばいので今後の実装を見据えてOPECODEをまとめたテーブルをグローバルに定義する。グローバル定数の定義にはlazy_staticではなくonce_cellを使ってみる(使ったことないので)。
該当コミット
CPUの初期化 · YuheiNakasaka/nes-rs@aad9284
アドレッシングモードの一部実装 · YuheiNakasaka/nes-rs@79686e0
OpeCodeとModeをテーブル化して整理する · YuheiNakasaka/nes-rs@4c20fab
Implementing the rest of CPU instructions
6502 Referenceを参考にして残りの命令も実装していく。150命令分あるので気合いでやるしかない....。あと複数アドレッシングモードがある場合のテストまで書くのはきついのでZeroPageがあればZeroPageだけとりあえず書くことにする。
INCR/DECRをする時はRustのwrapping_(add|sub|mul)を使うとオーバーフローを避けられて便利。
Rustでの整数オーバーフローまとめ - Qiita
ステータスレジスタのset/change/toggleメソッドを拵える構造体を作った方が楽かもしれないが、まだキツさはないので後々リファクタリングでいいやとなった。bit演算の筋力鍛えるのにも良さそうだし。
bitflags - Rustみたいなcrateを使うとさらに便利かも。
スタックにpushするというのはスタックポインタのアドレスのメモリに書き込むということか...。スタックポインタを上書きするのかと思って実装してた。
スタックポインタは0x1000~0x01FFで256byte。上位8bitは0x01固定で下位8bitが変動する。スタックポインタはPushすると1減ってPullされると1増える。
スタックポインタはオーバーフローしてもCPUは検知できないのでプログラムが死ぬ。注意。
テスト書きながらだと全然進まなくてしんどいので一旦ガッと命令だけ実装しようかな
RTI命令がよくわからん
割り込みが発生すると、割り込み完了後に復帰すべきアドレスそのものをスタックへ pushしさらにステータスレジスタP をpush し割り込みハンドラへジャンプする。
Return from interruptなのでこの↑割り込み処理の逆をやれば良い。つまりスタックからpopしてステータスレジスタPへ戻し、さらにpopしてプログラムカウンターにセットする。
RTS命令わからん
サブルーチンから復帰する命令。サブルーチンとはいうものの実用的にはジャンプしたい場所にジャンプするための便利命令みたいな使われ方をするらしい。実装としてはスタックをpopして1加えてプログラムカウンターにセットするだけ。
Running our first game
CPUがちゃんと実装できているか確認するために6502用に書かれたsnakeゲームを動かしてみる。ちなみにsnakeゲームのアセンブリはこれ→snake6502.asm。
メモリマップ
0xFE: ランダムな数値
0xFF: 最後に押したボタンのコード
0x0200..0x0600: スクリーン。32x32の行列。左上が(0,0)。0x0200が(0,0)の座標で、0x0201が(1,0)の座標で、0x0220が(0,1)の座標。
ゲームはメインループの中でユーザーからのインプットを受け取りゲームの状態を計算しスクリーンにレンダリングするという流れを繰り返すだけ。現状の実装だとユーザーからのインプットを受け取れる仕組みがないのでまずはそこから手を加えていく。
ユーザーインプットなどの割り込みを行うためにSimple DirectMedia Layer - Homepage(SDL)というオーディオ、キーボード、マウス、ジョイスティック、GPUなどへのローレベルアクセスを提供してくれるライブラリを使う。Rust用のcrateとしてはsdl2 - Rustが提供されている。
Windowの初期化、テクスチャの初期化、ユーザーインプットの初期化などを経て実行を行う。
画面が真っ黒になって何も動かない
原因はprogramをloadする位置が違った。NESの仕様通りの0x8000を始点にしてたが今回動かすプログラムは0x0600が始点になる。これを修正した。
Opcodesの実装漏れがあったので修正した。
W/S/A/Dのキー入力で動きが変わるはずだがS以外の時にoverflowエラーが発生する。
ADC/SBC命令にバグがありそう
算術処理がミスってた
adcは A + M + C。この結果がもしu8(0xFF)を超えた場合はキャリー。
sbcは A - M - (1 - C)。この結果がもしu8(0xFF)を超えた場合はキャリー。
A - M - (1 - C) = A + (-M) - 1 + Cになるのでadcの応用で簡単に実装できる。
Rustのsigned intにはマイナスに変換できるwrapping_negというメソッドがあるのでこれを使うと、
A + (M as i8).wrapping_neg().wrapping_sub(1) + Cという感じで計算可能
Emulating BUS
https://scrapbox.io/files/6367759f35f83f001dd36583.png
参照: Emulating BUS - Writing NES Emulator in Rust
CPUとメモリをつなぐためのワイヤー(バス)は下記3つある。
アドレスバス(16bit): アドレスを運ぶバグ
コントロールバス(1bit): R/Wを判別するフラグ的なバス
データバス(8bit): データをやりとりするバス
メモリマップ上のmirrorという場所について。CPU RAMは2KiBのサイズしかないので11bitで十分。しかしハードウェアの都合上RAMのスペースとして[0x0000 - 0x2000](13bit分)のスペースをアドレス用に用意されている。よって上位2bitは不要なので0埋めしてアドレス変換しないといけない。この辺をエミュレータではBUSで抽象化しておくと後々楽できる。PPUも同様に[0x2008 - 0x4000]のスペースのうち[0x2000 - 0x2008]しか使わないので対応しておく。
Cartridges
カセットの中身は下記で構成されている。
iNESヘッダー。16 bytes。
512 bytesのトレイナーと呼ばれるセクション。
PRG ROM。プログラムが収められているセクション。
CHR ROM。キャラクターのデータ(スプライト情報)が収められているセクション。
iNESヘッダーの仕様はINES - NESdev Wikiを見るとわかる。5byte,6byte目がそれぞれPRG ROMのサイズ、CHR ROMのサイズになっている。これを使ってカセットからそれぞれのROMデータを抽出してメモリにロードしていく。今回はiNES1.0のフォーマットだけ実装する。
https://scrapbox.io/files/6369ba66657c97001d21b4cb.png
参照: Cartridges - Writing NES Emulator in Rust
実装の流れはiNESヘッダーからPRG ROMとCHR ROMを抽出してメモリの所定の位置にロードするパース処理を書くこと。あとはBusにカセットのRomを渡してCPUにロードさせる処理に手を加えるだけ。
Running our first test ROM
コミュニティの尽力によりNESのためのテストケースが大量に用意されている。Emulator tests - NESdev Wikiを見ると各種デバイスやユースケース向けのテストが準備されていることがわかる。これらを使ってエミュレータが正しく実装されているか確認してみる。
まずはCPUの基本機能が正しく実装されているかを下記のnesファイルでテスト。
http://nickmass.com/images/nestest.nes
code:sh
cargo run > mynes.log
cat nestest.log | awk '{print substr($0,0, 73)}' > nestest_no_cycle.log
diff -y mynes.log nestest_no_cycle.log | grep ">" | less
正常に実装できていれば0xC6BC行目まではdiffが出ないらしいがそれ以前で止まっているので実装が間違っているっぽい...。
PPUのmem_readがtodo!になってたがテストでは無視して0を返すようにした
program_counterにreset後にself.program_counter = 0xC000をセットする必要があった。
AddressingModeのAccumulator/Relative/IndirectはNoneAddresssingModeとして扱って問題なかったので修正
JMP/JSR命令(0x4c,0x6c,0x20)はAddressingModeがAbsoluteだがNoneAddressingModeとして扱った方が良いので修正
PHP命令でstatusの値を使うだけで良いのにstatusの値まで更新してしまっていたバグを修正
PLP命令でstatusのBREAKフラグを設定し忘れていたバグを修正
SBC命令のoverflowフラグを立てる条件式が間違っていたので修正
0xC6BC行目から先で実行が失敗する原因はまだ未実装の命令にぶち当たったから。なんと6502には非公式のCPU命令があと110個あるらしい。なんやねん非公式て...。ダルすぎるが大抵のゲームがこれらの命令を使っているらしいので実装していく。
Unofficial Opcodes
NOP/LAX/SAX/SBC/DCP/ISB/SLO/RLA/SRE/RRAはテストログと見比べながらステップバイステップで実装していける。
それ以外の非公式Opcodesはテストケースにすら乗ってないので参照実装から拝借した。
Emulating Picture Processing Unit
Emulating PPU Registers
PPUの実装の流れ
レジスタとNMI割り込みの実装
NMI割り込み(non-maskable interrupt): CPU側でマスクできない割り込み
CHR ROMからパースとタイルの描画
PPUの状態(背景タイルとスプライト)をレンダリングする
スクロールの実装
PPUの構成
https://scrapbox.io/files/636d0bea73aa3c001de75835.png
参照: Emulating PPU Registers - Writing NES Emulator in Rust
レジスタ
8bit。CPUとのインタラクションに使われる。
内部メモリ
OAM: 256 B
Palette: 8 B
外部メモリ
VRAM: 2 KB
レジスタについて(CPUアドレス空間の0x2000~0x2007。PPUではない。)
0x2000(Controller)はPPUのロジック制御で使う
0x2001(Mask)はスプライトと背景のレンダリングするための制御で使う
0x2002(Status)はPPUの状態を報告するために使う
0x2003(OAM Address)と0x2004(OAM Data)はスプライトの状態を保持する内部メモリ(OAM)を制御するために使う
0x2005(Scroll)はviewportをセットするために使う
0x2006(アドレス)と0x2007(データ)はPPUのメモリマップへのアクセスに使う
CPUとPPUのやりとり
CPUはPPUレジスタ(0x2006と0x2007)を通じてPPUと相互にやりとりする
PPUはNMI割り込みをCPUに送る
PPUの描画サイクル
PPUはフレーム毎に262ラインレンダリングする
NESの解像度は320x240
よって262-241=21ラインは画面に見えることはない
241ライン目の描画に入るとVBlankというNMI割り込みがCPUに送られる
この間PPUのメモリは変更されないのでCPUかアクセスしたりできるようになるのでここで画面の状態を更新したい次のフレームのための準備をする
CPUからPPUへのメモリアクセスを行うときの少し奇妙な仕様(バッファさせるためにダミーのreadが必要)
https://scrapbox.io/files/636df1e1c899b8001d832f0f.png
参照: Emulating PPU Registers - Writing NES Emulator in Rust
例えばCPUがPPUを通じてCHR ROMの0x0600にアクセスしたい場合
まず対象のアドレス(0x0600)を2byteずつ2回に分けて命令(LDA #$06; STA $2006とLDA #$00; STA $2006)を実行し0x2006に書き込む
0x2007からデータを読み込む
内部的にはCHR ROMのデータがPPUのバッファにロードされる
もう一度0x2007からデータを読み込む
PPUのバッファにロードされたデータを読み込む
Rust的な話だがread_dataする時に2回読み込むためvramのaddrをインクリメントするからreadだけなはずなのにmutableな実装にする必要があり微妙な実装になる
レジスタから実装をしていく
Controllerレジスタではbitflags crateを導入した。
もっと早く導入しておけばよかった感...。CPUの実装で使えばよかった。
参照実装の意味が理解できないのでPPU registers - NESdev Wikiを読んでいく
読んでもあんまりイメージが湧かない...
VRAM Mirroringについて
4画面分の領域を確保することでスクローリングを可能にする
https://scrapbox.io/files/637091d736904b00205fc06a.gif
参照: PPU scrolling - NESdev Wiki
https://scrapbox.io/files/636e5ddfc7311f001d1f2db4.png
参照: Emulating PPU Registers - Writing NES Emulator in Rust
NESは一画面の状態を表現するのに1KBのVRAMを使用する。VRAMは2KBなのでつまり2画面分の状態を保持できる。
VRAMのメモリマップとして0x2000~0x3F00(4 KB分)がNametableとして予約されている。これはつまり2KB分(2画面分)余裕がある。これらの余分な領域も既存のVRAMにマッピングする必要がある。
このマッピングの仕方はiNES Headerの内容によってHorizontal MirroringとVertical Mirroringで2種類ある
PPUがグラフィックを画面に表示する実装があまり見えていないので全体像を情報を整理する
画面に何かを表示するには下記を設定すれば良い
0x2000~0x2400のネームテーブル
0x3F00~0x3F1Fのパレットテーブル
属性テーブル
実装の肝としてはCPUからPPUのメモリへ如何に命令を伝えるか
CPUからPPUのVRAMへは直接アクセスできない
ではどうするかというとPPUのレジスタを通じてCPUからPPUのVRAMへ書き込みを行う
PPUのレジスタはCPUから見て0x2000~0x2007番地に配置されている
CPUからPPUのレジスタの0x2006のアドレスに書き込み、PPUのレジスタの0x2007のアドレスにデータ読み書きを行う
PPUの画面の構成が詳細に書かれた記事
ファミコンのグラフィックスの省メモリ化テクニックとは? | POSTD
タイル: 32 * 30 = 960
ブロック: 16 * 15 = 240
CHR: 色や位置の情報を持たない未加工のピクセルアートで、タイルとして表現される。
ネームテーブル: 画面上の所定の位置にCHRのタイルを割り当てるもの。それぞれのポジションは1バイトで表す。
パレット: レンダリングに使用できる色の情報。64色。1つの画像に4パレット分まで使用可能。
属性: 1ブロックあたり2ビットが割り当てられていて、4つのパレットのうちどれを使うかを示すもの。
CHR、ネームテーブル、パレット、属性を含む1つの完全な画像を表現するのに必要なのは、4096+960+16+64 = 5136バイト
Emulating Interrupts
割り込みとは
CPUが何かしらの外部からのイベントにより一連のCPUの処理を中断するためのメカニズムのこと
NMI割り込み
PPUのVBLANKの間(1フレーム中にやるべきPPUのレンダリングが終わった後)に発せられる割り込み
CPUはこの割り込みの間に現在のフレームの更新が行える
CPUはこの割り込み命令を無視できない
詳細
PPUはフレーム毎に262回ラインをレンダリングする
各ラインごとのPPUのクロックサイクとは341。
241回目のスキャンラインからPPUはNMI割り込みを発し始める
1CPUクロックサイクル = 3PPUクロックサイクルになる
クロックサイクル
PPUとCPUのクロックサイクルは同じではない。よってこの二つのクロックサイクルを合わせるように調整する処理が必要。このフェーズのタイミング調整がNES最難関の一つ。
PPUはCPUのクロックサイクルの3倍。APUはCPUの2分の1。つまりCPU:PPU:APU = 2:6:1。
PPUのフレーム毎に341 * 262 = 89342 PPU クロックサイクルになる
CPUは~29780CPUサイクルごとにNMI割り込みを受け取ることが保証されている
CPU/PPU/APUなど他のコンポーネントを並行に実行してそれらをクロックサイクルで適切に同期させる方法
https://scrapbox.io/files/6371a30816e4c8001dc0caeb.png
参照: Emulating NMI Interrupt - Writing NES Emulator in Rust
Catch-upというテクニックを使う
1スレッドで実行する
CPUの実行する命令のクロックサイクルをPPUへ渡す
PPUは受け取ったサイクル数からどうレンダリングすべきかを計算する
CPU -> PPU のクロックサイクルの伝達の実装を行う
CPUとPPUのクロックサイクルの概要
PPUは262行を1フレームで描画する
1行は341クロックで構成される
1クロックは3CPUクロックで構成される
241行目から262行目まではVBlank期間
やるべきこと
CPU
命令実行毎にクロックサイクルをPPUへ伝達する
命令毎に条件付きでクロック数が変わったりする場合があるので大変
例えば
アドレッシングにおいてAbsolute_X Absolute_Y Indirect_Yでページクロスが発生していたら1クロック追加
ブランチ命令において条件が成立した場合に1クロック追加、さらにページクロスが発生していたら1クロック追加
6502 Referenceのアドレッシングモードのところを見て各種命令を修正していく
Bus
CPUとPPUの間の伝達を中継する(CPUは直接PPUへアクセスできない)
PPU
CPUから伝達されたクロックサイクルを処理するtick関数
サイクル数の計算
241行目にVBLANKが始まることをNMIで知らせる
262行目にVBLANKが終わることをNMIで知らせる
PPU -> CPU の割り込みの伝達の実装を行う
現在の命令の実行を終了する
プログラムカウンタとステータスフラグをスタックに格納
ステータスレジスタPに割り込み禁止フラグをセットして、割り込みを禁止する
0xFFFAから割込みハンドラルーチンのアドレスをロード(NMI用)
プログラム・カウンタ・レジスタをそのアドレスに設定
Rendering CHR ROM Tiles
CHR Romのデータについて
https://scrapbox.io/files/6371e88b875526001e53ab11.png
参照: Rendering CHR Rom Tiles - Writing NES Emulator in Rust
総サイズ: 8 KiB
タイル: 8x8のピクセルのデータで4色(2bit)まで利用可能(背景は4色、スプライトは3色+透過(0b00))
1タイル = 8 * 8 * 2 = 128 bits = 16 bytes
8 KiB / 128 bits = 512 タイル
CHR Romからは形だけの情報しかわからない
カラー情報自体はカラーパレットで管理されておりピクセル自体は色データを持たない
カラーパレットについて
バージョンによっては発色が違ったりすることもある
Rendering Static Screen
背景とスプライト画像について
これらはどちらもCHR ROMで構成される
背景でもスプライトもどちらも同じCHR ROMを使うが、背景もスプライトもそのメモリ空間自体は別で管理される
背景を描画するアルゴリズム
コントロールレジスタからビット0とビット1を読み出すことによって、現在のスクリーンに使用されているネームテーブルを決定する
コントロールレジスタからビット4を読み出すことにより、どのCHR ROMバンクが背景タイルに使用されているかを決定する
指定されたネームテーブルから960バイトを読み出し、32x30のタイルベースの画面を描画する。
色のレンダリング
https://scrapbox.io/files/6371fb8146799d0022ba0398.png
参照: Rendering Static Screen - Writing NES Emulator in Rust
1つのタイルは1つのパレットで塗られる
Attribute Table
どの部分にどのパレットを割り当てるかが定められているテーブル
Emulating joypads
ジョイパッドの仕組み
CPUアドレスの0x4016が1P、0x4017が2P。それぞれRead/Writeができる。
7bitから0bitまでにそれぞれRIGHT/LEFT/DOWN/UP/START/SELECT/BUTTON_B/BUTTON_Aが割り当てられている
当該bitが1ならpress、0ならrelease
初期化
ボタンが押されたらレジスタの値が変わるが毎回自分で初期化する必要がある
方法はまず0x1を0x4016に書き込む。次に0x00を0x4016に書き込む。
読み込む -> 押されているか確認 -> 読み込む...を0x4016に対して8回(ボタンの個数分)読み込むを繰り返す
第6章パッド入力 | ギコ猫でもわかるファミコンプログラミングのアセンブラ解説が一番わかりやすいかも
0x4016をどうやってループで確認してるか
PPUのレンダリングが終わりNMI割り込みが発生したVBLANKの際にcpu.rsのinterrupt関数が実行される
interrupt関数のmem_read_u16で0x4016が読み込まれて値が確認される
PPU Scrolling
sprite zero hit flag
非0のピクセルのスプライト0が非0の背景のピクセルと重なったときにセットされる
このフラグが0から1に変わったとき、CPUはPPUの0~Yのラインのレンダリングが終わったと判断できる
sprite overflow flag
バグ。
スクロールの仕組み
https://scrapbox.io/files/6373528734d6da0022692cba.png
参照: PPU Scrolling - Writing NES Emulator in Rust
静的な背景に対してviewportを動かしていって動いているように見せる。マリオの背景を思い浮かべてみる。
PPUの背景のレンダリングだけいじれば良い
PPUのスクロールの仕組みはなんとなくわかったが実装がよくわからん問題
注意
既存のファミコンROMをダウンロードするのはもちろん違法なのでやらないこと。フリーのROMが下記のサイトにたくさんあるのでそれらを使う。
Homebrewn Nintendo Entertainment System Games And (Nes Roms) (www.nesworld.com)
NES実装で得られる学び
CPUが各種命令をどうやって実行していくのかが少し理解できるようになる
レジスタやメモリがどう活躍するのかが少し理解できるようになる
アセンブリが少し理解できるようになる
機械語が少し理解できるようになる
ビット演算が少し理解できるようになる
PPUという特殊な描画チップの実装を知ることができる
用語メモ
2進数 <-> 16進数
2進数と16進数は8桁を2桁にできるので便利
例えば0b10100011は0xA3。1010 = Aで0011 = 3。
上位bit、下位bit
80 00なら80が上位で00が下位。
CPU - Wikipedia
メモリマップ
CPU/PPUからデータがメモリ内のどこにどのようにアクセスされるのかを表す表
アキュムレータ
演算結果を累積する用のレジスタ。よく使われるレジスタくらいの意味で理解しておけばまずは良さそう。
ステータスレジスタ
各種繰上げ/下げ・オーバーフローなどの状態のフラグをbitで管理するレジスタ。
アドレッシングモード
ある命令のオペランドを表すための方法。
スタックポイント
メモリのスタック領域の番地を管理するやつ。プログラムカウンタやフラグの退避先アドレスなどを指したりする。push/popするとインクリメントされたりデクリメントされたりする。
プログラムカウンタ
実行されるプログラムが今どこの番地なのかを管理するやつ。
WRAM(SRAM)
CPUがstack/registerの値を保存したりゲームの状態を保持させたりするのに使われるメモリ。2KB。
VRAM(SRAM)
PPUがグラフィックのデータを保存したりするのに使うメモリ。2KB。
リソース
NES Doc
ドキュメント
YuheiNakasaka/nes-rs
自分の実装
bjne/src at master · tanakh/bjne
tanakhさんのNESエミュレーター。
fogleman/nes: NES emulator written in Go.
Golangで書かれたNES。実装の参考になる。
ファミコンエミュレータの創り方 - Hello, World!編 - - Qiita
日本語で一番有名そうなNESの記事。
Introduction - Writing NES Emulator in Rust
NESの全体像の解説。かなり良い。
bugzmanov/nes_ebook: A mini book on writing NES emulator using rust lang
上記ガイドの参照実装
6502 Introduction
6502の仕様が詳しく書いてあるサイト
下記がよく参照するページ
6502 Registers
6502 Instructions
6502 Addressing Modes
6502 Reference
PPU registers - NESdev Wiki
Programming with unofficial opcodes - NESdev Wiki
非公式の命令セット
Nintendo Entertainment System (NES) Architecture | A Practical Analysis
まとまりのあるサイト
NES研究室 - 6502
ギコ猫でもわかるファミコンプログラミング
NES on FPGA
NES 基礎知識 - CPU - 壁は通り抜けられませんよ
6502のCPU命令の日本語解説
2進数、8進数、10進数、16進数相互変換ツール
ファミコンのグラフィックスの省メモリ化テクニックとは? | POSTD
CHR ROMのレンダリングの仕組みが比較的わかりやすい
Homebrewn Nintendo Entertainment System Games And (Nes Roms) (www.nesworld.com)
フリーのファミコンROMの配布サイト
Emulator tests - NESdev Wiki
エミュレータテスト用のROMが置いてある場所
NES Graphics Explained - YouTube
Questions about NES programming and architecture - Page 6 - nesdev.org
アーキテクチャ図としてmost better
marethyu/awesome-emu-resources: A curated list of emulator development resources
awesome emulatorのリポジトリ
ThePomelo/nes-apu
spieglt/nestur: The NES (emulator) you left outside in the rain but let dry and still kind of works