Note
この記事は Emulation Accuracy, Speed, and Optimization を翻訳したもので、主にエミュレータ開発者向けです。
エミュレーションでよく話題になるのが、エミュレータの「精度」です。
この言葉は、エミュレーションがオリジナルのハードウェアの動作にどれだけ近いかを意味します。
しかし、この単純な言葉の裏には、かなりの複雑さが隠されています。
何をもってエミュレータの「精度」とするか、単純な指標はありません。
精度の高いエミュレータとは、(速度は遅くても、)バグが少ないことを意味すると考えられています。精度の低いエミュレータは、ほとんどの場合、高速に動作し大半のゲームを遊ぶのに十分な場合が多いです。
これらの主張には核心をついていますが、現実にはもっと多くのことがあります。
エミュレーションの精度を表す言葉としてよく使われるのがサイクル精度(cycle accuracy)
です。
この言葉は、しばしば誤解され広義に適用されています。
サイクル精度とは、本来は、エミュレートされたシステムのすべての構成要素(CPU, GPU, メモリバスなど)が、他のすべての構成要素と比較して正しいタイミング(つまり同期して)で動作することを意味します。
タイミングが厳しく、ハードウェアへのアクセスがより直接的な多くのシステム、特に古いシステムでは、サイクル精度は高精度なエミュレーションの重要な要素です。
サイクル精度という言葉のサイクル
とは、デジタルロジックのタイミングの基本単位である「クロックサイクル」のことです。
一般的にシステムやプロセッサを表すMHzやGHzという数値は、そのシステムのクロックの周波数を意味しています。
ゲームボーイアドバンスのように16MHzのプロセッサを搭載したシステムでは、1秒間に1,600万回の動作をしていることになります。
もう一つの重要なハードウェアであるバスは、プロセッサとは異なるクロックレートを持っている場合もあります。
バスとは、CPUとメインメモリの間など、システムのさまざまなコンポーネント間でデータを転送する相互接続のことです。
ニンテンドーDSでは、ARM7(GBAと同じCPU)とARM9という2つのプロセッサがあり、それぞれが異なるクロックスピード(約33MHzと約67MHz)で動作していますが、バスはARM7と同じ33MHzで動作しています。
そのため、ARM9のクロックと同じ速度のバスであれば素早く取得できるデータを、メモリから取得するために、ARM9は自身より遅いバスで待たなければならないことがあります。
サイクルカウント精度(cycle-count accuracy)
も同様の概念ですが、すべての構成要素が、他の構成要素に対して正しいサイクルで(つまり同期して)エミュレートされるのではなく、各コンポーネントはアトミックに動作し、正しい時間を要しますが、他の構成要素のタイミングと正しく同期が取れていない場合があります。
このように、サイクルカウント精度はサイクル精度に比べて厳密に劣っているように聞こえるかもしれませんし、ハードウェアの完全な精度という観点からはその通りです。
しかし、サイクルカウント精度は、エミュレーションの設計、実装、保守が非常に容易なスタイルです。
mGBAがサイクル精度の観点から正確になっている、またはなるだろうというのはよくある誤解ですが、そうするためにはmGBAの基礎的な要素のいくつかを大きく書き換える必要があります。
うまく実装されていれば、サイクルカウント精度の高いエミュレータは、サイクル精度の高いエミュレータと非常に似通った、あるいはしばしば同一の結果をもたらします。
サイクル精度が解決する主な問題は、同じサイクルでアクションを実行する異なるハードウェアを正しくエミュレートすることです。
これは、あるサイクルで発生する個々のステップをすべて順番に実行し、次のサイクルに進み、それを繰り返すという簡単な作業のように聞こえるかもしれません。
これは非常に時間がかかるものであり、サイクル精度重視の設計とサイクルカウント精度重視の設計との間にパフォーマンスの大きな違いをもたらします。
ハードウェアは、これらすべてのステップを任意のサイクルで独立して実行します。ソフトウェアはこれらのステップを並行して実行することができないため、演算の間でスワップを行う必要があります。
これは計算量が多く、結果的にエミュレータの実行は遅くなります。例を見てみましょう。
仮に、CPUの1つの動作が常に2サイクルかかり、GPUが1サイクルごとにメモリからピクセルに値を読み出すという、単純なCPUアーキテクチャがあるとします。
理論的には、サイクル精度重視の設計では、これを1サイクルずつエミュレートしていきます。
まず、CPUの前半の動作を行った後、GPUのピクセル描画を1回行います。
次に、CPUの後半の処理を行い、さらにピクセルの描画を行います。
しかし、理論の下には多くの複雑な要素が隠されています。
CPUの命令のどの部分がどのサイクルで起こるのかを知る必要がありますが、これは実装に依存することが多く、定義が不十分です。
また、中途半端に終わった処理を保存しておき、後でそれに戻ることができなければなりません。
これはアトミックデザインに比べてさらに複雑になり、結果的にパフォーマンスが低下します。
アトミックデザイン(サイクルカウント精度重視)では、オペレーションはサイクルごとに分割されません。
その代わり、それぞれのオペレーションは完了するまで実行され、その後、次のオペレーションを実行することができます。
しかし、順序を間違えると、目に見える影響が出てきます。例えば、メモリへのアクセスがGPUの動作に干渉する場合、タイミングを誤ると正しくない画面になってしまいます。
このような問題はありますが、独立な動作はサイクルカウント精度重視の設計では基本になっています。
複数の構成要素を扱う場合、サイクルカウント精度は、個々の構成要素の動作が正しいサイクル数を要し、操作が過去に発生したかのように見えるようにします。
これらを組み合わせることで、完全ではありませんが、サイクル精度の近似値が得られます。
サイクル精度に忠実なモデルでは、CPUの命令を先取りしたり中断したりすることはできず、CPUの命令の間に他のハードウェアの操作が行われます。
しかし、これらのハードウェアビットが過去に現れるようにスケジュールすることで、すべてのビットが適切な時間を要することができます。
この方法の最大の欠点は、元のハードウェアでは相互に作用する同時動作が適切にインターレースされないことです。
しかし、システムが新しい場合や構成要素がシンプルな場合は、そのような考慮すべきような相互作用は非常に稀なものになります。
GBAのような古いシステムにはエッジケースや複雑な相互作用がつきものですが、Nintendo Switchのようなモダンなシステムは一般的に慎重に設計されており、そのような相互作用が全く起こらないように保護されています。
そのため、モダンなシステムは、サイクルカウント精度重視の設計に適しています。高速に実行でき、ほとんどの場合は十分に動作するからです。
しかし、では、何をもって「十分」な性能とするのか、これは主観的で議論の余地のあるテーマです。
明らかなバグがなく遊べるゲームであれば、多くのプレイヤーは「十分」に良いと言うかもしれません。しかし、スピードランナーやTASの場合は、精度の低さが問題になってきます。
ZSNESは、非常に長い間、SNESエミュレーションコミュニティの多くが、精度が低いながらも「十分」に良いと評価していました。
精度が低くても、多くのゲームはほぼ完璧に動作します。ZSNESが完全に崩壊してしまうようなエッジケースは常にありましたが、ほとんどの人気ゲームは十分にエミュレートされていたので、精度を上げることは優先されませんでした。
多くの人にとって、これは「十分」なことでした。今でもそうだと思う人もいるでしょう。しかし、完璧とは言えず、それが気に食わない人もいました。
その結果、サイクル精度の高いエミュレータで、最も有名な例である higanが誕生しました。
これは伝説的といっていいほど正確なものですが、同時に悪名高いほど遅いものでもあります。
これは、サイクル精度を追求したものであることが原因のひとつです。
higanは、必要に応じてエミュレーションの一部を切り替えるためにコルーチンを使用しており、これには多くのオーバーヘッドがあります。
higanはサイクル精度重視なエミュレータの最も有名な例であるため、サイクル精度重視であることが必ずしも非常に遅いという誤解を招いています。
しかし、higanのパフォーマンス問題の多くは、エミュレーションがスピードに最適化されていないことに起因しています。
これは、byuu氏がドキュメントとしてのコードを厳格に管理しているため、最終的に読みやすく、理解しやすいコードにするための意図的な判断です。
byuu氏は、higanをファミコンのゲームを遊ぶための手段としてだけでなく、ファミコンの挙動を記録する保存プロジェクトとしても位置づけています。
高度に最適化された正確なSNESエミュレータを作れば、正確さを犠牲にすることなく、higanよりもはるかに高速なエミュレータを作ることができますが、誰もそれを実現していません。なかなか大変な作業だと思います。
私がmGBAを作り始めたときの目標は、VisualBoyAdvanceよりもサイクル精度が正確で、かつ速いというものでした。
この2つの目標はしばしば相反するものですが、正確さにはタイミングだけでなく、もっと多くの要素があります。
画面のレンダリングが正しくないとか、メモリ操作が正しくエミュレートされていないなどの問題があるのです。
VisualBoyAdvanceでは、最適化されていない部分が多く、サイクル精度を上げてもスピードに影響しない部分が多いと感じました。
速度が向上したことで、速度に影響を与えるサイクル精度の向上のためのオーバーヘッド(余裕)も確保できました。
mGBAにはGBAとGBのエミュレーションが可能です。
GBAのエミュレーションとは異なり、のGBエミュレーション(mGBと呼ばれることもあります)は、サイクル精度を考慮して設計されています。
命令エミュレーションは、個々のクロックサイクルで実行されるタスクに分割され、これらのオペレーションの間に他のハードウェアをエミュレートすることができます。
しかし、演算を一度に実行するのではなく、バッチ処理を行う(同時並行的なインタラクションがある場合は、必要に応じてバッチを分割する)など、多くの最適化により、mGBは非常に高速に実行できるようになっています。
精度を犠牲にすることなく、最適化されていないサイクル精度重視な実装よりもはるかに高速です。
サイクルカウント精度重視の実装が障害となる顕著な例として、今後mGBAで実装予定のDSエミュレーションのGPUコマンドFIFOが挙げられます。
DSは、GPUにコマンドを書き込む際に、まだ処理されていないコマンドのリストを作成します。
新しいコマンドはこのリストに追加されます。
ただし、FIFOには最大サイズがあります。
FIFOが一杯になると、新しいコマンドのためにFIFOに十分なスペースができるまで書き込みがブロックされます。
実際のハードウェアでは、FIFOがいっぱいになると、メモリバスがストールし、ARM CPUはこの命令の途中で一時的にブロックされます。
その後、メモリバスとは独立にFIFOがGPUによって読み込まれ、FIFOに空きができるのでメモリバスが続行できるようになります。
mGBAのDSエミュレーションmedusa
ではCPUの動作はアトミックに扱われるので、命令の途中でメモリバスをストールさせてGPUでFIFOを処理することはできません。
代わりに、medusa
が満杯のFIFOへの書き込みを処理する方法は、書き込むべき値をキャッシュし、FIFOに空きができてキャッシュされた値がFIFOにフラッシュされるまで、新しい命令を実行できないことをCPUに伝えるというものです。
しかし、ARM CPUには、一度に複数の値をメモリに書き込むことができる命令群が用意されています。
つまり、1つの命令で複数のコマンドをFIFOに書き込むことが可能であり、書き込みの間にメモリバスがストールする可能性もあります。
例えば、FIFOに3つのコマンドを書き込んだが、1つしか入らない場合、3つ目を書き込もうとする前にストールしてしまいます。
これはmedusa
にとって大きな問題となりました。medusa
は現在、実際にはサイクルカウントの精度に違反しているが、新しいコマンドを書き込むのに十分な量のFIFOをすぐに処理するというアプローチでこれに対処しています。
これはある特定のエッジケースですが、正しくない動作なので対処する必要があります。
サイクル精度からさらに進んで、ハイレベルエミュレーション(HLE)という概念があります。
多くのゲーム機は、特に90年代後半以降、エミュレートされたソフトウェア自体には含まれていないプログラマブルなコンポーネントを持っています。
コンポーネントはシステム自体の一部であり、ゲームによって大きな違いはありません。
例えば、DSP(ゲームキューブに搭載)、システムソフトウェア(PSPや3DSに搭載)、NINTENDO64のRSPのようなマイクロコードプログラマブルデバイスなどです。
これらのコンポーネントは、直接エミュレートすることもできますが(ローレベルエミュレーション(LLE)と呼ばれています)、実際には、命令ごとに段階的にエミュレートしなくても済ませることができます。
ハードウェアと同じ効果を持つコンポーネントのカスタム実装を書き、それをエミュレートするのではなくネイティブコードとして実行することで、動作を大幅に高速化することができます。
一方で、異なるハードウェアコンポーネントとの同期や、適切なタイミングでの動作がほぼ不可能になるというデメリットもあります。
さらに、HLEの実装には、ハードウェアそのものだけでなく、その上で動作するマイクロコードについても膨大な研究が必要です。
HLEの初期の実装は、LLEにはないバグが多く、デバッグが非常に困難な場合があります。
一方、LLEの実装では、エミュレートされるコードのコピーが必要ですが、これは必ずしも容易ではなく、著作権の関係でエミュレータ本体と一緒に配布することもできません。
精度と速度のトレードオフは難しい問題です。
古いシステムでは、エミュレーションがすでにかなり高速で、通常はより厳しいタイミング制限があるため、ほとんどの場合、精度が優先されます。
最新のシステムでは、エミュレーションするためにはHLEが必要になります。バランスが難しいところですが、どちらにもメリットがあります。
一般的には、精度が高い方がエミュレートされたソフトウェアでの問題が少なく、保存のための価値も高くなりますが、スピードと最近のプラットフォームのエミュレーションのためには、精度は必ずしも必要ではありません。