セキュリティ系の勉強・その他開発メモとか雑談. Twitter, ブログカテゴリ一覧
本ブログはあくまでセキュリティに関する情報共有の一環として作成したものであり,公開されているシステム等に許可なく実行するなど、違法な行為を助長するものではありません.

ファミコンエミュレータを写経してみるお話7【DMC, SRAM(バッテリーバックアップRAM)】

前回
ファミコンエミュレータを写経してみるお話6【ROMの吸い出し、Mapper】 - 196Log

前回の振り返り

mapperの対応をメインにカセットが動くように修正を行いました。が、音周りに未実装な部分が残っていました。セーブも行えない状況です。

参考にするもの

  • コード周りはこちらのコードを観させてもらおうと思います。またRustの書き方の勉強としても頼りにさせてもらいます。
    github.com

ファミコンエミュレータの創り方 - Speaker Deck

  • こちらに有志の皆さんがNESの情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。

hp.vector.co.jp

pgate1.at-ninja.jp

  • こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。

booth.pm

読み始める前に

  • ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語はRustを使っており参考サイトもRustが中心のものが多いです。
  • また、解説力も乏しいので、適宜実装が完了したと思われるcommitにジャンプできるGithubのリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。

DMC

DMCは、サンプリングした音を鳴らす機構のようです。録音した音を再生する装置みたいな感じだと思います。本プロジェクトでは音の出力方法が本来のものと違うため、どこまで再現できるか不安ではありますが、以前参考にしたこちらのリポジトリを参考に実装してみます。

nes/apu.go at master · fogleman/nes · GitHub
そして実装はこちらのプルリクになります。

github.com


実装には

  • 矩形波がうまく鳴らないホットフィックス
  • DMCの仮実装

が含まれています。DMCに関しては自分がうまく理解できなかったため、あまり良い実装になっていないのと、音が若干違うこと、再生停止時の特有のノイズが入らない問題があります。また、WebAudioへの渡し方も微妙なので、ここのあたりは後々大きく変更するかもしれません。


SRAM(バッテリーRAM)

バッテリーバックアップRAM(正式名称不明)は、カセット側にあるRAMで、データの記憶方法を持たないファミコンにおいて、セーブデータなどを記録しておく場所として使われているはずです。色々と策は考えられますが、現状ブラウザのJSと繋がりがあるので、ブラウザに保存しておくのが一番簡単な方法かなと思います。今回はsave ramから取ってSRAMという名前で実装しています。

github.com


機能としては、(0x6000-0x7FFF)に割り当てられている8KBのRAMをローカルストレージに保存、またロード時に自動復帰する仕組みです。ローカルストレージにはカセットデータのパス名を利用してますので、上書きの心配はないと思います。


一区切り

ここまでを通じて、mapper0, Mapper3, Mapper4に加え、バックアップRAMの記録が動作するエミュレータを作成できました。私の目標はMapper4のゲームを動かすことでしたので、ここでコンスタントな実装は終了しようと思います。

一方幾つかバグや実装残しもあります。

  • APUの再実装(DMCがひどい)。具体的にはWebAudioをやめて、本来のNES寄りの出力方法にしたい
  • Spriteの描画プライオリティが正確ではない?例えばDQではスライムの影が手前に描画されている。
  • デバッグのためにCPUレジスタログの取得など
  • 長時間利用で重くなることがある?

などなどが現状気になっております。以降ブログにまとめることはないとは思いますが、リポジトリは適宜更新しようかなと思ってます。(多分)

以上。

ファミコンエミュレータを写経してみるお話6【ROMの吸い出し、Mapper】

前回

ファミコンエミュレータを写経してみるお話5【keypad, sound】 - 196Log


やること

前回、キーパッドと音の実装を行ったので、ゲームを動かしつつ、バグを修正していきます。タイトルは写経となっていますが、この辺りから少し毛色が変わってきますので、動くか心配です。

参考にするもの

  • コード周りはこちらのコードを観させてもらおうと思います。またRustの書き方の勉強としても頼りにさせてもらいます。

github.com

ファミコンエミュレータの創り方 - Speaker Deck

  • こちらに有志の皆さんがNESの情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。

hp.vector.co.jp

pgate1.at-ninja.jp

  • こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。

booth.pm

読み始める前に

  • ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語はRustを使っており参考サイトもRustが中心のものが多いです。
  • また、解説力も乏しいので、適宜実装が完了したと思われるcommitにジャンプできるGithubのリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。

ROMの吸い出し

有名なマリオのゲームをROMから吸い出しました。

吸い出す機会はこちらで購入しました。

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を配る分けにはいかないので、このあたりは各自で用意お願いします、、、

バグの修正

ここからは、ゲームが正常に動くように修正を繰り返しました。


最終的に修正が完了したプルリクが以下になります。修正ごとにコミットをなるべく分けているので、順に見て頂けると理解できるかと思います。

github.com

主な修正内容

ざっくりと修正したことを書きます。

  • Spriteの実装ミス
    • Spriteの横幅を考慮せずに生成を行っていたため、配列にアクセス時パニックを起こしてホワイトアウトしていた。
  • Keypadの実装ミス
    • jsからの入力に使用する領域を、適切に確保しておらず入力が上手く伝わらなかった。
  • スイープの方向フラグ
    • フラグの書き込み時の判定がタイポでおかしくなっていた。
  • 三角波の実装ミス
    • 周波数が毎回初期化でセットされていたため、音が途切れ途切れだった。
    • 音が流れっぱなしになってしまうので、ボリュームが0になるタイミングを適切になるようにした。
    • その他、音量などのリファクタを含む。

以上より、マリオが動くようになりました。

DMC等の対応ができていませんが、ひとまず動くところまで来れました。

Mapper3に対応したい

最終目標はMapper4への対応なのですが、参考リポジトリさんはMapper3の実装がされているので、先にこちらを練習がてら実装しようと思います。(実装量的にもかなり少ない。)
とりあえず、現状のコードをリファクタします。mapperとして機能を分けています。

github.com

次に本命のmapper3を実装しました。

github.com


その際に気がついた、バグについてはこちらで修正しております。

github.com


無事Mapper3ドラクエ1が動きました。が、どうやらBGMにIRQ割り込みを利用しているそうで、現状だと音が出ません。(APUに未実装)そのため、ROMを多少書き換える事で対応しました。


Mapper4

続いて、Mapper4に対応していきます。mmc3とも呼ばれているそうです。 cpuから見えるアドレスは既に固定されており、カセットのPRG ROMへは0x8000-0xFFFFでのみアクセスできます。つまり、カセットにどれだけソフトのプログラムを積んだところで、先の0x8000分のアドレス空間にしかアクセスできないため、全てのデータを読み出すことはできません。

これを解消するのがmapper(カセット側に搭載されている)で、mapper4ではバンクを切り替えることで、0x8000-0xFFFFのアクセスを柔軟に0x80000分ぐらいのPRGROMに振り分けています。
CHR RAMに対しても似たようにbank分けしている模様。

MMC3 - Nesdev wiki


実行は以下のプルリクになります。IRQの割り込みの実装がまだだったため、はじめに実装しています。また、mapper4ではcpuレジスタ等にアクセスが必要であったため、周辺の実装にも細かな変更が加わっています。

github.com

私は手元でff3の画面の動作を確認できました。


積み残し

  • ff3にて音が鳴らない(dmcが未実装なのでその可能性あり。もしくは再生停止周りのバグ)
  • Save Ramのデータが保持されない

の2点です。Save Ram0x6000からアクセスできるRamで、セーブに対応しているカセットはボタン電池にて、データが消えないように電気が流れているようです。次回はこのあたりを直していきたいと思います。

ファミコンエミュレータを写経してみるお話7【DMC, SRAM(バッテリーバックアップRAM)】 - 196Log

ファミコンエミュレータを写経してみるお話5【keypad, sound】

前回

ファミコンエミュレータを写経してみるお話4【nestest.nesの起動】 - 196Log

やること

前回はnestest.nesを起動させ無事画面が表示されました。(既に手動でcpuテストはほぼ終わっていますが、)キーパッドによる入力ができなければ、テスト実行ができません。なので今回はキーパッドで操作できるようにします。(短いので音もチャレンジします。)

参考にするもの

  • コード周りはこちらのコードを観させてもらおうと思います。またRustの書き方の勉強としても頼りにさせてもらいます。

github.com

ファミコンエミュレータの創り方 - Speaker Deck

  • こちらに有志の皆さんがNESの情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。

hp.vector.co.jp

pgate1.at-ninja.jp

  • こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。

booth.pm

読み始める前に

  • ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語はRustを使っており参考サイトもRustが中心のものが多いです。
  • また、解説力も乏しいので、適宜実装が完了したと思われるcommitにジャンプできるGithubのリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。

キーパッド

ブラウザでのキー入力を8bitにのせて、romの末尾にデータを添付する形でゲーム側に入力情報を渡しているようです。入力情報の更新は最終的に1フレームごとに行われてます。ファミコン本来では、情報の格納されたレジスタをリードするごとに対応ボタンを変更して、うまいことグルグル回しながら判定しているようです。詳しくは上の参考サイトをご覧ください、、

実装自体は参考リポジトリさんの通りの実装になっています。イベントをbuttonタグなんかにつけても良さそうですね。こちらが実装完了したリンクになります。

github.com


cpuテスト

を走らせてみたところ、全て通ってしまったので、このまま音の初期実装に映ります。ダメだった場合、前回の記事のデバッグ方法のメモを参考にしてみてください(雑ですが)
f:id:thinline196:20200310230420p:plain

サウンドまわりの実装

結局このスライドを参考にさせてもらいます。ほんとこのスライドは要点がわかりやすく纏まっている為、実装にとっつきやすくなります。ありがたい限りです。

ファミコンエミュレータの創り方 - Speaker Deck
要点をまとめると以下の2点かと思います。

  • js側で波形を生成して音を表現する。
  • 生成する波形の指示をRust実装側から逐次jsのメソッドに伝えていく。

まず、音を表現するjs側の実装を写経していきます。まず矩形波を実装して見ます。参考リポジトリを見ると、duty比によって使用する波形を自分でpulse.jsに用意しているようです。duty比は矩形波の凸凹の幅の比率だそうで、NESでは4種類を使い分けられるようです。今回js側で操るWebAudioAPIには、50%の矩形波しか作れないようで、このため自分で用意したと思われます。


矩形波

で、この記事によると、ノコギリ波を組み合わせると自由に矩形波を作れるようになるそうなので、参考にさせてもらおうと思います。

tyfkda.github.io


ひとまず矩形波Squareを実装しました。以下のリンクがその時のブランチの先頭になります。

github.com

また同時にapuテストのROMを走らせて、動作確認をしました。

https://wiki.nesdev.com/w/index.php/Emulator_tests

付属のサンプル音源と比較すると

  • 波形の切れ目がない(サンプルがあっているのかは不明)
  • 音が全体的にはっきりしている
  • 閾値での音階変化のテストで一部音が変化しない

の違いがありますが、ひとまず聴ける程度にはなっているはずなので、このまま進めます。

ノイズ

参考コードでは以下が正確には実装されていません。

  • ノイズのシフトレジスタによるロングとショートモードの実装(乱数をjsで生成するため不要)

どうするか悩みましたが、音の再現は一番の目的から少し外れてしまうため、ここでは完璧は求めず写経するだけにします。他のリポジトリを探しましたが、タイマーなどからしっかりエミュレートしている場合が多く、再実装するには割と大規模な実装と修正になりそうでしたので、後回しにしようと思います(APU自体も240Hzで駆動する?分周器のみにしか対応していない。irq割り込みも未対応)

ノイズに関しては、次のリポジトリを参考にいつでもリファクタが可能な実装を目指しました。

github.com

実装はこんな感じです。

github.com

三角波

三角波矩形波よりも若干楽に実装可能かと思います。今までの実装がわかればなんとかなります。

github.com

これでDCPM以外は最低限の音が出るようになりました。(DCPMは余力があれば後日) DMCPは、今回は未実装となりました。

まとめ

今回はキーパッドサウンド周りを実装しました。特にサウンド周りは構造を理解するのが難しく、まだ理解しきっていません。途中に貼ったGoで書かれたエミュレータリポジトリが、サイトにまとめらている資料により近い実装だったので、そちらを見た方が理解が早いかもしれません。
とっかかりは矩形波writeメソッド周りから初めて、レジスタがどのように使われているのか、なんとなく掴めると良いかなと思います。

github.com

次回

ロムを吸い出したり、バグがあれば修正したり、しようかなと思っていますが、未定です。

ファミコンエミュレータを写経してみるお話6【ROMの吸い出し、Mapper】 - 196Log

追記

次回でも書きますが、今回の実装にいくつか修正がありました。下のリンク先のプルリクで修正しているので該当コードを見てみて下さい。

github.com

ファミコンエミュレータを写経してみるお話4【nestest.nesの起動】

前回

ファミコンエミュレータを写経してみるお話3【HelloWorld(sample1.nes)の実行】 - 196Log

前回は

sample1.nesを起動させ、ひとまず橋渡し系の実装はできていたことが確認できたので、いよいよ実装自体を修正していきます。使うのはnestest.nesですが、起動させようにもまず画面が表示されませんでした、、

参考にするもの

  • コード周りはこちらのコードを観させてもらおうと思います。またRustの書き方の勉強としても頼りにさせてもらいます。
    github.com

ファミコンエミュレータの創り方 - Speaker Deck

  • こちらに有志の皆さんがNESの情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。

hp.vector.co.jp

pgate1.at-ninja.jp

  • こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。

booth.pm

読み始める前に

  • ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語はRustを使っており参考サイトもRustが中心のものが多いです。
  • また、解説力も乏しいので、適宜実装が完了したと思われるcommitにジャンプできるGithubのリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。

デバッグの開始

どうやらPCを0xC000にすることで、画面が見えなくても機能テストをやってくれるモードがある模様です。ありがたすぎる。ログが下のサイトに一緒に載っているので、そちらと自前で仕込んだログを見比べながら修正していくそうです。

Emulator tests - Nesdev wiki

デバッグの仕方

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のメソッドを呼んでいるので、registernew内ではなくresetPCを書き換えることで任意の場所から実行することが可能です。

変更点

詳細はリンク先のプルリクエストのコミットから確認可能になっているはずです。このプルリクエストでnestest.nesの画面が表示できるようになります。(サウンドキーパッドは未実装) 既に上手く起動している方はあまり関係ないかもしれません。

github.com

下にいくつか代表的な変更を書きます。

opecode, fetch命令の修正

サイクル数が間違っていたり、そもそも挙動が違っていた箇所を修正しています。txsに関しては研究室のページの説明がおそらく間違っていた気がします。 NES研究室 - 6502

不足分の命令

ドキュメントに載っていない命令が出てくるのでそれも実装します。(実装面は参考リポジトリ頼りでしたが、上のログにも出てきているので発見はできるかと思います。)

clear_sprite_hit

ppuレジスタのメソッドが大きく違っていました。これが原因で画面が表示される1日潰しました。

起動

起動しました。心が折れる前に動いてよかったです。参考リポジトリのプロジェクトにデバッグコードを仕込んで、自分の実装とどう動きが違うのか確認するのが正直手っ取り早いですが、バイナリ呼んで変な動きを探す方がカッコ良いです。。笑
f:id:thinline196:20200310210534p:plain

次回

ようやく画面が出てきたので、次はキーパッドを実装してnestestcpuデバッグができるようにしようと思います。 次

ファミコンエミュレータを写経してみるお話5【keypad, sound】 - 196Log

ファミコンエミュレータを写経してみるお話3【HelloWorld(sample1.nes)の実行】

前回

ファミコンエミュレータを写経してみるお話2【PPU】 - 196Log

やること

前回はPPUの機能を実装しましたが、まだ動作確認ができていないので、CPUと合わせてテストROMが動くようにしようと思います。まずはこちらのsample1.nesを起動させてみます。hello word!と表示されるものです。他のromはおそらく動きません。 f:id:thinline196:20200309165322p:plain

NES研究室 - サンプル


参考にするもの

  • コード周りはこちらのコードを観させてもらおうと思います。またRustの書き方の勉強としても頼りにさせてもらいます。
    github.com

ファミコンエミュレータの創り方 - Speaker Deck

  • こちらに有志の皆さんがNESの情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。

hp.vector.co.jp

pgate1.at-ninja.jp

  • こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。

booth.pm

読み始める前に

  • ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語はRustを使っており参考サイトもRustが中心のものが多いです。
  • また、解説力も乏しいので、適宜実装が完了したと思われるcommitにジャンプできるGithubのリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。

修正

sample1.nesを呼び出すために修正した箇所があります。まだ修正してない場合は参照してください。

【Fix】Hello world bug fix by 196Ikuchil · Pull Request #7 · 196Ikuchil/nes_emulator · GitHub


ひとまず呼び出そうとしてみる

現状頑張って機能を使おうとした場合、何が足りないのか把握します。下のリンクは軽い修正も含めて今までのものを実装したものになります。

github.com

  • rendererという生成したスプライトを描画する機能
  • カセットを読み込む機能(ROM, parse, Cassette
  • 上を操るjs側の実装全般

カセットのデータ

Busも通さなければいけないので、最初にカセットの読み込みを実装するのが良さそうです。下のリンクではカセットデータをRomへ流し込み、エミュレータContext内で扱えるように実装しなおしています。コミット履歴から、カセットのパーサーやRomの実装についても確認可能です。

github.com


描画担当

次はrendererを実装します。これは描画を担当する部分です。PPUで生成したデータを取り扱ってくれるようにします。

github.com

環境を整える

Rustの成果物をjsから呼び出して、ブラウザ上で動作させるようにします。npmによって入れるものはローカルで成果物を走らせてブラウザで表示するために使用します。main.rsには、js側から呼び出すrunメソッドを実装します。Makefileを作りmakeでビルド等を走らせます。.cargo/configに、ビルドに必要な設定を記述しておきましょう。

ビルドの際に必要になるものは以下なので、PCにインストールしておいてください。

- `nodejs`
- `cmake`
- `emscripten`(emcc)
- `rustup`

externsにはjs側が公開しているメソッドをrust側から呼び出すためのことを記述しています。起動実行にはemscriptenが用意したメソッドを、描画にはcanvas_renderというメソッドを呼び出せるようにしています。
最終的にプルリクにまとめているので、こちらを見た方が早いかもしれません。

github.com

sample1.nesは起動しますが、他はまだ起動しないでしょう。

デバッグに関して

起動しない場合、まずCPUの実装を疑うべきですが、初回はbackgroundの値を出力してみるべきと思います。それにより描画部分かそれ以前かに分けられるので、以前であればtileあたりのデータを覗いてみましょう。

そこにもデータが上手く反映されていなければ、CPUの実装ミスを疑います。特に今回のROMは短いので、バイナリエディタと合わせて、実装されるopecodeを追っていくことで、CPUの実装ミスに気がつけると思います。 <br> それがダメであればPPUの実装をみますが、この辺りは単独では大変なので、僕は参考コードと自分の実装、同じ位置にprintln!`をおき、値をくらべながら違う値が扱われている箇所を探しました。実装がほぼ同じであれば、意外と有効な技なので、試してみてください。

次回

次回はデバッグを行うためにnestest.nesを起動させ(るためにデバッグを行います。
ファミコンエミュレータを写経してみるお話4【nestest.nesの起動】 - 196Log