本記事は Emacs Advent Calendar 2018 の22日目の記事です。
成果物
まずは現時点 (12/22) での動作状況です。
https://github.com/gongo/emacs-nes
nestest.nes | palette_pal.nes |
---|---|
使い方はいつか README の方に書きますが(いつか)、ざっと書くと:
これで動くはずです。Byte Compile 推奨。
経緯
様々な言語で NES (= Nintendo Entertainment System) のエミュレータを実装する、というネタは昔からあります。私も何かしらの言語でやってみようかな? とボンヤリ考えたまま特に手をつけていませんでした。
そんな日々の中で参加した builderscon tokyo 2018 で、とある発表を見ました。
ファミコンエミュレータの創り方 - builderscon tokyo 2018
「何やっているのか良くわかんねえな?」と思いながらも「しかし面白いな!」という感想を得て、いよいよ自分でも何かで書いてみるかーという気持ちが再燃しました。
ちなみに「どの言語で実装するか?」ということですが、まだ誰も作っていなさそう*1 な Emacs Lisp でやってみることにしました。
記事の概要
- 書いてあること
- Emacs Lisp でそれっぽく動くところまでの話
- 書いてないこと
動作するまでの話
時系列でふんわりと
1. まず ROM パーサの実装
.nes
ファイルのフォーマット iNES の仕様どおりに読み込んでいきました。
- nes-cartridge.el
- bindat 初めて使った
2. CPU の実装
とにかく CPU が無いと始まらない! ということで CPU 周りに手をつけはじめました。
- 命令セットをとにかく Emacs Lisp に落としていく作業
- nes-instruction.el
- ユニットテストは無く、稼動させる環境もまだ整っていないため、ただひたすら「きっと動くやろ」と書き続けた
- CPU 周りで必要な要素などを Emacs Lisp に落としていく作業
- nes-cpu.el
- ここもまだ動かしていない
一通り落とし終わったので、いざ動くか試してみるぞ! と検証を開始しました。
検証に使用したのは
This is the best test to start with when getting a CPU emulator working for the first time.
と書かれるまでに便利な、 nestest.nes という「まずはこれ PASS したら次に進もうな」というテスト用 ROM です。
本来の nestest.nes は「コントローラ(ジョイスティック)でテストしたい CPU 命令を選んでテストを開始する」というものなのですが、この時点ではまだ画面やコントローラの実装できていません。そこで「コントローラの操作は無視して、ROM を読み込んで全ての CPU 命令に対するテストを実施する」というようなサンプルプログラムを書きました。
;; 当時のコードではなく、現状のコードで再現した形です ;; (この時はまだ ppu や interrupt は用意していなかったので) (let ((cart (nes/cartridge-load "/path/to/nestest.nes")) (cpu (make-nes/cpu :interrupt (make-nes/interrupt) :ppu (make-nes/ppu)))) (nes/cpu-set-working-ram cpu (make-vector #x0800 0)) (nes/cpu-set-program-rom cpu (lexical-let ((cart cart)) (lambda (addr) (nes/cartridge-read-from-prg-rom cart addr)))) ;; ;; 画面から操作せず、テストだけを実行するために ;; nestest.txt の1行目の状況を開始地点(program counter)にセットしている ;; ;; memo: 長くなるのでここには書いていないが、 status register とかも ;; 1行目と合わせておく ;; (nes/cpu-reset cpu) (setf (nes/cpu-register->pc (nes/cpu->register cpu)) #xC000) (while t (let* ((r (nes/cpu->register cpu)) (pc (nes/cpu-register->pc r)) (opcode (nes/cpu-read cpu pc)) (operand (nes/cpu-read cpu (1+ pc) :word)) (inst (aref nes/instruction:MAP opcode)) ) (message "%04X %02X %s A:%02X X:%02X Y:%02X P:%02X SP:%02X" pc opcode (if (null operand) " " (format "%4X" operand)) (nes/cpu-register->acc r) (nes/cpu-register->idx-x r) (nes/cpu-register->idx-y r) (logior (lsh (if (nes/cpu-register->sr-negative r) 1 0) 7) (lsh (if (nes/cpu-register->sr-overflow r) 1 0) 6) (lsh (if (nes/cpu-register->sr-reserved r) 1 0) 5) (lsh (if (nes/cpu-register->sr-break r) 1 0) 4) (lsh (if (nes/cpu-register->sr-decimal r) 1 0) 3) (lsh (if (nes/cpu-register->sr-interrupt r) 1 0) 2) (lsh (if (nes/cpu-register->sr-zero r) 1 0) 1) (lsh (if (nes/cpu-register->sr-carry r) 1 0) 0)) (nes/cpu-register->sp r) ) (nes/cpu-step cpu) )))
このプログラムを実行すると、下記のような結果が *Message*
バッファに書き出されます:
C000 4C C5F5 A:00 X:00 Y:00 P:24 SP:FD C5F5 A2 8600 A:00 X:00 Y:00 P:24 SP:FD C5F7 86 8600 A:00 X:00 Y:00 P:26 SP:FD C5F9 86 8610 A:00 X:00 Y:00 P:26 SP:FD C5FB 86 2011 A:00 X:00 Y:00 P:26 SP:FD C5FD 20 C72D A:00 X:00 Y:00 P:26 SP:FD C72D EA B038 A:00 X:00 Y:00 P:26 SP:FB C72E 38 4B0 A:00 X:00 Y:00 P:26 SP:FB C72F B0 A204 A:00 X:00 Y:00 P:27 SP:FB C735 EA B018 A:00 X:00 Y:00 P:27 SP:FB C736 18 3B0 A:00 X:00 Y:00 P:27 SP:FB C737 B0 4C03 A:00 X:00 Y:00 P:26 SP:FB C739 4C C740 A:00 X:00 Y:00 P:26 SP:FB C740 EA 9038 A:00 X:00 Y:00 P:26 SP:FB C741 38 390 A:00 X:00 Y:00 P:26 SP:FB C742 90 4C03 A:00 X:00 Y:00 P:27 SP:FB C744 4C C74B A:00 X:00 Y:00 P:27 SP:FB C74B EA 9018 A:00 X:00 Y:00 P:27 SP:FB C74C 18 490 A:00 X:00 Y:00 P:27 SP:FB ... ...
あとはこの結果と、「 nestest.nes の pc=C000
以降の動きがこうなっていれば正解やで」が記載されている nestest.log を比較して、PC や各レジスタの値が同じまま最後まで到達したら CPU はひとまず実装終わり!
長い戦いだったが、ようやく第一フェーズが終わった。次の方が大変だけど pic.twitter.com/kLjzjKhvFg
— Wataru MIYAGUNI (@gongoZ) 2018年11月3日
3. PPU の実装 (白黒・背景画像だけ)
ついに見える化を進めます。
ところで、Emacs で「指定した位置に、この色でピクセルを描画する」という実装を行う場合、様々な方法があると思います。
今回は 18日目の記事でも紹介されていた gamegrid.el
を使用しました。
私自身も2年前に gamegrid.el で一ネタ 作っており、とりあえずこれでいいかーという気持ちで選定しました
PPU の実装、ものすごく大変でした。 CPU と違って「目チェックしては修正して〜」の繰り返し*2。そんな PPU の実装を開始して一週間後、ついに…
ついにここまできたぞい pic.twitter.com/KnljNbwcKp
— Wataru MIYAGUNI (@gongoZ) 2018年11月11日
ここまで来るとモチベーションも高まり、あとは勢いのまま突き進みました。
画面が使えるようになることで、他のテスト用ROMでも試せるようになり、更に便利に。
cpu_dummy_reads.nes 便利すぎない? pic.twitter.com/ZESJ506bFb
— Wataru MIYAGUNI (@gongoZ) 2018年11月12日
4. コントローラの実装
コントローラ操作はもちろんキー入力。キー入力と云えば Emacs の独壇場。特に問題もなく実装できました(2Pコンはまだ未実装なのですが)。
コントローラも実装できたことで、以前テキスト比較でのみ使用していた nestest.nes
も、ついに画面で結果を見ることに成功。
NES エミュレータ written by Emacs Lisp で nestest.nes のテスト全クリした(めっちゃ動作遅いけど) pic.twitter.com/bWlRNoFxbJ
— Wataru MIYAGUNI (@gongoZ) 2018年11月14日
5. PPU の実装 (色有り・背景画像だけ)
そろそろ白黒から脱却する時期になりました。一気に実装
NES エミュレータ written by Emacs Lisp、ついに色ついた(一層遅くなった) pic.twitter.com/0n37afspFR
— Wataru MIYAGUNI (@gongoZ) 2018年11月20日
📝 当時は色テーブルが間違っており、色が変でした。
6. PPU の実装(スプライト画像)
背景画像とはまた別の計算が絡んできて一層混乱しました。
sprite がおかしいというところまではわかった pic.twitter.com/2P2HNWuWBH
— Wataru MIYAGUNI (@gongoZ) 2018年11月28日
とはいえ、あとはひたすら微調整作業だったので、無事にそこそこ動作するまでもってこれました。終わり。
まだ実装できていないところ
1. スプライトの処理がまだ甘い
まだズレがある
2. スクロール処理がおかしい
この ROM は ギコ猫でもわかるファミコンプログラミング にある、背景画像スクロールの例題です。何かこう、移動がぎこちない。素直に左に進んで欲しい
3. 遅い
今のところ体感で 3 FPS ぐらいです。なんとか ギコ猫 がギリギリ動かせるぐらい(十字キーの左を押しっぱなしでこれぐらい):
4. BGM
さすがに Emacs 単体では無理かもしれない
5. 縦横のサイズを調整するのが少し面倒くさい
gamegrid.el に限らず、Emacs で各ポジションに色つけたりする時は face や font を弄ると思います。 つまり、その位置にある文字の width と height が、そのまま NES エミュレータ側からみる「1ピクセルの width と height 」になります。
これの何が面倒くさいのかというと、基本的に使っているフォントは「縦のサイズ = 横*2」みたいなことが多いので:
こんな感じで縦長になってしまいます( iTerm で 256 x 149 の様子。256 x 240 が領域なので、下の方が見えない)
仕方ないので、今のところ端末設定の「行間隔」に相当する設定を弄ってそれっぽく見えるように調整*3しています。(冒頭の画像とかはそれ)
まとめ
深い解説のない、ただの開発日記みたいになりましたが如何でしたでしょうか。 取り組み始めて約2ヶ月(毎日やっていたわけではないですが)かかり、なんとかモノになったので良かったです。
難しいといえば難しいですが、やはり目に見えて良くなっていくのは開発してて面白いですね。 もう少しマシなゲームが動かせるようになったら、またどこかで報告します。*5
それでは、今年もお疲れ様でした。良い御年を
参考
- 参考実装
- JavaScript https://github.com/bokuweb/flownes
- Ruby https://github.com/r7kamura/rnes
- Go https://github.com/fogleman/nes
- 各テストROMがいい感じにすぐ動かせて、コードも弄りやすかった
fogleman/nes
が終盤で助かりました。特に PPU 周り
- 仕様資料
- 全体
- http://taotao54321.hatenablog.com/entry/2017/04/11/135825
- 軽くどんなもんか? と見るには充分の記事まとめ
- https://wiki.nesdev.com/w/index.php/Nesdev_Wiki
- 最終的にみんなここを見る
- http://taotao54321.hatenablog.com/entry/2017/04/11/135825
- CPU
- http://www.oxyron.de/html/opcodes02.html
- http://obelisk.me.uk/6502/reference.html
- 各参考実装で差異がある命令セットを見つけたあとは、ここを見て真実を確かめる
- 全体
- 動作確認
- https://wiki.nesdev.com/w/index.php/Emulator_tests
- nestest.nes をはじめとする、有志によるテスト用 ROM が置かれている。便利
- http://gikofami.fc2web.com/
- ゲーム感のあるソフトが無いとモチベーションが停滞する。そんな時はここ
- アセンブラベースではありますが、解説もあるので便利
- https://wiki.nesdev.com/w/index.php/Emulator_tests
- その他
- https://twitter.com/r7kamura/status/1069482196212183040
- 縦長になってつらいーって時に行間隔の設定おしえてもらいました
- https://twitter.com/r7kamura/status/1069482196212183040