読者です 読者をやめる 読者になる 読者になる

APUメモ

APUが1割くらい理解できた(NesDevWikiを読んだだけですが)ので、既存のエミュレータの実装も合わせて少し書いてみます。

まずAPUはフレームカウンタを内蔵しており、フレームカウンタはCPUサイクルに連動するシーケンサを持ちます。このシーケンサの動作サイクルをAPUサイクルと呼びます。

フレームカウンタ内のシーケンサには1つ飛びでCPUクロックが送られます。よって1APUサイクル=2CPUサイクルです。ちなみに1CPUサイクル=3PPUサイクルなので、まとめると1APUサイクル=2CPUサイクル=6PPUサイクルとなります。ただ三角波のタイマは1CPUサイクル単位で動作するようなので、APUサイクルを別単位として考えることにさほど意味はないかもしれません。

いずれにしろNES実機ではCPU, PPU, APUが上記の関係を保ちつつ並列に動作しています。そして、例えばフレームカウンタの動作表を見ればわかるように、実機の動作を完璧にエミュレートするにはサイクル単位での処理が必要です。

しかし大抵のエミュレータはサイクルベースでの処理は行っていません(コーディングが難しい上に遅くなるからだと思われます)。私は主にFCEUXbjneを参考にしていますが、これらはどちらも「PPUを1ラインずつ処理し、CPU, APUは可能な範囲でこれに同期させる」という感じになっています。私が書いているのもこれらの真似なので同様です。

PPUとCPUの同期についてはサイクル数を計算してその分CPUを動かすだけ(それで完璧とは言ってない)なので割と簡単ですが、APUをPPUに同期させるのは結構面倒な感じです。まずAPUフレームカウンタのフレームの定義からしてPPUの1フレームとはわずかにズレています。このため、APUの処理はエミュレータによって個性が出るところみたいです。

FCEUXはCPUで1命令実行するごとにAPUを処理しているようです(X6502_Run() から FCEU_SoundCPUHook() を呼び出している部分)。これは比較的実機に近い動作かもしれません。bjneはAPU関連レジスタへの書き込みをサイクル数も含めてキューに記録し、後からそれを処理して辻褄を合わせる形にしているようです。

とりあえずの目標としてはFCEUXとムービーレベルで互換性を持たせたいので、FCEUXを真似して書いてみようかと思います。

ついでに言うとサウンド再生自体が結構面倒なんですよね…。今のところLinux上で開発していますが、音声のレイテンシを気にし始めるときりがなさそうな感じです。Linux版のFCEUXからして明らかに音が遅れてますし…(仮想環境だからかもしれませんが)。まあムービーレベルで再現性があれば出力の品質はあまり気にしない方向で行こうかと思っています。

映像/音声出力は今のところSDL2を使っているのですが、最新版の2.0.4でSDL_QueueAudioなるAPIが追加されたようです。SDLの音声出力は基本的にコールバック方式で、メインスレッドで音を溜めてコールバックで出力する、というのが基本みたいですが、このAPIを使うと音声データを突っ込んでやれば後は再生処理を丸投げできるっぽいです。まだ試してませんが、これを使っておけばスレッド間の同期問題はひとまずスルーできるかも?