本記事は Emacs Advent Calendar 2018 の22日目の記事です。
成果物
まずは現時点 (12/22) での動作状況です。
https://github.com/gongo/emacs-nes
使い方はいつか README の方に書きますが(いつか)、ざっと書くと:
- ソースコードもってくる
nes*.el
があるディレクトリに load-path
を通す
load-library nes
M-x nes
で *.nes
ファイルを選択
これで動くはずです。Byte Compile 推奨。
経緯
様々な言語で NES (= Nintendo Entertainment System) のエミュレータを実装する、というネタは昔からあります。私も何かしらの言語でやってみようかな? とボンヤリ考えたまま特に手をつけていませんでした。
そんな日々の中で参加した builderscon tokyo 2018 で、とある発表を見ました。
ファミコンエミュレータの創り方 - builderscon tokyo 2018
「何やっているのか良くわかんねえな?」と思いながらも「しかし面白いな!」という感想を得て、いよいよ自分でも何かで書いてみるかーという気持ちが再燃しました。
ちなみに「どの言語で実装するか?」ということですが、まだ誰も作っていなさそう*1 な Emacs Lisp でやってみることにしました。
記事の概要
動作するまでの話
時系列でふんわりと
1. まず ROM パーサの実装
.nes
ファイルのフォーマット iNES の仕様どおりに読み込んでいきました。
2. CPU の実装
とにかく CPU が無いと始まらない! ということで CPU 周りに手をつけはじめました。
- 命令セットをとにかく Emacs Lisp に落としていく作業
- CPU 周りで必要な要素などを Emacs Lisp に落としていく作業
一通り落とし終わったので、いざ動くか試してみるぞ! と検証を開始しました。
検証に使用したのは
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 命令に対するテストを実施する」というようなサンプルプログラムを書きました。
(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))))
(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 はひとまず実装終わり!
3. PPU の実装 (白黒・背景画像だけ)
ついに見える化を進めます。
ところで、Emacs で「指定した位置に、この色でピクセルを描画する」という実装を行う場合、様々な方法があると思います。
今回は 18日目の記事でも紹介されていた gamegrid.el
を使用しました。
私自身も2年前に gamegrid.el で一ネタ 作っており、とりあえずこれでいいかーという気持ちで選定しました
nes-ppu.el
PPU の実装、ものすごく大変でした。 CPU と違って「目チェックしては修正して〜」の繰り返し*2。そんな PPU の実装を開始して一週間後、ついに…
ここまで来るとモチベーションも高まり、あとは勢いのまま突き進みました。
画面が使えるようになることで、他のテスト用ROMでも試せるようになり、更に便利に。
4. コントローラの実装
コントローラ操作はもちろんキー入力。キー入力と云えば Emacs の独壇場。特に問題もなく実装できました(2Pコンはまだ未実装なのですが)。
nes-keypad.el
コントローラも実装できたことで、以前テキスト比較でのみ使用していた nestest.nes
も、ついに画面で結果を見ることに成功。
5. PPU の実装 (色有り・背景画像だけ)
そろそろ白黒から脱却する時期になりました。一気に実装
📝 当時は色テーブルが間違っており、色が変でした。
6. PPU の実装(スプライト画像)
背景画像とはまた別の計算が絡んできて一層混乱しました。
とはいえ、あとはひたすら微調整作業だったので、無事にそこそこ動作するまでもってこれました。終わり。
まだ実装できていないところ
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しています。(冒頭の画像とかはそれ)
- Emacs の設定だけでは上記のような行間隔設定ができない*4
- おそらくそれ用(height == width)のフォントを用意すればいけるのかな…?
まとめ
深い解説のない、ただの開発日記みたいになりましたが如何でしたでしょうか。
取り組み始めて約2ヶ月(毎日やっていたわけではないですが)かかり、なんとかモノになったので良かったです。
難しいといえば難しいですが、やはり目に見えて良くなっていくのは開発してて面白いですね。
もう少しマシなゲームが動かせるようになったら、またどこかで報告します。*5
それでは、今年もお疲れ様でした。良い御年を
参考