Thanks Driven Life

日々是感謝

Emacs で動く NES エミュレータを作っている話

本記事は Emacs Advent Calendar 2018 の22日目の記事です。

成果物

まずは現時点 (12/22) での動作状況です。

https://github.com/gongo/emacs-nes

nestest.nes palette_pal.nes
f:id:gongoZ:20181222155809g:plain:w300 f:id:gongoZ:20181222160136j:plain:w300

使い方はいつか README の方に書きますが(いつか)、ざっと書くと:

  1. ソースコードもってくる
  2. nes*.el があるディレクトリに load-path を通す
  3. load-library nes
  4. M-x nes*.nes ファイルを選択

これで動くはずです。Byte Compile 推奨。

経緯

様々な言語で NES (= Nintendo Entertainment System) のエミュレータを実装する、というネタは昔からあります。私も何かしらの言語でやってみようかな? とボンヤリ考えたまま特に手をつけていませんでした。

そんな日々の中で参加した builderscon tokyo 2018 で、とある発表を見ました。

ファミコンエミュレータの創り方 - builderscon tokyo 2018

「何やっているのか良くわかんねえな?」と思いながらも「しかし面白いな!」という感想を得て、いよいよ自分でも何かで書いてみるかーという気持ちが再燃しました。

ちなみに「どの言語で実装するか?」ということですが、まだ誰も作っていなさそう*1Emacs Lisp でやってみることにしました。

記事の概要

  • 書いてあること
    • Emacs Lisp でそれっぽく動くところまでの話
  • 書いてないこと

動作するまでの話

時系列でふんわりと

1. まず ROM パーサの実装

.nes ファイルのフォーマット iNES の仕様どおりに読み込んでいきました。

2. CPU の実装

とにかく CPU が無いと始まらない! ということで CPU 周りに手をつけはじめました。

  1. 命令セットをとにかく Emacs Lisp に落としていく作業
  2. 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 命令に対するテストを実施する」というようなサンプルプログラムを書きました。

;; 当時のコードではなく、現状のコードで再現した形です
;; (この時はまだ 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.nespc=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. スプライトの処理がまだ甘い

まだズレがある

f:id:gongoZ:20181222184432p:plain:w300

2. スクロール処理がおかしい

f:id:gongoZ:20181222155710g:plain:w300

この ROM は ギコ猫でもわかるファミコンプログラミング にある、背景画像スクロールの例題です。何かこう、移動がぎこちない。素直に左に進んで欲しい

3. 遅い

今のところ体感で 3 FPS ぐらいです。なんとか ギコ猫 がギリギリ動かせるぐらい(十字キーの左を押しっぱなしでこれぐらい):

f:id:gongoZ:20181222183547g:plain:w300

4. BGM

さすがに Emacs 単体では無理かもしれない

5. 縦横のサイズを調整するのが少し面倒くさい

gamegrid.el に限らず、Emacs で各ポジションに色つけたりする時は face や font を弄ると思います。 つまり、その位置にある文字の width と height が、そのまま NES エミュレータ側からみる「1ピクセルの width と height 」になります。

これの何が面倒くさいのかというと、基本的に使っているフォントは「縦のサイズ = 横*2」みたいなことが多いので:

f:id:gongoZ:20181222181533p:plain:w300

こんな感じで縦長になってしまいます( iTerm で 256 x 149 の様子。256 x 240 が領域なので、下の方が見えない)

仕方ないので、今のところ端末設定の「行間隔」に相当する設定を弄ってそれっぽく見えるように調整*3しています。(冒頭の画像とかはそれ)

f:id:gongoZ:20181222182238p:plain:w300

  • Emacs の設定だけでは上記のような行間隔設定ができない*4
  • おそらくそれ用(height == width)のフォントを用意すればいけるのかな…?

まとめ

深い解説のない、ただの開発日記みたいになりましたが如何でしたでしょうか。 取り組み始めて約2ヶ月(毎日やっていたわけではないですが)かかり、なんとかモノになったので良かったです。

難しいといえば難しいですが、やはり目に見えて良くなっていくのは開発してて面白いですね。 もう少しマシなゲームが動かせるようになったら、またどこかで報告します。*5

それでは、今年もお疲れ様でした。良い御年を

参考

*1:Lisp」という括りではいくつかありました https://news.ycombinator.com/item?id=8666976

*2:特にパターンテーブルの理解に時間をかけました。つまりどういうことやねん状態

*3:Emacs に限らず、端末に描画するタイプはここらへん調整が必要になりそう

*4:見つけきれてないだけかも。あると嬉しい

*5:吸い出し機買わないと…