Rustでファミコンとスーパーファミコンのエミュレーターを書いた
ゲームボーイエミュレーター、ゲームボーイアドバンスエミュレーターに続いて、Rustでファミコンエミュレーター"Sabicom"とスーパーファミコンエミュレーター"Super Sabicom"を書きました。
名前にRustっぽさを出してみました。
前回作ったマルチエミュレーターMERUのコアとして実装したので、ステートセーブや巻き戻しなどの機能も使えます。MERUの対応コアはこれで4つになりました。
こちらからWindowsとLinuxのプリコンパイルバイナリがダウンロードできるようになっています。
他のプラットフォームおよびソースコードからコンパイルする場合は
$ cargo install meru
でインストールできると思います。レポジトリからソースを取ってくる場合は、
$ git clone https://github.com/tanakh/meru --recursive
$ cd meru
$ cargo run --release
でできると思います。
ファミコンとスーパーファミコンどちらも一通り本体の機能は実装してあるつもりです。スーパーファミコンは割と細かいところまでちゃんと動くようにしてあるはずなので、動かなかったり表示がおかしかったりするソフトがあればバグですので、ぜひご報告ください。
ファミコンの方は、ゲームボーイエミュレーターを書くちょっと前に書いて放置してたやつを、今回MERUにコアとして組み込んだ形になります。ファミコンエミュレーターは昔から何となく定期的に新しい言語を勉強した時に書いたりしていたので、手癖で書いていてあんまり細かい所まで詰められていなくて、精度はあんまり高くないかもしれません。まあでもそれなりにはちゃんと動いているようには見えます。
ファミコンはシンプルなハードウェアながら、多くのソフトをカバーするのは実は結構大変で、その一つの理由は多種多様な拡張チップの存在です。ファミコンのCPUはメモリ空間が16ビット(64KB)と非常に小さいので、それより大きな容量のソフトではカセット側にバンク切り替えのためのチップが搭載されています。最初期のゲーム以外ではメモリ空間にマップできる容量を超えてくるので、そういったチップが必須になってきます。後期のものでは割り込み処理や音源の拡張までするようになって、これが非常にバリエーションに富んでいるので、いくつサポートするかでソフトのカバー率が変わってきます。Sabicomでは比較的多くのタイトルで使われている任天堂製のチップを4つほど実装しています。ファミコンカセットのデータベースなどを眺めている感じではこれで8割ぐらいはカバーされるようですが、多くのサードパーティー製の凝ったソフトは動かなかったりするので、いずれはもっと充実させたいとは思います。
スーパーファミコンはCPUのアドレス空間が24ビット(16MB)あるので、ここに大容量のROMがリニアにマッピングされていて、バンク切り替えのためのチップは必要なくなっています。なので、カセットに搭載される拡張チップのバリエーションはファミコンに比べると大幅に少なくなっていますが、その分わざわざコストをかけてまで載せるモチベーションのある重量級のものが揃っています。スーパーファミコン本体に搭載されているCPUと同じものでクロックが3倍も高いSA-1や、3D描画のためのRISC CPUであるSuper FXなどなど、もはや本体の性能を軽く上回るものが載っていたりします。当時の半導体技術の進歩の速さを感じさせるところです。Super Sabicomでは今のところ拡張チップは一つも実装していません。しかしこれらを搭載したソフトには名作も多いので、いずれは対応していきたいところです。
というわけで、ファミコンとスーパーファミコンとゲームボーイカラーが動くようになったので、各機種向けにリリースされたドラゴンクエスト3を並べてみました。
感慨深い。
ハードウェア所感
それぞれのハードウェアの詳細に関しては、ファミコンはここを、スーパーファミコンはここが良くまとまっていて、それぞれ一通り読めばとりあえず動くエミュレーターが作れると思います。実際のところは、エミュレーターはゲーム機上で動くアプリを書くのとは違って、無効な値の組み合わせとか、やらないように書いてあることをするとどうなるのかとかの挙動をサポートしないといけないので、そこが大変だったりします。ただ、ファミコンとスーパーファミコンはどちらもメジャーなハードで人気も高く、エミュレーターも熱心に開発されてきたので、先人によって書かれた解析記事やテストロムがインターネット上に多数転がっています。頑張ってインターネットを探せば細かい部分の挙動も大体は見つかると思います。
bsnesという高精度なSNESエミュレーターを書いていた方による興味深い記事がありました。もぐらたたきエミュレーションという私も覚えがある現象にも言及があり、これは例えばタイミングによって動かないソフトに遭遇した場合に、CPUのクロックをちょっと速くしたら動くようになった。じゃあCPUの速度をその値に変えようってすると、それはたぶん正しいコードではないので、今度は違うソフトが動かなくなるといったもので、それがもぐらたたきという比喩という話ですね。そういったことを回避するために、昔のエミュレーターでは特定のソフトを認識して、ソフトごとに速度やらレジスタの挙動やらを変えるハックを行っているものがありました。しかし、現実には同一のマシン個体でそれら全てのソフトが動いているわけですから、何かおかしいわけです。エミュレーター開発は一つのコードで全部のソフトが動くようになるというのが究極の目標であり、そのためにははっきりしない細かい部分のハードウェアの挙動を一つ一つ確かめていかなければならないのですが、これは本当に大変な作業です。この記事に挙げられているように、詰めるところまで詰めると、実チップのダイ写真まで解析して回路がこうなっているならここはこういう動きをしているはずだとか、そういうところまでやられているそうで、その労力には頭が下がります。そういったわけで、特定のソフトがとりあえず不具合なく動くエミュレーターを作るのと、全てのソフトが動くようなエミュレーターを作るのには天と地ほどの差があるわけです。Super Sabicomでも一応そういったハックを行わなずになるべく多くのソフトを動かせるようにするのを目標にしてはいますが、こういった苦労をみるにつけ、完璧を目指すのは恐ろしいことなんだなあと改めて思います。
まあ、それはそれとして、ここからは前回のように、読み物としてそれぞれのハードウェアがどういったものなのか、感想を交えてつらつらと書いていこうと思います。
CPU
ファミコンのCPUはリコーの2A03という8ビットCPUで、おおまかにはMOS Technologyの6502から10進命令を削除したようなものです。周波数は1.79MHzで、これはテレビ画面の周波数の60Hzと画素数で割ると、CPUの1クロックでちょうど画面の3ピクセル分に対応します。6502は一番速い命令でも実行に2サイクルかかって、長いものだと7サイクルかかるので、1命令実行するだけで画面の表示が数ドット~数十ドットすすんでしまいます。凝ったことをやろうとするとすぐに1フレーム過ぎてしまうし、画面のちらつきをなくすためにはテレビの走査線が非表示領域(VBlankという)にいるうちに次のフレームの表示の準備をしないといけないし、当時ゲームを作っていた人達は大変だっただろうなあと漠然と思ったりします。
6502はCISCのCPUで、命令は可変長で最初の1バイトが命令の種類を表しますが、256個全部命令で埋まっているわけではなく、かなり抜けがあります。普通にコードを書く分にはあまり意識しなくてもいい所ですが、エミュレーター的にはそれらの命令を実行した場合にどうなるのかということを考えなければなりません。しかし当然ながらそんなのはメーカーの仕様書には書いてありません。昨今のCPUならこういうケースに遭遇した場合は例外が発生したりするようになっているものですが、6502では回路の削減のためか特に理由はないのかは分からないけどそういうチェックはされておらず、無効命令も何となく実行されます。おそらく8ビットのうちどこかがALUの演算の種類とか、どこかがアドレッシングモードとかになっていて、あんまり意味のない感じの組み合わせで何となく処理が実行されています。こういったものも、バグか意図的にかで実行してしまうソフトはあるかもしれないので、なんとか解釈してやらないといけませんが、ファミコンのエミュレーションの歴史は長く、当然すでにそういった挙動は調べられているので、ありがたく参照させてもらいました。
スーパーファミコンのCPUは6502を16ビットに拡張したバージョンの65816(65C816)と呼ばれるCPUで、6502とは後方互換性がある・・・ということになっていますが、互換モードでもいろいろ挙動が違うので、どの程度互換モードが使われていたのかは個人的にはかなり謎です。当時のソフトはほとんどフルアセンブリで作られていたようなので、そういった意味でのソフトの移植はしやすかったのかもしれません。メモリ空間が16ビットから24ビットに拡張されて、16MBもの大容量のリニアなメモリ空間が使えて、レジスタも16ビットになり、コードはだいぶ書きやすくなっていたんではないかと思います。
クロックは3.58MHzとか、3.58MHzと2.68MHzと1.79MHzの3段階切り替えとか書かれていますが、これは明示的にそういう切り替えがあるわけではなく、メモリ空間のそれぞれの場所に対して静的にウェイトサイクルが設定されており、命令フェッチとかメモリの読み書きを遅い所から行うと2.68MHzで動いているように見えて、速い所から行うと3.58MHzで動いているように見える(入力クロックが21.4MHzで、速い所は6サイクル、遅い所は8サイクル、超遅い所は12サイクルかかる)という仕組みです。計算のために内部でストールする場合は6サイクル固定なので、実際は遅いメモリ領域で実行していても、3.58MHzと2.68MHzの間のクロックで動いているように見えると思います。
65816ではCPU内部にアキュームレータレジスタとメモリオペランド、インデックスレジスタそれぞれに対して8ビット/16ビットを切り替えるフラグがあり、これによって即値オペランドを持つ命令の命令長が変わります。つまり、実行コンテクストによってプログラムのバイナリの解釈が変わってくるということになるので、静的な逆アセンブラの作成が困難あるいは不可能で、エミュレーター作成の難度を若干上げている感があります。とはいえ、CPUエミュレーションのタイミング以外の挙動を正確にするのは、エミュレーター全体から言えばそんなに大変なことではないので、最初にそういった部分をしっかり地固めしておけばそういった部分のバグで悩まされることもあんまりないかもしれません。
65816は6502とは違って、命令コードが256個すべて有効な命令で埋められています。なので、無効命令でどういう挙動とかそういうのを考える必要がなく、そこはエミュレーターを作る上では助かる部分です。
その他には、アドレッシングモード(メモリオペランドがメモリのどのアドレスを読み書きするのか指定するもの)の挙動がわりと不可解で、ここに比較的分かりやすく書かれてはいるのですが、例えば16ビット即値アドレスに対して8ビットのレジスタでオフセットする場合、8ビット境界を超える繰り上がりがどう処理されるのか、あるいはさらに間接アドレッシングで別のレジスタでオフセットされる場合の繰り上がりはどうなるのか、などの挙動がこういったドキュメントや、公式のドキュメントでも間違っていたりして、かなり難儀しました。繰り上がりの挙動自体も6502エミュレーションモードでは違っていたり、スタックポインタ相対の時だけおかしかったりだいぶ変なのですが、ドキュメントには添え字オフセットでの繰り上がりを避けるようにプログラミングしようと書いてあるのが常套なので、そういったものだけで正確なコードを書くのもままならないものがあります。65816に関してはそういった怪しいケースをテストするためのコードもあまり転がっていなかったので、私のコードも完璧に正しいかどうか不安な部分ではあります。
メモリ
ファミコンはメインメモリが2KBにビデオメモリが2KBで、スーパーファミコンはメインメモリが128KBにビデオメモリが64KB、さらに音源チップ占有のメモリが64KBあります。実に64倍ものメモリが使えると考えると、ファミコンと比べてはるかにリッチな表現ができていたのも頷けるとともに、わずか2KBで動いていたファミコンのゲームの凄さを実感するところです。まあ、ファミコンはカセットの方に8KBのRAMやVRAMを搭載できたので後期のタイトルではそこらへんは緩和されていたかもしれません。ファミコンから7年でメモリ容量が数十倍になるという半導体の進歩の速さにも改めて驚かされます。
PPU(グラフィックチップ)
ファミコンのPPUは8x8ドットのタイルを敷き詰めたBGが1つに、8x8または8x16ドットのスプライトが64個(同一ラインには8個まで)というシンプルなものです。タイルデータは2bppなので、同一タイル内では4色(透過色を除けば3色)しか表示できません。少しでもリッチに見えるようにするために、BGにスプライトを重ねて色数を多く見せていたソフトなんかあったみたいですね。BGを表示する際に表示するデータをどのアドレスから読むかのオフセットを指定できるようにして、それでなめらかなスクロールを実現していたのが当時としては凄かったようです。MSX版ドラゴンクエストとか、PC88版スーパーマリオとかをみると、やっぱりスクロール機能のあるなしではゲームとしての見栄えが全然違ってきますね。
スーパーファミコンのPPUはBGが最大で4面になり、各BG面は2,4,8bppの組み合わせのいくつかのプリセットから選べて、色数を増やしてBG面を減らすか、BG面を増やして色数を我慢するかといったトレードオフになっています。実際のゲームでは4bppのBGを2枚重ねて、テキスト用に2bppを使っていたりするのがよくある使い方のようです。スプライトは4bppになり、最大で64x64ドットのものが128個(同一ラインには32個まで)で、こちらも大幅に強化されています。
スーパーファミコン最大の特徴といえば、BGを拡大縮小回転できるMode7の存在でしょうか。F-ZEROのようなゲームの可能性をローンチタイトルで出せたのも大きかったと思います。内部的には16ビット×8ビットができる高速な乗算機を持っているらしく、これでピクセルごとにメモリの参照位置を計算しているようです。今どきのGPUだとFP32の乗算機が数千個搭載されていたりするので隔世の感がありますが、当時は(もちろん6502と65816にも)CPUに乗算命令が無かったりしたので、こういったもので様々なタイトルの印象的な画面表現が生み出されたと思うと、すごいもんだなあと思ったりもします。画面を表示していない期間にはPPUの乗算器をCPUが計算に使うこともできるようになっていて、これってGPGPUじゃん!とかとも思ったり思わわなかったり。
これに加えて、各BGとスプライトのレンダリングターゲットが2つあってそれぞれどっちに描画するかを選択できて、最終的な画面出力をその合成として表示できる半透明機能とか、半透明にする代わりに横に並べて表示することで横方向の解像度を512ドットにできる高解像度モードとか、ピクセルの参照位置をカウンタで制御するだけで実現できるモザイク機能、描画領域をクリッピングできるウインドウ機能など結構色々な機能があります。全体としてみると、回路的なコストを最小限に抑えて多彩な表現ができるように考え抜かれたものになっているなあといった印象です。
スーパーファミコンからおよそ10年後に発売されたゲームボーイアドバンスも機能的にはスーパーファミコンと似たPPUになっていますが、同じBG数でも使える色が多かったり、回転が2枚同時に出来たり、スプライトが全部回転できたり、半透明のターゲットがよりフレキシブルになって加算合成する際に重みを掛けられたり、全体的によりパワフルなものになっているので、メモリが増えたのと乗算機のコストが下がってたくさん載せられるようになった効果が出ているのかなと思いました。
音源チップ
ファミコンは矩形波2チャンネルと三角波1チャンネルとDPCMが1チャンネルとノイズ1がチャンネルの系5チャンネルに加えて、ゲームカートリッジからも音声出力できて、それらがミックスされて出力されます。特に後期のタイトルだと、各社カセットに追加の矩形波とかを出せる音源やら波形メモリ音源やらFM音源やらを搭載してやりたい放題していましたね。
また、実はDPCMの出力レベルを直接書き込めるレジスタがあるので、これをつかえば任意の7ビットPCM音声をストリーミングできるのですが、それ用のバッファもDMAも何もないのでCPUを完全に音声出力に使うことになってしまうので、実際利用していたゲームはあんまりないみたいですね。
スーパーファミコンの音源チップはファミコンとは打って変わって超絶リッチなものになりました。ソニー製のチップで、BRRと呼ばれるADPCMの一種(?)でエンコードされた波形を8ch同時再生できるDSPと、それを制御するためのSPC700と呼ばれる8ビットCPUから成っています。SPC700は本体のCPUとバスを共有しておらず、4つの8ビットポートで通信する非常に独立性の高い作りになっています。初期の筐体だと音源チップのボード自体が独立していて、クロックも別系統で、他社のチップであることに加えてこういうハードの都合からもこういう通信方法になったのかなあと勝手に想像しています。
音声出力は32000Hzの16ビットで、ポテンシャルとしてはかなりクリアなサウンドが出せた筈ですが、当時としては大容量の64KBのメモリを以ってしても波形データを格納するには心もとないものがあったか、そもそも初期のROMは512KBしか無かったりしたのでそんなに音声データにばかり容量を割くこともできなかったのか、低めのサンプリングレートのものを使っていたりして、特に初期のタイトルにはくぐもった音のものが多かったように思います。カウンタでサンプルを進めて波形の音程を制御する形になっているのですが、その端数で4点ガウシアン補完を行うことによって、低いサンプリングレートのデータでもガビガビとした音になるのではなく、よく言えば滑らかな、悪く言えばくぐもったような音になります。さらに、メモリの一部をワークとして使うことで、ディレイを掛けた出力をフィードバック入力として使ってエコーをハードウェア的に掛けられたり、チャンネルの出力をカスケードすることで同時発生数を犠牲にして周波数変調を行ってビブラートを掛けたり、CPUリソースを費やさずに色々なエフェクトを掛けることができるようになっています。後期のタイトルになってくると、ROMの容量を生かしてか、はたまた使い方の習熟が進んでか、そのポテンシャルを生かした素晴らしい楽曲の数々が生み出されてきたのはご周知のとおり。
エミュレーションの観点から言うと、やはりこの細い通信ポートが文字通りネックになってきます。ポート自体は単なるラッチですが、ここだけを使ってCPUからSPC700に波形データに楽曲データ、それとプログラム含めて全てのデータを送らないといけないので、ゲームによってはかなり責めた通信コードを書いているものがあります。というのも、本体のCPUもSPCもそんなに速くないので、少しでも高速化するためにか、単に偶然動いてるだけのコードなのかはわからないけど、タイミングにシビアになってしまっているものがあったりします。典型的な通信コードは、ポートを2個使って1つをデータ、もう一つをackのために用いて、1バイトのデータを送ったよ、受け取ったよ、という感じで進んでいくのですが、ackを読むビジーループを少しでも速く認識させて、次のデータを速く送ってもらうためにか、SPC700側が先にackに書き込んでからデータを読むということをやっていたりします。SPC700側のコードはackを書いてしまったらCPUから次のデータが送られてきてしまうので、その前にデータを取得しなければならず当然次の命令でデータを読み取るようになっているのですが、SPC700は本体のCPUに比べてもだいぶ遅いので、SPC700がack書き込み→データ読み取りのたった1命令の間に、CPU側のack読み取り、ackチェック、条件分岐、送信用データのロード、ポートへのデータ書き込みまでが間に合ってしまう可能性があって、ここのタイミングが非常にシビアになっています。なので2つのプロセッサをタイミングを合わせてエミュレートするにあたって命令レベルの同期では不十分で、命令実行にかかる複数サイクルのうち、どのタイミングでデータのバスへの出力が行われるのか、そういういわゆるサブ命令レベルでの同期が必要になってきます。さらにそれだけでも足りず、どうやら実際のポートのラッチへの反映にはさらに何サイクルか遅れが生じるらしいという挙動があるらしく、実際何サイクル遅れるのか具体的な解析も見つけられず、ここは想像で実装することになりました。そういうソフトに関しては、SPC700を若干オーバークロックすれば同期が壊れなくなるのでちゃんと動いてしまったりもするのですが、そういうのがもぐらたたきエミュレーションがやられていた理由だったりするんでしょう。
スーパーファミコンの音源チップはソニー製なので(?)これを拡張したチップがプレイステーションとプレイステーション2にも採用されています。プレイステーションのチップではチャンネル数が8から24に増えて、再生レートが44.1KHzになって、メモリが512KBになって、制御用のCPUを載せるのではなくFIFOで本体のCPUからDSP用のメモリに直接データ転送するようになり、レジスタの制御も本体のCPUから直接行うようになっているようです。チャンネル数3倍に対してメモリが8倍、データもCDROMの膨大な容量が使えるので、スーパーファミコンの音源の独特の雰囲気のようなものは薄くなっているように感じます。プレイステーション2まではこれをさらに拡張したチップが載っていたようですが、プレイステーション3ではもはや音声処理は計算量的にもデータ量的にも大したことが無くなったのかストリーミングすればいいという話なのかソフトウェア処理になって、音源チップはオミットされてしまったようです。こういうゲームハード音源の独特の味というのもなくなってしまってちょっと寂しい気もします。
あとがき
というわけで、前回に続いてとりとめのない話を長々と書いてきましたが、自分がいわゆるスーファミ世代というやつなので、思い入れのあるゲームをこの手で動作させるというのは、やはり格別のものがあります。実のところ、20年ほど前にもスーパーファミコンのエミュレーターを書いていて、当時としては割と使ってもらえていたような気もするんですが、別の人と共同で書いていて、私が書いた部分はあまりクオリティーを高くできなくて、PPUとか音源の部分は最終的にはほとんど貢献できてなかった感じでした。今回改めて独力で書いてみて、全体的なクオリティーも自分としては一応満足できるものになって、何となくのどの奥の小骨が取れたような気持ちです。この精度に至るまでの期間と労力も当時とは比較にならないほど小さく、昔と比べて遥かに充実した情報量と、Rustによる記述力の高さとデバッグのしやすさ、それに加えて多少なりとも自分自身の成長があるとうれしいですね。
というわけで、もしよろしければ使ってみていただけると幸いです。不具合の報告や感想とか要望、コントリビュートなど頂けるとさらに幸いです。そして、万が一奇特な方がいらっしゃるなら、MERUの新しいコアを実装してくださるとこれ以上ないです。ではまた。
Discussion