概要
ファミコンのエミュレータをJSでだらだらと作ってた。そこそこ遊べるようになったので公開しておく。技術的な内容は、またどこかで発表したり、Qiitaなどにまとめたい。(忘れないうちに。需要があるかは怪しいが。)
随分昔に作ってみたいなーと思いFPGAでの実装を開始したんだけど、早々に挫折した覚えがある。今思うとFPGAの場合タイミングの問題が付き纏うのでJSで書くより圧倒的に難易度も高いし、ハードエミュレータを実装するにしても前段階としてソフトウェミュレータを実装するのが定石っぽいので無謀だったっぽい。
ひとまずMapper0という基本的なカセット形式のみに対応し、スーパーマリオブラザーズがそこそこ遊べるくらいを目標とした。
成果物
ファミコンのスペック
- MPU 6502(RP2A03), 8bit
- WRAM2KB
- VRAM 2KB
- 最大発色数 52色
- 画面解像度 256x240
MPUは6502にAPUと呼ばれるオーディオプロセッサを搭載したカスタム品。メモリマップを覗くとわかるがAPUがブチ込まれた感が表現されていて良い。結構無茶したんじゃなかろうか。
解像度は256x240。デモを見せた人が口をそろえて「小さい」というが確かに小さい。
上記に加えてPPU(ピクチャープロセッシングユニット)という独自ICが実装されていて、各ハードウェアをひとつずつ再現していくことになる。
過程
Sprite2png
まずはどのように描画すべきなのか理解するためにカセットの中のスプライト領域をpngで出力するツールを書いてみた。
たとえばスーパーマリオブラザーズのスプライトは以下。これだけであの世界が構築されているのはすごい。
Hello world
まずはHello worldだけどCPUとPPU(ピクチャープロセッシングユニット)の背景レンダリングくらいは出来上がっていないといけないのでHello worldまでもそこそこ大変。
このツイート前はcanvasに描画してたんだけど、上のtweetは遊びで1div + cssでレンダリングしたときの。10FPS前後出てた。
ROMは以下で手に入る。C言語版もあるのでわかりやすい。
GIKO005
Hello worldのあとは「ギコ猫でもわかるファミコンプログラミング」を順にやっていくとよい。
これはGIKO005のスプライトを表示するサンプル。パレットがまだ実装されていないのか色がついていない。
GIKO013
これはGIKO013。GIKO013はAPUのサンプルでAPU(オーディオプロセッシングユニット)にどのような矩形波を出力すべきかが書き込まれるのでWebAudioで矩形波を作って音を鳴らしている。WebAudio自体あまり触ったこと無いし、音楽の知識もないのでここは本当にきつかった。
ここまでのサンプルで1Playerのキーパッドや背景スクロール、キャラクター移動などは実装済だった。 これだけ動いていたのでかなり順調で「CPUなんて完璧では?」と思ったりもしたが、この後に大量のバグと不可解な挙動に遭遇する。逆に言うとCPUがボロボロでもこの程度は動く。
GIKO016
縦スクロールのサンプルなんだけど全然だめだった。このあたりからパッと見つかるようなバグは減っていて、ひたすらアセンブラを読みながらデバッグするようになってた。結局「あとちょっとだから」っといって最後までやらなかったんだけど、デバッガの実装を早めにやっておけばよかった。CPUができていれば難しくない。結局全然「あとちょっと」じゃなかった。
GIKO017
これは横スクロールのサンプルで身体がないのは8 * 16形式のスプライトに対応してないから。マリオは8 * 8で動いたので、結局まだ8 * 16は対応してない。比較的このサンプルはすんなり動いた。
nestest
nes用のテストROMを発見してテストが通るようにデバッグを開始した。このROMの存在は知ったのはマリオに着手し始めたあとなので、この記事の内容の順番は実際とは多少前後している。このROMは最高でもっと早く試すべきだった。注意点としてはテスト対象のCPU上でこのテストROMが走るため、エラーとなった箇所を鵜呑みにはできない点。とは言えこいつのおかげでかなり進んだ。
CPU / PPU / keypadを早めに優先してこいつをGIKOたちより先に動作させるのが手順としては良さそう。
http://www.qmtpro.com/~nes/misc/nestest.log
テストログも落ちているのでデバッグもしやすい。
Super mario bros
だれもいない。
マリオが出たが、1マス浮いてる。ガタガタ。
味がある。
パレットをいじっていたらクリボーにコインの点滅が移ってしまった。
斜めになりながらものおばあちゃんを思い出した
重力無視してた。左上のスコアの0に黒いものが移っているけど、これはSprite0と呼ばれるもので、こいつが描画された瞬間PPUのあるビットがtrueになる。この画像では描画位置ズレて見えてしまってるが、本当はコインの裏くらいに隠してあるっぽい。
多分プレイ中はスコアやタイムなどのヘッダは固定されていて、コインより下の部分のみスクロール処理が必要なため、この位置においてあるんだと思う。CPUはPPUのsprite 0 hit フラグが立つのをポーリングしていてこいつがtrueになったらスクロールなどを始めるんだと思う。
ちなみにマリオのソースが以下で読める。
A Comprehensive Super Mario Bros. Disassembly · GitHub
未実装
未実装な箇所はたくさんあって例えば以下。 Audioはまだまだバグってるっぽくて変な音なる場合がある。
- 8 * 16 スプライト
- 各Mapper
- Noise audio
- 2 player keypad
- DCM
Mapper3くらいは対応してやりたい。
現状
ファイナルファンタジー3の高速飛空艇はCPUのバグを突いてあの速度を実現しているらしく、どんな仕組みなのか解析したいと思ってたけど、そもそもファイナルファンタジー3を単に動かすだけでも道のりは遠そう。まだ実装しなきゃいけない箇所が結構ある。 ひとまずJS版はもういいや、という気持ちになったので今はRust + wasmで書き直してる。楽しいけどRust難しい。
詳細をどっかにまとめようかとは思うが、需要ありますか?