プログラミング
PSP版ゴッド・オブ・ウォーがWebAssemblyに再コンパイルされ、ブラウザで動作
God of War on PSP, recompiled to WebAssembly and running in the browser (github.com)
要約
このプロジェクトは、PSPゲームをWebAssemblyに再コンパイルし、エミュレータなしでブラウザ上で直接実行できるようにするものです。ゲームのMIPSマシンコードをC++に変換し、PSPのOSとグラフィックチップの簡易的な再実装と組み合わせてWebGL2で描画します。これにより、God of Warシリーズがネイティブ解像度の最大4倍で、毎秒60フレームで動作します。
全文翻訳
PSP Web Recomp
PSPゲームをエミュレータなしでブラウザで実行。
ゲームのMIPSマシンコードは、事前にC++に翻訳され、WebAssemblyにコンパイルされ、PSPのオペレーティングシステムとグラフィックチップの小さな再実装とリンクされ、WebGLで描画されます。
フル録画を見る (1:46)
タイトルメニュー
ブラウザ内
1440×816でフルスクリーン
この方法で最初に起動されたタイトルは、God of War: Chains of Olympusです。起動からメニュー、カットシーン、戦闘まで、ChromeとFirefoxのラップトップで測定されたシーンでは毎秒60フレームで動作し、PSPの解像度の最大4倍、オン画面タッチコントロール付きのスマートフォンでも動作します。音楽、音声、効果音は機能します。ムービーは現在スキップされています。
God of War: Ghost of Spartaも同じスクリプトで続きました。PSPのDRM復号化が1つの小さなファイル、少数のシステムコール、ライティング修正で必要でしたが、パフォーマンス作業は不要でした。ネイティブ解像度の3倍で毎秒55〜60フレームで動作します。ゲームデータは含まれていません。所有しているゲームのディスクイメージを用意すると、このリポジトリのスクリプトがそれをローカルマシン上のWebページに変換します。
仕組み
再コンパイル。
PSPRecompは、復号化された実行可能ファイルを分析し、その関数を見つけ、ゲストコードの各16 KiBの翻訳単位にC++を出力します。そのコードは、レジスタファイルとPSPメモリのモデルに対して実行されます。このプロジェクトでは、PSPRecompに少数の修正(patches/内)と、新しいターゲットとしてwebプロファイル(profile/内)を追加します。
小さなPSPカーネル。
ゲームがPSPのオペレーティングシステムに要求するものはすべて、profile/host/内のハイレベルエミュレーションによって応答されます。セマフォ、イベントフラグ、コールバックを備えた協調スレッド、メモリパーティション、ファイルシステム(ディスクデータはHTTP Rangeリクエスト経由でストリーミングされるため、実行可能ファイルのみが最初にダウンロードされます)、コントローラー、オーディオ出力、セーブデータとメッセージダイアログ、ムービープレーヤーのブックキーピングが含まれます。ゲスト時間はフレーム単位で進むため、ホストがどれだけ速く実行しても、ゲームは安定した60 Hzを見ます。
グラフィックス。
PSPのグラフィックチップであるGEは、ディスプレイリストを受け取ります。ge.cppは、頂点フォーマット、スキニング、ライティング、テクスチャ生成、クリッピング、バックフェイスカリングなどを含めてCPU上でデコードし、バッチ処理された三角形とレンダーステートをge_gl.cppに渡します。そこで、フレームバッファはVRAM内の場所をキーとするWebGLレンダーターゲットになり、テクスチャにレンダリングしてそれを読み戻すエフェクトはGPU上に留まります。ピクセルフォーマットの再解釈(ゲームは32ビットバッファを16ビットテクスチャとして読み書きする)、PSPのステンシルインアルファ、フォグ、ブロック転送もGPU上でエミュレートされ、すべてネイティブの480×272の1倍から4倍の解像度でレンダリングできます。
2つのスレッド、PSPのように。
PSPでは、グラフィックチップは1フレームのディスプレイリストを処理しながらCPUが次のフレームを準備し、ゲームはそのように書かれています。ここでは、GEとそのWebGLコンテキストはOffscreenCanvasを持つワーカー スレッドで実行されます。リストをキューイングするとすぐに返され、明示的な同期コールのみが待機します。1フレームのコストは、どちらかのスレッドがビジーであるかによって決まります。
サウンド。
効果音は、PSPのボイスシンセサイザー(ADPCM 32ボイス、ピッチとADSRエンベロープ付き)の再実装から、音楽と音声はFFmpegデコーダーでデコードされたATRAC3+ストリームから来ており、ミックスはAudioWorkletに送られます。
高速化からの教訓
最初のプレイ可能なビルドは毎秒6フレームで動作しました。60フレームへの道のりのほとんどは、レンダラーを高速化するのではなく、時間が実際どこに行っていたかを見つけることから来ました。
God of Warは垂直ブランクを待たずにフレームバッファをスワップするため、何も妨げがないと、ゲームは画面に到達するフレームの約8倍を描画していました。PPSSPPも使用するトリックである、スワップするスレッドを1回のブランク内で2回保持し、次のブランクまで待つことで、表示されるフレームあたりの作業量が8分の1になりました。WebAssemblyでは、std::chronoを介してクロックを読み取ることは、JavaScriptでのclock_gettimeとBigInt変換を経由します。プリミティブごとに読み取るプロファイリングタイマーは、プロファイリングがオンの場合にのみperformance.now()を読み取るようにするまで、フレームの約3分の1を消費していました。
Firefoxは、すべてのWebGLバッファアップロードをGPUプロセスにコピーし、インデックスバッファに変更があった後、次の描画時にバッファ全体を再検証します。共有4MBのインデックスリングは、Firefoxを毎秒3フレームに低下させました。各描画に小さなインデックスバッファを独自に割り当てることで、この問題は修正されました。逆の修正、つまり頂点データを大きなバッファに少しずつ書き込む方法は、どのブラウザでも役立たず、携帯電話をスタックさせました。これは、GPUがまだ読み取っている可能性のあるバッファが変更されたときに、モバイルGPUドライバーが待機またはコピーするためです。
PSPはフレームバッファのアルファチャンネルにステンシルバッファを保持しており、God of Warはそれを投影されたシャドウやブラーパスの制限に使用します。フルスクリーンのパスでステンシルとアルファを相互にミラーリングするのは正しかったですが、4倍解像度ではフレームあたり6000万ピクセル以上の追加コストがかかりました。実際に変更された矩形、ステンシルビット、定数値を追跡することで、約700万ピクセルに削減されました。
最後に、God of Warは各フレームのディスプレイリストをキューイングし、次のフレームの作業を待機する前に続行します。GEを独自のワーカー スレッドで実行することで、ビジーな戦闘のコスト(Firefoxではゲームとグラフィックスで約17ミリ秒)が、より大きい方の値(約10ミリ秒)になりました。
2番目のゲームであるGhost of Spartaは、ZIPからport.shを介してページに移行しましたが、スクリプトや再コンパイラに変更はありませんでした。その後、起動時に永遠に待機しました。PSPのDRMフラグを持つ176バイトのファイルを開き、そのキーをsceIoIoctlに渡し、読み取ったものをチェックします。ファイルはPGD形式で、amctrl.prxがKIRK暗号エンジンで復号化します。ディスクゲームの場合、これはKIRKのキーボールトからの3つのキーを使用したAES-128になります。ヘッダーのCMACベースのチェックと、データのカウンターモードです。profile/host/pgd.cppがこれを実装しています。
次の問題は白い空でした。メニューにも同じ白い霞がかかっていました。1フレームの描画をバイセクトすると、ライティングがオンで描画されたクラウドレイヤーが見つかりました。その不透明度は、グローバルアンビエントライトのアルファから来ており、ライティングコードが欠落していた要因です。パフォーマンス作業は不要でした。フレームのコストは、Chains of Olympusと同様に、Firefoxでは6〜8ミリ秒です。
独自のゲームをポートする
git、CMake、Ninja、C++20コンパイラ、Python 3が必要です。すべてLinuxで実行されました。macOSでもブラウザバージョンはビルドできるはずですが、未テストです。ネイティブテストランナーにはEGLとOpenGL ESヘッダー(DebianおよびUbuntuではlibegl-devとlibgles-dev)が必要です。抽出されたディスク、生成されたコード、ビルドごとに約2 GBのディスク容量を予想してください。
git clone https://github.com/snuri00/psp-web-recomp.git
cd psp-web-recomp
scripts/setup.sh # PSPRecomp、パッチ、Emscripten SDK
PSP_DECRYPT=/path/to/decrypter scripts/port.sh mygame "My Game.iso"
scripts/serve.sh mygame # http://localhost:8613/
port.shはディスク(ISO、それを内包するZIP、または既に抽出されたフォルダ)を抽出し、PSP_GAME/SYSDIR/EBOOT.BINを復号化し、C++に翻訳し、残りのディスクのストリーミング用マニフェストを書き込み、ページをビルドします。数分かかります。God of WarはZIPからプレイ可能なページまで約4分(8コアラップトップの場合)で移行します。
--nativeフラグを追加すると、フレームを画像にダンプしたりオーディオを録音したりできるヘッドレスランナーもビルドされます。これが新しいゲームがどこまで進むかを確認する最も速い方法です。
市販ディスクの実行可能ファイルは暗号化されています。PSP_DECRYPTは、tool <in> <out>として呼び出され、プレーンELFを書き込む任意のツールを指定します。例えば、DecEbootやpspdecryptなどです。PPSSPPは、ゲームを実行中に復号化された実行可能ファイルをダンプすることもできます(設定、ツール、開発者ツール)。
期待値を調整してください。これまでに2つのゲームが移植されており、どちらも同じエンジンで構築されたReady at Dawnのタイトルであるため、他のスタジオのゲームがどこまで進むかについてはあまり示唆していません。別のゲームは、まだ実装されていないシステムコールで停止する可能性が非常に高いです([hle] unimplemented ...としてログに記録されます)、またはこのレンダラーが処理しないGE機能を使用します。docs/internals.mdには、何が不足しているかを見つけるためのツールが記載されており、コードはコールを追加しやすいように整理されています。