プログラミング
Show HN: PS2ゲームがWebAssemblyに静的に再コンパイルされました
Show HN: PS2 games statically recompiled to WebAssembly (github.com)
要約
このプロジェクトは、エミュレータを使用せずにPlayStation 2(PS2)ゲームをブラウザで直接実行できるようにします。PS2のEmotion Engineマシンコードを事前にC++に変換し、WebAssemblyにコンパイルし、PS2のOSとハードウェアの再実装にリンクしてWebGPUで描画します。これにより、『グランツーリスモ4』や『God of War II』などの要求の高いゲームが、ブラウザ上でネイティブ解像度またはそれ以上の解像度でスムーズに動作します。
全文翻訳
PS2 Web Recomp
エミュレータなしでブラウザで動作するPlayStation 2ゲーム。
ゲームのEmotion Engineマシンコードは、事前にC++に変換され、WebAssemblyにコンパイルされ、WebGPUで描画するPS2のオペレーティングシステムとハードウェアの再実装にリンクされます。
フル録画を見る (1:24)
これはpsp-web-recompの姉妹プロジェクトであり、同じ方法論に従っています。
この方法で最初に登場したタイトルは『グランツーリスモ4』です。このゲームはシステム上で最も要求の厳しいゲームの1つであるため選ばれました。ベクターユニットの両方、リプレイやライセンステストでの正確な浮動小数点演算、レンダリングされた画像をパレットとして読み取るグラフィックトリックに依存しています。起動からオープニングムービー、メニュー、レースまで、音楽と効果音とともに動作し、ブラウザに保存されたメモリカードにセーブします。
レースは、Chromeのラップトップの内蔵GPUで、PS2の解像度の2倍で、毎秒50フレーム(このPALリリースの場合)でフルスピードで実行されます。これが上記の録画が作成された方法です。
『God of War II』も同じスクリプトで続きました。ゲーム固有のコードは必要ありませんでしたが、グランツーリスモ4では決してトリガーされなかった2つのバグ(リコンパイラ内の1つとベクターユニットタイミング内の1つ)と、PS2のI/Oプロセッサのいくつかの欠落した部分が見つかりました。現在、メニューからカットシーンを経て戦闘まで動作し、ChromeでPS2の解像度で重い戦闘中に毎秒60フレームを維持します。解像度が2倍になると、ラップトップの内蔵GPUによって毎秒48〜60フレームで動作します。
ゲームデータは含まれていません。所有しているゲームのディスクイメージを用意すると、このリポジトリのスクリプトがそれをローカルマシン上のWebページに変換します。
仕組み
再コンパイル。
eerecompはゲームの実行可能ファイルを読み込み、その制御フローをたどって関数を見つけ、16 KiBのゲストコードの単位でC++を出力します。出力はR5900の癖を保持します:128ビットレジスタとそれらを操作するマルチメディア命令、ブランチ遅延スロット、無限大や非正規数を持たず、IEEEとは異なる丸めを行うPS2の浮動小数点ユニット、コプロセッサとして使用されるベクターユニット0。
ユニット間のジャンプはテールコールになり、スレッドスイッチはホストスタックをアンワインドしてゲストアドレスで再開するため、生成されたコードはゲストスレッドごとにスタックを必要としません。
小さなPS2カーネル。
オペレーティングシステムのEE側は、スレッド、セマフォ、DMAコントローラと垂直ブランクの割り込みハンドラ、アラーム、メインCPUとI/Oプロセッサを接続するSIFチャネルなどのハイレベルエミュレーションで応答されます。
I/Oプロセッサは、R3000 CPUのインタプリタ上で、ゲーム固有のIRXモジュール、サウンドドライバ、ディスクストリーミングコード、ディスクに出荷されるコントローラとメモリカードドライバを実行します。IOPカーネルとそれらのモジュールが呼び出すライブラリはC++で再実装されています。
ディスクはHTTP Rangeリクエストで読み取られるため、実行可能ファイルのみが最初にダウンロードされます。
ベクターユニット。
PS2ゲームの3D処理の大部分は、ゲームがベクターユニット1にアップロードするマイクロプログラムで行われます。これらは、ビット単位で正確なPS2浮動小数点演算と、MAC、ステータス、クリップフラグのパイプライン遅延をモデル化して実行されます。ゲームはそれらの周りにコードをスケジュールするためです。
テスト実行中に表示されるマイクロプログラムは、事前にSIMD C++にコンパイルされます。ページが初めて遭遇するものは、その場でWebAssemblyモジュールに変換され、プログラムあたり数ミリ秒かかります。
グラフィックス。
グラフィックスシンセサイザーの4MBのビデオメモリはGPU上にあり、ネイティブのスウィズルレイアウトで配置されています。描画はWebGPUコンピュートシェーダーによって行われます。三角形はタイルにビンされ、テクスチャリング、パレット、アルファブレンディング、深度、デスティネーションアルファテストに関するGS独自のルールでラスタライズされます。
すべてがGSメモリ内に留まるため、テクスチャにレンダリングしたり、フレームバッファをパレットとして再解釈したりする効果は、コンソールで行われるのと同じように機能します。
内部解像度はネイティブ解像度の1、2、または3倍にできます。
同じGSのソフトウェアラスタライザは、GPU出力が比較される参照として残っています。
スレッド。
エミュレーションはワーカーで実行され、ベクターユニット1はコンソールで並列実行されるのと同じように独自のワーカーを持ち、GSコマンドはリングバッファを介してWebGPUデバイスを所有するページスレッドに渡されます。
サウンドとムービー。
SPU2は、48ボイスのADPCM、ADSRエンベロープ、音楽に使用されるストリーミングDMAを再実装しており、その出力は共有メモリを介してAudioWorkletに渡されます。
ムービーは、IPU(PS2のMPEG-2デコーダ)の実装によってデコードされます。
高速化からの教訓
最初のレースは、ソフトウェアラスタライザとベクターユニット1のインタプリタを使用して、ネイティブで毎秒3フレームで実行されました。GSをWebGPUコンピュートシェーダーに移動し、ベクターユニットプログラムをコンパイルしたところ、ネイティブで約70フレームになり、残りの作業のほとんどはエミュレーションではなく、ブラウザが時間を費やしている場所に関するものでした。
最初のGPUレンダラーバージョンは、フレームあたり約650のコンピュートディスパッチを送信しました。これは、テクスチャのアップロードとパレットのロードごとに、その周りの三角形のバッチが分割されたためです。同じ出力で、依存しない三角形の前にアップロードをホイスティングしたところ、約80になりました。
『グランツーリスモ4』は、レースの各フレームで同じ3MBのテクスチャをアップロードします。GSメモリはフレーム間でスワップされるため、アップロードを単純にスキップすることはできませんが、GPU上の最近アップロードされたコンテンツの16MBキャッシュにより、ほとんどのアップロードをWebAssemblyからGPUへの再転送なしで提供でき、フレームあたりのデータ送信量を約60%削減しました。
ネイティブ解像度の3倍では、フレームレートは23に低下しました。シェーダーのプロファイリングにより、三角形ごとのセットアップがスレッド間で異なるブランチの後にロードされ、セットアップ全体が分岐してしまい、1MBの断片に分割されたアップロードがChromeのページスレッドをブロックすることが示されました。
セットアップを均一にロードし、各フレームのデータを1つの塊で書き込むことで、同じ内蔵GPUで3倍の解像度を毎秒48〜50フレームに引き上げました。
ブラウザ自体のスケジューリングも重要でした。ネストされたsetTimeout呼び出しは4ミリ秒に制限されるため、エミュレーションループは現在MessageChannelを介してyieldするように変更され、エミュレーションをワーカーに移動してページスレッドがGPUにのみフィードするようにしたことで、毎秒40〜50フレームの間で変動していたレースが一定の50フレーム/秒で安定しました。
2番目のゲーム『God of War II』は、ISOからport.shを介してページに移行しました。汎用的な変更のみで、そのそれぞれが最初のゲームでは単に実行されなかったバグまたはギャップでした。
それは沈黙から始まりました。ゲームのサウンドドライバは、I/Oプロセッサのハードウェアタイマーの1つからティックを駆動しますが、何も実装されていなかったため、ドライバはサンプルをミキシングしませんでした。
難易度メニューの後のイントロは巨大なフラットポリゴンのごちゃ混ぜになり、ゲームプレイが始まろうとしたときに、レンダリングリストがメモリの終端を超えて垂直ブランクハンドラを上書きした後にゲームがクラッシュしました。
破損した連結リストをハードウェアウォッチポイントで追跡した結果、リコンパイラが見つかりました。ブランチが16 KiBユニットの最後の命令である場合、ブランチを取らないパスは、遅延スロットではなく次のユニットで実行され、その命令を2回実行するか、ブランチライクリがスキップすべき命令を実行しました。
これによりクラッシュは修正され、ゲームは戦闘に到達しましたが、画面の一部にはまだ巨大なポリゴンが表示されていました。これらは、カリングされるべきだった三角形から来ていました。
『God of War II』のマイクロコードは、CLIPが設定された3命令後にクリップフラグを読み取りますが、ベクターユニットモデルでは4命令後にのみ表示されるため、『グランツーリスモ4』(マイクロコードは常に長く待機する)では気づけませんでした。
その過程で、BIOSが提供するようなブートパス、メモリからロードされるモジュール(ディスクからではなく)、およびスクラッチパッドDMAのデスティネーションチェーンモードも必要でした。
独自のゲームをポートする
git、CMake、Ninja、C++20コンパイラ(clangが推奨され、自動的に検出されます)、Python 3、およびディスクイメージが.7z形式の場合は7-Zipが必要です。すべてLinux上のx86-64で実行されています。
最初の実行でEmscripten SDKがインストールされます。