PlayStation 2 GSエミュレーション ― Vulkanコンピュートエミュレーションの最後のフロンティア
(themaister.net)- paraLLEl-GS は PS2 の GS(Graphics Synthesizer) を Vulkan コンピュートで再現し、約20年にわたり事実上の標準だった GSdx が抱えていた精度とアップスケーリングの限界に取り組む
- GS は 4MiB の VRAM と高いフィルレートを前提に動作するが、宛先アルファテスト・条件付きブレンディング・1.0 を超えるアルファ/色といった ピクセルパイプライン の特性のため、一般的なグラフィックス API では再現が難しい
- 実装では VRAM をページと 256 バイトブロック単位で追跡し、CLUT スナップショット・テクスチャのアン・スウィズル・レンダーパスのバッチ化を組み合わせて フレームバッファ/テクスチャフィードバック を処理する
- Tales of the Abyss、Final Fantasy X、MGS2、Valkyrie Profile 2、Shadow of the Colossus といった事例で、アップスケーリング、UI、高精度ブレンディング、テクスチャフィードバックの問題が比較され、8x・16x SSAA の場面も扱われる
- 現在の検証は主に GS dump の再生に依存しており、PCSX2 用の hack patch や mkfifo によるリアルタイムテストはあるものの、実際のユーザーに届くにはエミュレータ統合が必要
paraLLEl-GSの目標と出発点
- paraLLEl-GS は PlayStation 2 の GS(Graphics Synthesizer) を Vulkan コンピュートでエミュレートするプロジェクト
- 同じ作者による 2020 年の作業 paraLLEl-RDP は N64 RDP を Vulkan コンピュートで実装し、Angrylion を基準にビット精度に近い結果とアップスケーリングを目標としていた
- PS2 では GSdx が約 20 年にわたり事実上の最先端実装として残っていた
- 2014 年ごろに OpenCL ベースの PS2 GS コンピュート実装の試みがあったが完了せず、現在は upstream リポジトリでも見つけにくい
- PS2 でコンピュートシェーダーラスタライズを使う理由は N64 より弱い
- PCSX2 にはよく最適化されたソフトウェアレンダラーと、比較的堅牢なグラフィックスベースレンダラーがある
- ソフトウェアレンダラーはアップスケーリングをサポートしない
- グラフィックスベースレンダラーは、特にアップスケーリング時にさまざまなバグやグリッチを示す
- paraLLEl-GS はハードウェアに対する ビット精度 よりも、明白な精度問題を避けることに重点を置いている
- GSdx のソフトウェアレンダラーもハードウェアのビット精度実装には見えず、直接比較ベースのテストはすぐ限界に達する
PS2 GSが厄介な理由
- GS は 2000 年当時、理論上毎秒 10 億ピクセル超を処理できる フィルレートと帯域幅 を持つ装置だった
- VRAM は 4MiB と小さいが、複数の DMA エンジンを通じて継続的にストリーミングされる前提で設計されている
- ピクセルパイプライン自体は N64 RDP より単純な面もある
- 単一テクスチャ
- 単一サイクルコンバイナ
- 非常に基本的なアンチエイリアシング
- 一般的なグラフィックス API では実装しづらい機能が多い
- 1.0 を超えるブレンディング: PS1 から引き継がれた動作で、0x80 が 1.0 のように扱われ、0xff まで表現できる
- 宛先アルファテスト: 宛先アルファを擬似ステンシルのように使える
- 条件付きブレンディング: アルファに応じてブレンディングを条件付きで無効化できる
- アルファ補正: アルファを書き込む前に MSB を OR して強制的に 1 に近づけられる
- アルファテストの部分破棄: 色だけ捨て、深度書き込みは維持するといった動作が可能
- AA1: coverage-to-alpha 方式で、ピクセルごとの深度書き込み制御と絡む
- 32 ビット固定小数点 Z: D32_UINT サポートは技術的には存在するが、実際の使用例はまだ見ていない
- プログラマブルブレンディングがない場合、即時モードのデスクトップ GPU では ROV やピクセル単位バリアが必要になり、性能が大きく低下する
- コンピュート実装は独自の タイルベース遅延レンダラー(TBDR) を作ることで、こうした制約を回避する
ラスタ規則、頂点キュー、メモリ配置
- GS のプリミティブはクリップ空間で比較的素直に供給される
- VU1 が変換とクリッピングを行い、GS に複数の頂点属性を出力する
- 座標と属性は GS 固有の形式を持つ
- X/Y: 12.4 unsigned fixed-point
- Z: 24 ビットまたは 32 ビット uint
- FOG: 8 ビット uint
- RGBA: 頂点ごとのライティング用 8 ビット値
- STQ: 透視補正付きテクスチャ座標
- UV: 透視補正なしの 12.4 固定小数点・非正規化座標
- ラスタ規則は D3D9 スタイルに近い
- 三角形は現代の GPU と同様に top-left raster 規則を使う
- ピクセル中心は D3D9 のように整数座標上にある
- 線は Bresenham アルゴリズムを使うためアップスケーリングが難しく、rect や parallelogram で近似する必要がある
- 点は最も近いピクセルにスナップされる
- スプライトは 2 座標を持つ単純なクアッド
- GS の頂点キューは OpenGL 1.0 の即時モードに似ている
- RGBA、STQ、複数のレジスタを設定し、XYZ レジスタへの書き込みが頂点の “kick” を形成する
- TRIANGLE_FAN もサポートする
- PS2 のピクセル座標は ページ 単位で配置される
- 1 ページは 8KiB
- ページは 32 個のブロックに分割される
- 32 ビット RGBA では 1 ページは 64×32 ピクセルで、32 個の 8×8 ブロックが Z-order でスウィズルされる
- 24 ビット色や 24 ビット深度にレンダリングする際、余った上位 8 ビットにテクスチャを置ける
- 8H、4HL、4HH 形式は 8 ビット・4 ビットパレットに有用
テクスチャ、CLUT、TEXFLUSH
- GS のテクスチャ処理には現代 API に近い部分と独特な部分が混在している
- テクセル中心は現代 API と同様に half-pixel にある
- サブテクセル精度は 8 ビットではなく 4 ビットに見える
- バイリニアフィルタは通常のバイリニアで、N64 の 3-point filter のような特殊構造ではない
- 特殊なアドレッシングモードが実装難度を上げる
- REGION_CLAMP はテクスチャアトラス内の任意領域に clamp を適用できる
- REGION_REPEAT は座標ごとに
(u & MASK) | FIXのようなビット演算を適用でき、実装がさらに厄介
- ミップマッピングは微分値ではなく、補間された Q factor の log2 とスケール係数で LOD を計算する
- コンピュート実装では微分値に依存しない点が大きな利点
- この方式では anisotropic filtering のような機能はサポートできない
- CLUT は現在のパレットを保持する 1KiB キャッシュ
- 使用するには VRAM から CLUT キャッシュへ明示的にコピーする必要がある
- 32 ビット色なら 256 色パレット 1 つを保持できる
- 16bpp では 16 色パレットを 32 個保持できる
- TEXFLUSH はテクスチャキャッシュの同期・無効化に近い明示的命令
- 実装初期には TEXFLUSH をハザード追跡の基準にしようとしたが、最終的には無視せざるを得なかった
- ゲーム側が TEXFLUSH を忘れたり、逆に多用しすぎたりする問題があった
- 最終実装では minimal caching アプローチを採用した
- キャッシュがないものとしてハザードを直接追跡する
- フィードバックループには別の例外処理を考慮する
- GSdx も同じ方向に見える
Vulkanコンピュートレンダリングパイプライン
- 実装パイプラインは各段階間の同期を前提に構成されている
- CPU の VRAM コピーを GPU と同期する
- VRAM アップロードまたは local-to-local copy を実行する
- VRAM から CLUT キャッシュを更新する
- VRAM を VkImage にアン・スウィズルして直接サンプリング可能に変換する
- レンダリングを実行する
- GPU 側の VRAM コピーを CPU に再同期する
- 一般的なゲームの動作はこのパイプラインとうまく噛み合う
- テクスチャを VRAM にアップロードする
- パレットを VRAM にアップロードする
- CLUT キャッシュを更新する
- テクスチャで描画する
- 必要なら VRAM から VkImage にアン・スウィズルする
- プリミティブのバッチをレンダーパスとして構成する
- 後方に向かうハザードがなければ、バッチ化と同期の遅延が可能
- この種のレンダラーで性能を出すには バッチ維持 が重要
- 主なハザード事例は別途処理される
- すでに copy が書いた VRAM への再 copy
- サンプリングされたテクスチャや CLUT が読んだ VRAM への copy
- レンダリング済み領域をテクスチャとしてサンプリング
- レンダリング済み VRAM への copy
ページ追跡とテクスチャキャッシュ
- GS エミュレーションで最も難しい部分は、VRAM の read-after-write と write-after-write ハザードを扱うこと
- 4MiB の VRAM はまずページ単位に分けられる
- ページはフレームバッファと深度バッファの単位なので、最も意味のある追跡基準になる
- ページ単位で追跡する状態は次のとおり
- pending frame buffer write
- pending frame buffer read
- テクスチャと VRAM copy は 256 バイト整列なので、32 ブロックに対する u32 ビットマスクを使う
- VRAM copy write
- VRAM copy read
- CLUT キャッシュまたは VkImage への pending read
- 何らかの write で上書きされたブロック
- 24 ビット色にレンダリングしつつ上位 8 ビットをテクスチャとしてサンプリングする場合、ハザードが発生しないことがある
- そのためフレームバッファ書き込みマスクとテクスチャ読み取りマスクを別々に追跡する
- 各ページは関連する VkImage の一覧を持つ
- ページテクスチャが無効化されると、そのイメージは破棄され、VRAM から再度アン・スウィズルする必要がある
- 1 つのテクスチャが複数ページにまたがることがあり、そのうち 1 ページでも上書きされればテクスチャ全体が無効化される
- 単純で保守的な追跡だけでは PS2 ゲームでは動作しない
- 256 バイトブロック単位の追跡 と write/read mask の考慮が重要
- POT テクスチャかつ REGION_CLAMP 未使用のため false positive が生じることがある
- たとえば 512×448 のレンダーターゲットを 512×512 テクスチャとして設定すると、未使用領域がハザードのように見える
- 実装ではその “red zone” の潜在的ハザードを無視する回避策を使う
CLUTのバッチ化とテクスチャのアン・スウィズル
- テクスチャアップロードをバッチ化するには CLUT アップロードも一緒にバッチ化する必要がある
- 実装は CLUT の 1024 個のコピーを スナップショットリングバッファ として持つ
- 1 つのワークグループが更新を巡回し、SSBO に書き込む
- N64 RDP の TMEM 更新に似ているが、CLUT 更新の方がはるかに単純
- Vulkan では新しい VkImage を割り当て、VkDeviceMemory からサブアロケーションしたうえで、コンピュートシェーダーでアン・スウィズルする
- Vulkan specialization constants を使ってテクスチャ形式とスウィズルロジックを特化する
- REGION_REPEAT の特殊動作もアン・スウィズル段階で処理する
- その後は ubershader がこのケースを考慮して手動バイリニアフィルタリングを行う必要が減る
- レンダーターゲットも VRAM SSBO を経由してテクスチャと往復する
- レンダーターゲットをテクスチャへ直接 forward しようとする試みは、バグと例外が多すぎると判断された
三角形セットアップ、ビニング、ubershader
- paraLLEl-GS は paraLLEl-RDP と同様に タイルベースレンダラー
- ビニング前に三角形セットアップを行い、入力は 3 つの配列に分かれる
- 位置
- 頂点ごとの属性
- プリミティブごとの属性
- ラスタライザは barycentric ベースで、Fabian Giesen のグラフィックスパイプライン記事と Pineda 1988 論文で説明される並列ラスタライズ方式の強い影響を受けている
- 実際の PS2 GS は DDA、つまり scanline rasterizer だが、GS DDA のビット精度な説明が不明なため barycentric 方式を使っている
- wide line と sprite 実装のために parallelogram もサポートする
- inv_area はカスタム固定小数点 RCP で計算される
- 標準 GPU の RCP は実装間の一貫性が低く、精度も約 22.5 ビット程度なので避けている
- カスタム RCP は約 24.0 ビット精度を目標とする
- ビニングは通常 32×32 ピクセルブロックを使う
- 1 レンダーパスあたりのプリミティブ最大数は u16 インデックスのため 64k
- 観測された主要レンダーパスは概ね 10k〜30k プリミティブの範囲だった
- PS2 GS はフィルレートが高く、ピクセルあたりの複雑さが低いため 純粋な ubershader が可能
- N64 と違って bindless を活用できるため、テクスチャ処理の複雑さも下がる
- ubershader は early Z、deferred on-tile shading、lazy pixel shading を使う
- ピクセルが以前の結果に依存する場合にだけ実際のシェーディングを行う
- alpha test、color write mask、alpha blending などがこの依存性を生む
- 最終フレームバッファ色と深度は SSBO に書かれ、GPU 帯域幅使用を削減する
スーパーサンプリングとアップスケーリングアーティファクトの軽減
- 単一サンプルレンダリングだけでは、このレンダラーの価値を十分に引き出せない
- たとえば 8x SSAA では、GPU 上に VRAM の 10 バージョンを保持する
- 単一サンプル VRAM 1 つ
- 単一サンプル VRAM の reference 値 1 つ
- スーパーサンプル 8 つ
- レンダリング時、単一サンプル VRAM と reference が一致していればスーパーサンプル版をロードする
- これはインクリメンタルレンダリングで重要
- タイル完了時に clustered subgroup 演算でマルチサンプル resolve を行い、スーパーサンプルと単一サンプルのコピーを書き込む
- スーパーサンプリングは単純なアップスケーリングよりジャギーが少なく、3D 要素と UI 要素の解像感をより一貫して揃えられる
- スプライトプリミティブは常に single-rate でレンダリングする必要がある
- ほとんどが UI あるいはそれに近い要素で、アップスケールすると意図した rect の外をサンプリングしたり、バイリニアフィルタで過度にぼやけたりしうる
- 多くの UI は通常の三角形として描かれるため、一部の flat primitive では属性補間を single-pixel 座標へ落とす
- perspective を使っていても、全頂点の Q が同じで Z も同じなら flat UI primitive と推定する
- false positive の可能性はあるが、テストしたゲームでは十分うまく機能した
ゲーム別の結果と難しい事例
- Tales of the Abyss では、PCSX2 Vulkan backend のアップスケーリングでガラスの bloom の位置ずれと四角いパターンが見られる
- paraLLEl-GS の 8x SSAA では、悪いアップスケーリングに典型的な問題があまり目立たない
- 該当スクリーンショットには FSR1 による後処理アップスケールも適用されている
- Final Fantasy X の UI は native resolution と 4x upscale の比較でアップスケーリング問題が現れる
- MSAA snap trick がアーティファクト回避に有効
- 核心原則は UI を nearest neighbor の整数倍率以上にアップスケールしないこと
- MGS2 は PCSX2 で高い blending accuracy を要求する例がある
- PCSX2 は programmable blending パスでプリミティブごとにバリアが入るため性能が大きく落ちる
- paraLLEl-GS は常に 100% blend accuracy で動作する構造で、RX 7600 における 16x SSAA の場面が 25W、GPU 使用率 17% として示されている
- Valkyrie Profile 2 には、自身のピクセルのアルファをパレットインデックスとしてサンプリングする事例がある
- paraLLEl-GS はこれを検出してテクスチャインデックスを特殊値にし、in-register framebuffer color を参照する
- この最適化により render pass barrier は 500 個超から 18 個に減少した
- MGS2 intro の camo 効果は framebuffer をテクスチャとしてサンプリングするが、ピクセル整列が合わない重なった座標を使う
- PCSX2 もここではバリアを追加していないようで、paraLLEl-GS も同様に処理する
- Shadow of the Colossus は強いストレステストとして機能する
- PCSX2 の最大 blend accuracy では intro で 2x upscale だけでも GPU が 24 FPS まで落ちる
- paraLLEl-GS は 8x SSAA でも性能は良好だが、その場面では負荷が大きい
- この場合のボトルネックは GPU より CPU の geometry processing にある
現在の状態と次の段階
- 現在の実用的なテスト方法は GS dump を使うこと
- PCSX2 で raw GS trace をダンプできる hack-patch がある
- mkfifo による粗いリアルタイムテストも可能
- 最終的にエンドユーザーに有用になるには、何らかの形でエミュレータ統合が必要
- PS2 のライブラリは非常に大きいため、まだ多くのバグが潜んでいる可能性が高い
- standalone library という性質上、古いスタイルのレンダリング API のように使える潜在的ユースケースもある
1件のコメント
Hacker News のコメント
この記事で GS という略語がいつ展開されるのか、しばらく探さないといけないのかと思った。内容は興味深いけれど、ここで少し置いていかれる感じがする
「プログラマブルブレンディングがあるよう祈れ」だなんて、2000年代初頭に ピクセルシェーダー を初めて学んで以来、「ブレンディングシェーダー」によるプログラマブルブレンディングをずっと望んでいた
ちなみに、カスタムテクスチャ形式/圧縮、テクスチャ合成などに役立つ「テクスチャシェーダー」によるプログラマブルなテクスチャデコードも欲しかった
どういうわけか GPU はプログラマブルブレンディングより先に レイトレーシング を手に入れたけれど、前者は夏の夜の夢のようで、後者はまた一つの固定機能ブロックをプログラマブルブロックに置き換える程度に感じられた。テクスチャシェーダーはいまだに待っている
https://medium.com/pocket-gems/programmable-blending-on-ios-...
https://developer.apple.com/videos/play/tech-talks/605
これを活用した良い例がある: https://vulkan.org/user/pages/09.events/vulkanised-2024/vulk...
ある程度は実装可能だと思うが、説得力のあるユースケースがなければ実装作業を正当化するのは難しいと思う
GS でいちばん好きなところは、バス構造 のとんでもない規模だった。合計2560ビット幅で、キャッシュ分割も賢かった
PS3 はある意味では後退のように感じたし、特にブレンディングがそうだった
このアプローチは Dolphin の ubershader と比べてどうなのか気になる
実のところ Dolphin が導入した時点でも、すでに古い手法だった。このプロジェクトはラスタライザまで含む完全なレンダラーで、本文にあるようにブレンディング用の ubershader も含む
シェーダーが三角形を描くのではなく、三角形内の各点ごとに呼び出され、いくつかの入力を受け取ってその点の色を決める。CPU エミュレーター全体と、ADD/MUL 命令だけを実装したものを比較するのに、ぼんやり似ている
top-left raster とはどういう意味なのか気になる