ファミコンエミュレータを写経してみるお話7【DMC, SRAM(バッテリーバックアップRAM)】
前回
ファミコンエミュレータを写経してみるお話6【ROMの吸い出し、Mapper】 - 196Log
前回の振り返り
mapper
の対応をメインにカセットが動くように修正を行いました。が、音周りに未実装な部分が残っていました。セーブも行えない状況です。
参考にするもの
- コード周りはこちらのコードを観させてもらおうと思います。また
Rust
の書き方の勉強としても頼りにさせてもらいます。
github.com
ファミコンエミュレータの創り方 - Speaker Deck
- こちらに有志の皆さんが
NES
の情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。
- こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。
読み始める前に
- ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語は
Rust
を使っており参考サイトもRust
が中心のものが多いです。 - また、解説力も乏しいので、適宜実装が完了したと思われる
commit
にジャンプできるGithub
のリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。
DMC
DMC
は、サンプリングした音を鳴らす機構のようです。録音した音を再生する装置みたいな感じだと思います。本プロジェクトでは音の出力方法が本来のものと違うため、どこまで再現できるか不安ではありますが、以前参考にしたこちらのリポジトリを参考に実装してみます。
nes/apu.go at master · fogleman/nes · GitHub
そして実装はこちらのプルリクになります。
実装には
- 矩形波がうまく鳴らないホットフィックス
DMC
の仮実装
が含まれています。DMC
に関しては自分がうまく理解できなかったため、あまり良い実装になっていないのと、音が若干違うこと、再生停止時の特有のノイズが入らない問題があります。また、WebAudio
への渡し方も微妙なので、ここのあたりは後々大きく変更するかもしれません。
SRAM(バッテリーRAM)
バッテリーバックアップRAM
(正式名称不明)は、カセット側にあるRAM
で、データの記憶方法を持たないファミコンにおいて、セーブデータなどを記録しておく場所として使われているはずです。色々と策は考えられますが、現状ブラウザのJS
と繋がりがあるので、ブラウザに保存しておくのが一番簡単な方法かなと思います。今回はsave ram
から取ってSRAM
という名前で実装しています。
機能としては、(0x6000-0x7FFF)に割り当てられている8KBのRAM
をローカルストレージに保存、またロード時に自動復帰する仕組みです。ローカルストレージにはカセットデータのパス名を利用してますので、上書きの心配はないと思います。
一区切り
ここまでを通じて、mapper0
, Mapper3
, Mapper4
に加え、バックアップRAMの記録が動作するエミュレータを作成できました。私の目標はMapper4
のゲームを動かすことでしたので、ここでコンスタントな実装は終了しようと思います。
一方幾つかバグや実装残しもあります。
APU
の再実装(DMC
がひどい)。具体的にはWebAudio
をやめて、本来のNES寄りの出力方法にしたい- Spriteの描画プライオリティが正確ではない?例えば
DQ
ではスライムの影が手前に描画されている。 - デバッグのために
CPU
のレジスタログの取得など - 長時間利用で重くなることがある?
などなどが現状気になっております。以降ブログにまとめることはないとは思いますが、リポジトリは適宜更新しようかなと思ってます。(多分)
以上。
ファミコンエミュレータを写経してみるお話6【ROMの吸い出し、Mapper】
前回
ファミコンエミュレータを写経してみるお話5【keypad, sound】 - 196Log
やること
前回、キーパッドと音の実装を行ったので、ゲームを動かしつつ、バグを修正していきます。タイトルは写経となっていますが、この辺りから少し毛色が変わってきますので、動くか心配です。
参考にするもの
- コード周りはこちらのコードを観させてもらおうと思います。また
Rust
の書き方の勉強としても頼りにさせてもらいます。
ファミコンエミュレータの創り方 - Speaker Deck
- こちらに有志の皆さんが
NES
の情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。
- こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。
読み始める前に
- ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語は
Rust
を使っており参考サイトもRust
が中心のものが多いです。 - また、解説力も乏しいので、適宜実装が完了したと思われる
commit
にジャンプできるGithub
のリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。
ROMの吸い出し
有名なマリオのゲームをROM
から吸い出しました。
動きそうなので吸い出すよっ pic.twitter.com/gpRTAGrMxt
— いくちる (@196Ikuchil) March 17, 2020
吸い出す機会はこちらで購入しました。
https://www.amazon.co.jp/gp/product/B073PT9QB5/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1
吸い出しアプリケーションは製造元のサイトからダウンロードできます。Win
, Mac
共に対応していましたが、なんとなく怖かったので私は仮想Windows上で作業しました。特に問題はないかと思います。吸い出し自体もほぼ自動で情報を読み取って作業してくれました。 ちなみに、カセットにはマッパーと呼ばれるいくつかの種類に分けられるそうですが、本エミュレータは現状Mapper0(ベーシックなやつ)にしか対応してないです。
もちろん吸い出したrom
を配る分けにはいかないので、このあたりは各自で用意お願いします、、、
バグの修正
ここからは、ゲームが正常に動くように修正を繰り返しました。
後ろにしか進めないジャンプが不調なマリオ pic.twitter.com/7gEUINO2g5
— いくちる (@196Ikuchil) March 18, 2020
最終的に修正が完了したプルリクが以下になります。修正ごとにコミットをなるべく分けているので、順に見て頂けると理解できるかと思います。
主な修正内容
ざっくりと修正したことを書きます。
Sprite
の実装ミスSprite
の横幅を考慮せずに生成を行っていたため、配列にアクセス時パニックを起こしてホワイトアウトしていた。
Keypad
の実装ミスjs
からの入力に使用する領域を、適切に確保しておらず入力が上手く伝わらなかった。
- スイープの方向フラグ
- フラグの書き込み時の判定がタイポでおかしくなっていた。
- 三角波の実装ミス
- 周波数が毎回初期化でセットされていたため、音が途切れ途切れだった。
- 音が流れっぱなしになってしまうので、ボリュームが0になるタイミングを適切になるようにした。
- その他、音量などのリファクタを含む。
以上より、マリオが動くようになりました。
スイープとDMCが不完全だけど、あらかた動いたっす pic.twitter.com/apse0U9bhb
— いくちる (@196Ikuchil) March 19, 2020
DMC
等の対応ができていませんが、ひとまず動くところまで来れました。
Mapper3に対応したい
最終目標はMapper4
への対応なのですが、参考リポジトリさんはMapper3
の実装がされているので、先にこちらを練習がてら実装しようと思います。(実装量的にもかなり少ない。)
とりあえず、現状のコードをリファクタします。mapper
として機能を分けています。
次に本命のmapper3
を実装しました。
その際に気がついた、バグについてはこちらで修正しております。
無事Mapper3
のドラクエ1が動きました。が、どうやらBGMにIRQ
割り込みを利用しているそうで、現状だと音が出ません。(APUに未実装)そのため、ROM
を多少書き換える事で対応しました。
原点にして始点 pic.twitter.com/GBKpYdv43M
— いくちる (@196Ikuchil) March 26, 2020
Mapper4
続いて、Mapper4
に対応していきます。mmc3
とも呼ばれているそうです。 cpu
から見えるアドレスは既に固定されており、カセットのPRG ROMへは0x8000-0xFFFFでのみアクセスできます。つまり、カセットにどれだけソフトのプログラムを積んだところで、先の0x8000分のアドレス空間にしかアクセスできないため、全てのデータを読み出すことはできません。
これを解消するのがmapper(カセット側に搭載されている)で、mapper4ではバンクを切り替えることで、0x8000-0xFFFFのアクセスを柔軟に0x80000分ぐらいのPRGROMに振り分けています。
CHR RAMに対しても似たようにbank分けしている模様。
実行は以下のプルリクになります。IRQ
の割り込みの実装がまだだったため、はじめに実装しています。また、mapper4
ではcpuレジスタ等にアクセスが必要であったため、周辺の実装にも細かな変更が加わっています。
私は手元でff3
の画面の動作を確認できました。
やっと動いた、、! pic.twitter.com/D8HjEMRa0K
— いくちる (@196Ikuchil) April 4, 2020
積み残し
ff3
にて音が鳴らない(dmc
が未実装なのでその可能性あり。もしくは再生停止周りのバグ)Save Ram
のデータが保持されない
の2点です。Save Ram
は0x6000
からアクセスできるRamで、セーブに対応しているカセットはボタン電池にて、データが消えないように電気が流れているようです。次回はこのあたりを直していきたいと思います。
次
ファミコンエミュレータを写経してみるお話5【keypad, sound】
前回
ファミコンエミュレータを写経してみるお話4【nestest.nesの起動】 - 196Log
やること
前回はnestest.nes
を起動させ無事画面が表示されました。(既に手動でcpuテストはほぼ終わっていますが、)キーパッドによる入力ができなければ、テスト実行ができません。なので今回はキーパッドで操作できるようにします。(短いので音もチャレンジします。)
参考にするもの
- コード周りはこちらのコードを観させてもらおうと思います。また
Rust
の書き方の勉強としても頼りにさせてもらいます。
ファミコンエミュレータの創り方 - Speaker Deck
- こちらに有志の皆さんが
NES
の情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。
- こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。
読み始める前に
- ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語は
Rust
を使っており参考サイトもRust
が中心のものが多いです。 - また、解説力も乏しいので、適宜実装が完了したと思われる
commit
にジャンプできるGithub
のリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。
キーパッド
ブラウザでのキー入力を8bitにのせて、romの末尾にデータを添付する形でゲーム側に入力情報を渡しているようです。入力情報の更新は最終的に1フレームごとに行われてます。ファミコン本来では、情報の格納されたレジスタをリードするごとに対応ボタンを変更して、うまいことグルグル回しながら判定しているようです。詳しくは上の参考サイトをご覧ください、、
実装自体は参考リポジトリさんの通りの実装になっています。イベントをbutton
タグなんかにつけても良さそうですね。こちらが実装完了したリンクになります。
cpuテスト
を走らせてみたところ、全て通ってしまったので、このまま音の初期実装に映ります。ダメだった場合、前回の記事のデバッグ方法のメモを参考にしてみてください(雑ですが)
サウンドまわりの実装
結局このスライドを参考にさせてもらいます。ほんとこのスライドは要点がわかりやすく纏まっている為、実装にとっつきやすくなります。ありがたい限りです。
ファミコンエミュレータの創り方 - Speaker Deck
要点をまとめると以下の2点かと思います。
js
側で波形を生成して音を表現する。- 生成する波形の指示を
Rust
実装側から逐次js
のメソッドに伝えていく。
まず、音を表現するjs
側の実装を写経していきます。まず矩形波を実装して見ます。参考リポジトリを見ると、duty比
によって使用する波形を自分でpulse.js
に用意しているようです。duty比は
矩形波の凸凹の幅の比率だそうで、NES
では4種類を使い分けられるようです。今回js
側で操るWebAudioAPI
には、50%の矩形波しか作れないようで、このため自分で用意したと思われます。
矩形波
で、この記事によると、ノコギリ波を組み合わせると自由に矩形波を作れるようになるそうなので、参考にさせてもらおうと思います。
ひとまず矩形波Square
を実装しました。以下のリンクがその時のブランチの先頭になります。
また同時にapu
テストのROMを走らせて、動作確認をしました。
https://wiki.nesdev.com/w/index.php/Emulator_tests
付属のサンプル音源と比較すると
- 波形の切れ目がない(サンプルがあっているのかは不明)
- 音が全体的にはっきりしている
- 閾値での音階変化のテストで一部音が変化しない
の違いがありますが、ひとまず聴ける程度にはなっているはずなので、このまま進めます。
ノイズ
参考コードでは以下が正確には実装されていません。
- ノイズのシフトレジスタによるロングとショートモードの実装(乱数をjsで生成するため不要)
どうするか悩みましたが、音の再現は一番の目的から少し外れてしまうため、ここでは完璧は求めず写経するだけにします。他のリポジトリを探しましたが、タイマーなどからしっかりエミュレートしている場合が多く、再実装するには割と大規模な実装と修正になりそうでしたので、後回しにしようと思います(APU
自体も240Hz
で駆動する?分周器のみにしか対応していない。irq
割り込みも未対応)
ノイズに関しては、次のリポジトリを参考にいつでもリファクタが可能な実装を目指しました。
実装はこんな感じです。
三角波
三角波は矩形波よりも若干楽に実装可能かと思います。今までの実装がわかればなんとかなります。
これでDCPM
以外は最低限の音が出るようになりました。(DCPM
は余力があれば後日)
DMCP
は、今回は未実装となりました。
まとめ
今回はキーパッドとサウンド周りを実装しました。特にサウンド周りは構造を理解するのが難しく、まだ理解しきっていません。途中に貼ったGo
で書かれたエミュレータのリポジトリが、サイトにまとめらている資料により近い実装だったので、そちらを見た方が理解が早いかもしれません。
とっかかりは矩形波のwrite
メソッド周りから初めて、レジスタがどのように使われているのか、なんとなく掴めると良いかなと思います。
次回
ロムを吸い出したり、バグがあれば修正したり、しようかなと思っていますが、未定です。
次
ファミコンエミュレータを写経してみるお話6【ROMの吸い出し、Mapper】 - 196Log
追記
次回でも書きますが、今回の実装にいくつか修正がありました。下のリンク先のプルリクで修正しているので該当コードを見てみて下さい。
ファミコンエミュレータを写経してみるお話4【nestest.nesの起動】
前回
ファミコンエミュレータを写経してみるお話3【HelloWorld(sample1.nes)の実行】 - 196Log
前回は
sample1.nes
を起動させ、ひとまず橋渡し系の実装はできていたことが確認できたので、いよいよ実装自体を修正していきます。使うのはnestest.nes
ですが、起動させようにもまず画面が表示されませんでした、、
参考にするもの
- コード周りはこちらのコードを観させてもらおうと思います。また
Rust
の書き方の勉強としても頼りにさせてもらいます。
github.com
ファミコンエミュレータの創り方 - Speaker Deck
- こちらに有志の皆さんが
NES
の情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。
- こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。
読み始める前に
- ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語は
Rust
を使っており参考サイトもRust
が中心のものが多いです。 - また、解説力も乏しいので、適宜実装が完了したと思われる
commit
にジャンプできるGithub
のリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。
デバッグの開始
どうやらPC
を0xC000にすることで、画面が見えなくても機能テストをやってくれるモードがある模様です。ありがたすぎる。ログが下のサイトに一緒に載っているので、そちらと自前で仕込んだログを見比べながら修正していくそうです。
デバッグの仕方
cpu
周りであれば実行される命令を逐次表示すると良いと思います。下は先で紹介したログとの比較に使いました。(正確には参考リポジトリのコードにもprintln
を仕込んで自分のと照らし合わせていたので、先で紹介したログと若干結果が違うかもしれない。)
print!("{:x} ", register.get_PC()); let code = fetch(register, cpu_bus); print!("cpde:{:x}", code); let ref opemap = opecodes::OPEMAP; let code = &*opemap.get(&code).unwrap(); let operand = fetch_operand(&code, register, cpu_bus); println!("opecode = {:?},operand = {:x},A:{:x},X:{:x}.Y:{:x},statys:{:x},SP:{:x}", code, operand, register.get_A(),register.get_X(), register.get_Y(),register.get_Status(),register.get_S());
ppu
周りは今回はあまりデバッグできていないのですが、僕の場合はとりあえず画面が表示されなかったので、そこまではnestest.nes
のバイナリを読んで、どの命令で詰まっているのか確認しました。エントリーポイントは0xC00C
でした。
ちなみに、画面なしのデバッグ0xC000
の場合もそうですが、今回の実装は起動時にreset
のメソッドを呼んでいるので、register
のnew
内ではなくreset
でPC
を書き換えることで任意の場所から実行することが可能です。
変更点
詳細はリンク先のプルリクエストのコミットから確認可能になっているはずです。このプルリクエストでnestest.nes
の画面が表示できるようになります。(サウンド、キーパッドは未実装) 既に上手く起動している方はあまり関係ないかもしれません。
下にいくつか代表的な変更を書きます。
opecode, fetch命令の修正
サイクル数が間違っていたり、そもそも挙動が違っていた箇所を修正しています。txs
に関しては研究室のページの説明がおそらく間違っていた気がします。
NES研究室 - 6502
不足分の命令
ドキュメントに載っていない命令が出てくるのでそれも実装します。(実装面は参考リポジトリ頼りでしたが、上のログにも出てきているので発見はできるかと思います。)
clear_sprite_hit
ppu
レジスタのメソッドが大きく違っていました。これが原因で画面が表示される1日潰しました。
起動
起動しました。心が折れる前に動いてよかったです。参考リポジトリのプロジェクトにデバッグコードを仕込んで、自分の実装とどう動きが違うのか確認するのが正直手っ取り早いですが、バイナリ呼んで変な動きを探す方がカッコ良いです。。笑
次回
ようやく画面が出てきたので、次はキーパッドを実装してnestest
でcpu
のデバッグができるようにしようと思います。
次
ファミコンエミュレータを写経してみるお話3【HelloWorld(sample1.nes)の実行】
前回
ファミコンエミュレータを写経してみるお話2【PPU】 - 196Log
やること
前回はPPU
の機能を実装しましたが、まだ動作確認ができていないので、CPU
と合わせてテストROMが動くようにしようと思います。まずはこちらのsample1.nes
を起動させてみます。hello word!
と表示されるものです。他のromはおそらく動きません。
参考にするもの
- コード周りはこちらのコードを観させてもらおうと思います。また
Rust
の書き方の勉強としても頼りにさせてもらいます。
github.com
ファミコンエミュレータの創り方 - Speaker Deck
- こちらに有志の皆さんが
NES
の情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。
- こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。
読み始める前に
- ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語は
Rust
を使っており参考サイトもRust
が中心のものが多いです。 - また、解説力も乏しいので、適宜実装が完了したと思われる
commit
にジャンプできるGithub
のリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。
修正
sample1.nes
を呼び出すために修正した箇所があります。まだ修正してない場合は参照してください。
【Fix】Hello world bug fix by 196Ikuchil · Pull Request #7 · 196Ikuchil/nes_emulator · GitHub
ひとまず呼び出そうとしてみる
現状頑張って機能を使おうとした場合、何が足りないのか把握します。下のリンクは軽い修正も含めて今までのものを実装したものになります。
renderer
という生成したスプライトを描画する機能- カセットを読み込む機能(
ROM
,parse
,Cassette
) - 上を操る
js
側の実装全般
カセットのデータ
Bus
も通さなければいけないので、最初にカセットの読み込みを実装するのが良さそうです。下のリンクではカセットデータをRom
へ流し込み、エミュレータのContext
内で扱えるように実装しなおしています。コミット履歴から、カセットのパーサーやRom
の実装についても確認可能です。
描画担当
次はrenderer
を実装します。これは描画を担当する部分です。PPU
で生成したデータを取り扱ってくれるようにします。
環境を整える
Rust
の成果物をjs
から呼び出して、ブラウザ上で動作させるようにします。npm
によって入れるものはローカルで成果物を走らせてブラウザで表示するために使用します。main.rs
には、js
側から呼び出すrun
メソッドを実装します。Makefile
を作りmake
でビルド等を走らせます。.cargo/config
に、ビルドに必要な設定を記述しておきましょう。
ビルドの際に必要になるものは以下なので、PCにインストールしておいてください。
- `nodejs` - `cmake` - `emscripten`(emcc) - `rustup`
externs
にはjs
側が公開しているメソッドをrust
側から呼び出すためのことを記述しています。起動実行にはemscripten
が用意したメソッドを、描画にはcanvas_render
というメソッドを呼び出せるようにしています。
最終的にプルリクにまとめているので、こちらを見た方が早いかもしれません。
sample1.nes
は起動しますが、他はまだ起動しないでしょう。
デバッグに関して
起動しない場合、まずCPU
の実装を疑うべきですが、初回はbackground
の値を出力してみるべきと思います。それにより描画部分かそれ以前かに分けられるので、以前であればtile
あたりのデータを覗いてみましょう。
そこにもデータが上手く反映されていなければ、CPU
の実装ミスを疑います。特に今回のROMは短いので、バイナリエディタと合わせて、実装される
opecodeを追っていくことで、
CPUの実装ミスに気がつけると思います。
<br>
それがダメであれば
PPUの実装をみますが、この辺りは単独では大変なので、僕は参考コードと自分の実装、同じ位置に
println!`をおき、値をくらべながら違う値が扱われている箇所を探しました。実装がほぼ同じであれば、意外と有効な技なので、試してみてください。
次回
次回はデバッグを行うためにnestest.nes
を起動させ(るためにデバッグを行います。
次
ファミコンエミュレータを写経してみるお話4【nestest.nesの起動】 - 196Log