TypeScript版NESエミュレータここまでの振り返り

11月から開発している TypeScript 版 NES エミュレータで、NES 研究所の提供してくれているサンプル ROM で「HELLO, WORLD!」が表示できるようになったので、この記事でここまでの開発の振り返りを行います。

https://github.com/r7kamura/nes8

Prettier

Prettier はやはり早期に導入しておいて良かった。今のところエディタと特に連携させていないけれど、現状の疎結合な状態も気に入っているので、しばらくこのままで良さそう。但し全ての変更を Pull Request でグループ化して CI の結果を見ながら merge する感じにしないと、たまに Prettier を走らせるのを忘れたまま master に push してしまうという事故が起きてしまう。とはいえ commit 時に hook で何か走らせるのもあまり好むところではないので、今後は Pull Request を意識的につくっていくような運用にしたい。まあ1人でしか開発しないことが分かりきっているプロジェクトなので、そこまで神経質にやるよりはモチベーションが継続することや開発への取っ掛かりやすさを優先した方が明らかに良い。

TSLint

TypeScript が優秀なこともあって、TSLint に怒られる機会があまり無い…。

空のブロックを許可しない no-empty というルールを自分で無効化している。全ての CPU 命令をそれぞれ対応する名前のメソッドにするという設計にしており、NOP 命令という何もしない命令が存在するので、この設計を優先するため。あと後から実装するために引数の型だけ合わせてある TODO な関数についても空のブロックが発生するので、これのためでもある。 他にビット演算子を許可しない no-bitwise というルールも自分で無効化した。JavaScript でビット演算子を利用することは稀なので許可されていないというやつで、今回は CPU やレジスタを実装する都合ビット演算子を多用するので無効化することになった。

Webpack

Web ブラウザで動かすためにコードを bundle したい気持ちがあって導入した。導入前はかなり腰が重かったが、ts-loader で bundle するだけなので、導入してみるとそこまで大変ではなかった。いまは生成されるコードが ES6 向けのものになっているが、ES5 にしないといけないケースが出てきたらまた考えないといけない。

以下はこれまでに発生した不具合。

未完成な PPU バス

PPU バスの実装が未完成なまま動かしていて、あれ動かないなと不思議がっていた。コード中に TODO のところは TODO とコメントで真面目に書いていたので、これを TODO リストとして上手く管理しながら開発していったほうが良かったかもしれない。とはいえ音声周りなどはかなり遠い将来に実装する TODO になるので、TODO の中でも優先度付けが必要そう。

https://github.com/r7kamura/nes8/commit/8d35472a4a8f9b4478e2553470bb822554ffcec8

requestAnimationFrame

メインループの処理が際限なく動いていたため、canvas がいつまで経っても更新されない状態になっていた。1画面更新する単位で処理を関数に分けて、requestAnimationFrame で 60fps を維持するように変更することで対処した。

https://github.com/r7kamura/nes8/commit/8f63a614b92ef47ede51e9dcf4536895a325ddbb

DEY 命令

DEY 命令が DEX 命令のコピペを元に実装されていたが、操作対象のレジスタを X から Y にするのを忘れていた。命令群の実装はこういう不具合が発生しがちなので、数十個の命令を疲れを押して一気に実装するのは避けるべきだった。

https://github.com/r7kamura/nes8/commit/fdab8a55d9795e1aa497fe0222eedb16eaf8f459

2次元配列から1次元配列へのインデックス変換

256 x 240 のカラーコードの配列を canvas 要素に描画する際、XY 座標を一次元配列に置き換える処理に不具合があった。XY 座標から一次元配列のインデックスに変換するところをもう少し抽象化しておけば、明らかに不具合があることに気付けたかも。

https://github.com/r7kamura/nes8/commit/66e4519aa93e1355ac1bc4258589cd3b881b0534

ImageData のインデックス計算

canvas で利用する ImageData は、Uint8Array で RGBA を格納するので、4要素で1画素を表すことになるが、これの計算処理に不具合があった。インデックス計算で不具合が発生しがちなので何とかしたい。

https://github.com/r7kamura/nes8/commit/8bc75bebc7cfd0ea2ae46fe7420b7a5766f101be

オーバーフロー対策の丸め処理

与えられたアドレスの次のアドレスからデータを読み取る処理において、16 bit の範囲を超えないように & 演算子で対策をしていたが、誤って 8 bit の範囲に丸めている不具合があった。

https://github.com/r7kamura/nes8/commit/23f1b9d7c7fc3dab922c7a5ee64009ecaee85a01

三項演算子の結合規則

引き算のオペランドとして三項演算子で求めた値を利用しようとするコードがあったが、括弧を忘れていて意図しない式が結合する不具合があった。これは演算子の優先順位の問題ではなかったが、別言語からの移植時に困ることも多いので、演算子の優先順位に関係無く動くように明示的に括弧を付けるようにした方が良いかもしれない。

https://github.com/r7kamura/nes8/commit/b6e25e59040c2304476a61c4d4101d50f6d5a21b

開発途中の様子1

なんとか canvas で描画できるようになったので HELLO, WORLD! を試してみたときの図。

開発中の様子2

少し不具合を修正した図。座標変換がミスっていそうな印象を受ける。長方形の画像のはずなのに左下に少しはみ出ている領域があるのも気になる。

開発中の様子3

ようやく HELLO, WORLD! がまともに表示されるようになった図。

まとめ

以上、TypeScript で NES エミュレータをつくっている上での振り返りでした。

NES エミュレータの仕組み自体については Ruby 版で概ね理解していたので、再実装時にどれだけ意図しない不具合の混入を防げるかということが課題でした。TypeScript のおかげで、ここまでは解決に時間の掛かる不具合はほぼ発生しなかったと思います。JavaScript の debugger をそこまで熟知していないことや、Web ブラウザで描画する際の特有の課題、デバッグのしづらさみたいなところで時間が掛かったような気がしています。

次はスーパーマリオブラザーズが動くところを目標として進捗していこうと思います。