なぜ『Cities: Skylines 2』のパフォーマンスは低いのか
(blog.paavo.me)- Cities: Skylines 2はシミュレーション中心のジャンルであるにもかかわらず、ほとんどの状況でGPUボトルネックが目立ち、最低設定を超える1080p 60FPSを狙うには約1000~2000ユーロ級のグラフィックカードが必要だというベンチマークがある
- Renderdocで取得したサンプルフレームは87.8ms、6705 draw call、5万件超のAPI call、約6.7GBのGPUバッファ・テクスチャ使用量を示しており、60FPSの基準である16.7msとは大きな隔たりがある
- ボトルネックの核心は、多くのメッシュでLOD不足と単純なカリングにより、画面にほとんど寄与しない高ポリゴンのジオメトリが繰り返しレンダリングされる構造にあり、シャドウパスだけで40ms、全draw callの72%を占める
- 議論になった市民の歯は実際にレンダリングされているが単独の原因ではなく、市民モデル・小物・装飾物全般の過剰な頂点数と非効率なレンダリングが積み重なっていることのほうが大きな問題である
- Unity DOTSとHDRPの統合は未成熟で、Colossal OrderがECSとレンダラーの接続、カリング、仮想テクスチャリングなどを自前で実装した形跡があり、CPUボトルネックは減らせた一方で、グラフィックスパイプラインは十分に磨き込まれていないように見える
発売前後に表面化したパフォーマンス問題
- Cities: Skylines 2は発売前から推奨スペックの引き上げ、コンソール版の2024年延期、パフォーマンス関連言及の制限があった
- 発売1週間前、Colossal Orderはパフォーマンス問題の事前釈明に近い告知を出し、発売後はパフォーマンスがほぼ普遍的な批判対象となった
- 都市建設ゲームは高フレームレートを出しにくいことがあるが、本作はジャンルでよくあるCPUボトルネックよりもGPUボトルネックが目立つ
- PC Games HardwareとGamers Nexusのベンチマークでは、「very low」以上の設定で1080p 60FPSを出すには約1000~2000ユーロ級のGPUが必要という結果がある
- RTX 3080、Ryzen 7 5800X、5120×1440モニター環境で初回起動時、メインメニューは10FPS未満だったが、開発元推奨どおりにdepth of field、motion blur、volumetric effectsを無効にすると約90FPSまで上がった
- 空のマップでは約30~40FPSが出て、1時間ほどプレイした後も同程度の水準と断続的な引っかかりが続いた
エンジンとレンダリング構造
- Cities: Skylines 2はUnity 2022.3.7ベースで、UnityのDOTS、ECS、Burst compilerを活用している
- DOTSのおかげで、前作より複数のCPUコアをはるかに効率よく使っているように見える
- ゲームロジックはコード上およそ1200個のシステムで構成されており、実質的に大半のゲームロジックがECS構造の上に置かれているようだ
- UIはUnity UI Toolkitではなく、HTML、CSS、JavaScriptベースのCoherent Gamefaceを使用している
- JSバンドルにはReactとWebpack使用の痕跡がある
- UIは保守や修正の面で利点があるかもしれないが、分析データ上では主要ボトルネックではない
- グラフィックスはDirect3D 11とUnity HDRPを使用している
- DOTS/ECSで作ったゲームを既存のUnityレンダリングシステムにつなぐには別レイヤーが必要だが、Cities: Skylines 2はUnity Entities Graphicsを使っていないように見える
- Entities Graphicsのskinningとocclusion cullingはexperimentalと表示されている
- virtual texturingはサポートされていない
- 代わりに
BatchRendererGroupと低レベルコードを使って独自の接続レイヤーを実装したようだ
Renderdoc分析環境と限界
- レンダリング分析にはRenderdocが使われた
- Game Pass版はサンドボックス化またはファイル権限の問題により、RenderdocとNVidia Nsight Graphicsが実行ファイルへアクセスできなかった
- Steam版もParadox LauncherとSteam認証フローのため通常の方法ではRenderdoc接続が難しく、最終的にRenderdocのGlobal Process Hookでキャプチャに成功した
- NVidia Nsight GraphicsはSteamをNsightから開き、その中でゲームを起動する方式で動作したが、D3D11では多くのプロファイリング機能がサポートされておらず、Renderdocより多くの情報は得られなかった
- 分析対象のフレームは人口約1000人規模の都市でキャプチャされた
- ゲームバージョンは
1.0.11f1 - 最新パッチ
1.0.12f1には一部改善が含まれるが、問題をすべて解決してはいない - Renderdoc測定のフレーム時間は87.8msで、約11.4FPSに相当する
- 実際のゲームプレイ中の平均は30~40FPSだったため、Renderdocのオーバーヘッド、またはそのフレーム固有の外れ値の可能性がある
- ゲームバージョンは
サンプルフレームのレンダリング指標
- Renderdocが報告したサンプルフレームの統計は以下のとおり
- Draw calls: 6705
- Dispatch calls: 191
- API calls: 53361
- Index/vertex bind calls: 8724
- Constant bind calls: 25006
- Resource update calls: 1679
- Textures: 342個、約3926MB
- Render targets: 180個、約2328MB
- Buffers: 4144個、約447MB
- GPU bufferとtextureの合計: 約6.7GB
- 6.7GBのVRAM使用量は比較的単純なシーンとしては高く、8GB VRAMを持つ現行世代の中級GPUも依然として存在する
- FAQの追加分析によれば、このフレームのレンダリングには1億2100万 input verticesと約3600万 rasterized trianglesが含まれる
- これは画面に実際に見えている総ポリゴン数ではなく、全レンダーパス全体で処理されたジオメトリ量である
- Redditでは、より大きな都市で数億頂点、特定状況ではフレームあたり最大10億頂点に達するという報告もある
DOTSインスタンシングとGPUデータ更新
- ほぼすべてのdraw callでinstancingが使われている
- ゲームは全オブジェクトのレンダリングに必要なインスタンスデータを1つの大きなバッファに格納している
- 一般的なゲームオブジェクトはインスタンスあたり約50個のfloatを使用し、道路はさらに多くのデータを使っているようだ
- 可視オブジェクトのインスタンスデータは毎フレームバッファに更新され、GPUへアップロードされる
- バッファは約60MBから始まり、必要に応じてより大きなサイズへ再割り当てされる
- このバッファはほぼすべてのdraw callで使われ、Renderdoc上ではvertex shaderとpixel shaderの両方からアクセス可能な状態だった
- すべての頂点でバッファ参照コストが発生する可能性があり、高ポリゴンメッシュ問題と組み合わさっている恐れがある
シミュレーションと仮想テクスチャリング
- GPU compute shaderは、水、雪、パーティクル、スケルトンアニメーションなどのグラフィックス関連シミュレーションに使われている
- これらの処理は合計約1.5msで、全フレーム時間の2%未満である
- ゲームの実際のシミュレーションをGPUへ大量オフロードしているという当初の推測は、逆コンパイルしたコードとGPU callの観点からは当てはまらない
- Cities: Skylines 2は独自の仮想テクスチャリング/テクスチャストリーミングシステムを実装しているようだ
- Unity内蔵のStreaming Virtual Texturingは実験的・未サポートの状態にとどまっている
- ゲームは地形を除く静的3Dオブジェクトの大半で仮想テクスチャリングを使っているように見える
- 仮想テクスチャリングは必要なテクスチャタイルだけを読み込んでメモリを節約できるが、現行実装では近距離の表面でも高解像度テクスチャが読み込まれない問題がある
- anisotropic texture filtering非対応も、仮想テクスチャリング採用と関係している可能性がある
- このパスは約0.5msかかる
主要レンダリングパスごとのコスト
-
Skybox generation
- Unity HDRPの物理ベース空システムを使ってcubemapを毎フレーム生成している
- 約0.65msかかり、60FPS基準ではフレーム予算のほぼ4%に相当する
-
Pre-pass
- deferred renderingの第一段階として、depth、normal、smoothness推定情報を別テクスチャに書き込む
- 約8.2msかかり、非常に重い
-
Motion vectors
- anti-aliasingとmotion blurに使うper-pixel motion vectorを別パスでレンダリングする
- 約0.6msかかる
- motion vectorは一部壊れているように見え、このため執筆時点ではDLSSやFSR2をサポートしていない
-
Roads and decals
- 道路、草、地形表面に沿う要素をレンダリングする
- 約1msかかる
-
Main pass
- deferred renderingの中核パスで、ここまでのバッファと仮想テクスチャキャッシュを使ってalbedo、normal、PBR属性、depthなどを生成する
- 仮想テクスチャのvisibility情報もここで生成される
- 水平方向の解像度半分でレンダリングされているようで、地形は仮想テクスチャリングを使わずフル解像度でレンダリングされる
- 約16.7msかかり、60FPSでフレーム全体に許される時間と同じである
-
Ambient occlusion
- shader debug nameから判断するとGTAOを使っているようだ
- 約1.6msかかる
-
SSR + SSGI
- Unity HDRPのscreen space reflectionsとscreen space global illuminationを使っている
- 2つの効果を合算して約3msかかる
-
Deferred lighting
- 先に生成した中間バッファを合成し、ほぼ最終画像に近い結果を作る
- 約2.1msかかる
-
Water rendering
- compute shaderの前処理と縮小・ブラー済み画像入力を用いて水面をレンダリングする
- 約1msかかる
-
Post-processing
- temporal AA、bloom、tonemapping、有効時はDOFとmotion blurを使用する
- 全体で約1~2msかかる
-
UI
- GamefaceベースのUIとワールド内テキストをレンダリングする
- 道路名は2D signed distance fieldsでレンダリングされ、depth bufferを使って建物の後ろにあるときはシーンとブレンドされる
- 最終UIパスの時間は無視できるほど小さい
歯の論争とキャラクターモデル
- 市民キャラクターは実際に完全な歯のモデルを持っており、ゲーム内の通常視点では見えない
- RedditユーザーHexcoder0のNVidia Nsight Graphics分析をきっかけに、歯が常に最高品質でレンダリングされている事実が知られるようになった
- より重要な問題は、キャラクター関連メッシュ全般にLOD variantがない点である
- Colossal Orderはこの問題を公に認め、より広いLOD処理の問題にも言及した
- 市民モデルはDidimo Popul8ベースで生成され、ゲーム内の歯・口モデルは6108 verticesで、Didimoの基本メッシュ1060 verticesより多い
- 髪、衣装、アクセサリーを除いた単一キャラクターだけで約5万6000 verticesに達する
- 平均的な低密度住宅建物は、庭の小物や細部要素を除けば1万vertices未満である
- サンプルフレームでは歯13セットがレンダリングされているが、最終画像には1ピクセルたりとも影響していない
- 歯は性能低下の単独原因というより、不必要な高ポリゴンジオメトリが広範に存在する証拠に近い
高ポリゴン小物とカリング問題
- ゲームは最終画像にほとんど、あるいは全く寄与しないオブジェクトを多すぎる数、かつ高すぎるポリゴン数でレンダリングしている
- 主な原因は2つある
- 一部モデルにLOD variantがない
- 独自カリングシステムが単純で、frustum cullingしか実装されておらず、occlusion cullingの痕跡がない
- 距離ベースのカリングは存在するが積極的ではなく、pop-inは減る一方でパフォーマンスには不利になっている
- 確認された高ポリゴン小物の例は以下のとおり
- ガスボンベのパレット: 1万7000 vertices超
- 物干しロープの小物: 1個あたり2万5000 vertices、より密なvariantは3万 vertices超
- 駐車ブース: 4万 vertices超、LODなし、モニターやキーボード接続ケーブルまで個別モデリングされている
- 丸太の山: 10万 vertices超
- 建物と室内小物を1つのメッシュに統合するとdraw callは減るが、内部小物を個別にカリングできない
- 都市建設ゲームでは、同じ非効率なモデルが1フレームで数百回レンダリングされうるため、小さな無駄が積み重なる
- 高解像度モデル自体が問題なのではなく、現在の問題は高いディテールをゲームがさばき切れておらず、ポリゴン使用が非効率かつ不整合である点にある
シャドウパスが最大のボトルネック
- Cities: Skylines 2はcascaded shadow mappingを使用している
- 影にはartifactやflickeringが多く、太陽や木の葉が動くと特に目立つ
- ゲームは4つのcascadeを使い、cascadeごとの解像度は2048×2048である
- 高度なグラフィック設定にdirectional shadow map resolution項目はあるが、執筆時点のコードには接続されていない
- 個別設定でも全体のshadow quality設定でもshadow map resolutionは変わらない
- mediumとhighのshadow presetは実質同じである
- low presetでは地形が落とす影を無効にする
- 品質が低いにもかかわらず、shadow mappingは約40msで最も遅いレンダリングパスである
- サンプルフレームの6705個のdraw callのうち4828個、つまり72%がshadow mappingに使われている
- ゲームはすべての3Dオブジェクトを、サイズや距離に関係なく、すべての品質設定で潜在的なshadow casterとして扱っているように見える
- Renderdocの性能カウンターでは、多くのdraw callがshadow mapの0~100ピクセル未満にしか影響しておらず、歯もシャドウパスに再登場する
- LODとカリングの改善はshadow mapping性能にも大きな影響を与えうる
- 前向きな細部として、ゲームは都市の現在の日付・時刻・座標を使って太陽と月の位置を計算している
メインメニューのパフォーマンス問題
- メインメニューは静的背景画像とボタンしかないように見えるが、実際には3Dシーンが常に存在している
- メニューの背後では、地形、水、skyboxを含むシーンがレンダリングされ、その後UIが完全に覆い隠す
- 見えないシーンにもレンダリングパイプライン全体が使われるため、グラフィック設定はメインメニューの性能にも即座に影響する
- 発売時点では大半の設定がデフォルトで最大近くになっており、開発元が無効化を推奨した重い効果も含まれていた
- ただし、初回起動時に7FPS前後まで落ちた原因は完全には解明されていない
- 同程度の性能低下はその後再現されていない
- 初回起動時には仮想テクスチャキャッシュ処理が行われるが、GPU使用の有無は確認されていない
- メインメニュー全体のシーンには約400 draw calls、56万3000 input vertices、74万5000 rasterized trianglesが含まれる
FAQで補足された解釈
- Unreal Engine 5で作るべきだったかは断定しにくい
- UE5にはNanite、Lumen、Virtual Shadow Mapsがあり、C:S2が抱える一部問題に対応できる可能性がある
- 一方でUnity ECSに相当する大規模ゲームロジック・シミュレーション向けの実用レベル機能は不足しており、C++中心構造はモッディングの柔軟性とアクセス性で不利になりうる
- ゲームにLODが全くないわけではない
- 多くの建物には適切なLODがあるようだ
- パイプ、庭の小物、装飾物のような要素ではLODがない、または選択されていない例が多い
- InstaLODはゲームのasset pipeline、とくにmod toolで新しいassetを取り込む際に使われているようで、ランタイムレンダリングには使われていないようだ
- JavaScriptベースのUIは分析データ上、主要ボトルネックではない
- GamefaceはElectronのような完全なブラウザーエンジン基盤ではなく、ゲームUI用途向けのカスタムフレームワークである
- Chromium/BlinkやWebKitベースよりメモリ使用量と性能で有利なはずだという説明がある
- Renderdocは精密ベンチマークツールとしては限界があるが、「ゲームが何をしていて遅いのか」を把握するには十分な証拠を与えている
結論: CPUボトルネックを減らした代わりにGPUパイプラインは未完成に近い
- Cities: Skylines 2がGPUに過剰な負荷をかけている直接的な理由は、グラフィックカードに不要なジオメトリを送りすぎているためである
- このジオメトリの無駄は、多くのメッシュにおけるLOD不足と、単純で調整不足のカリング実装に起因する
- 独自のカリングおよびレンダリング接続レイヤーを実装した理由は、Unity DOTSとHDRPの統合がまだ進行中で、実ゲームに使うには制約が大きかったためだと思われる
- Unityの仮想テクスチャリングも実験的な状態のため、Colossal Orderは独自ソリューションを実装しており、このシステムにもまだ完成度の低い部分がある
- 推測される流れとして、Colossal OrderはUnityの新技術であるDOTSに賭け、CPUボトルネックとシミュレーション規模の面では成果を得たものの、グラフィックス面ではカリング、アニメーション、テクスチャストリーミングなどのシステムを自前で実装せざるを得なかったようだ
- ゲームが当初から30FPSを目標にしていたという開発元の主張は信じがたいとする評価があり、純粋なPCゲームであることやグラフィック品質を踏まえると、性能目標として正当化しにくいとの判断である
- 改善余地が大きい領域としては、LOD追加、カリング改善、shadow caster選定の最適化、小物単位のasset整理などがあり、多くのassetを個別に手直しする必要があるため時間を要する可能性が高い
1件のコメント
Hacker News のコメント
興味深い記事なので、具体的に何が興味深いのかを中心に議論してほしい。
こういうスレッドはすぐに
$THINGそのもの、ここではゲーム全般、あるいは$RELATED、ここではフレームワーク、または似たような対象全般についての一般論に流れがち。原則として悪いわけではないが、一般論へ一歩ずつ進むほど議論は浅くなり、面白さも薄れる。だからサイトのガイドラインにも「一般的な脇道の議論を避ける」が入っている - https://news.ycombinator.com/newsguidelines.html
「Colossal Order が Unity 内蔵のカリングを使わず独自実装を置いた理由は、DOTS と HDRP の Unity 統合がまだ進行中で、実際のゲームの大半には不向きと見なせるため、グラフィックス側をかなり自前で実装する必要があったから」というくだりは、私の Unity ツールでの経験とも悲しいほど合致する。
DOTS はリリースされたが、Unity が買収した他のツールと同じように実装が放置されているように感じる。会社の運営は深刻に間違っていて、数週間前に公に炎上した価格ポリシーの件を見ると、エンジン改善よりもユーザーからさらに金を搾り取る方法に集中しているように見える。
Bevy の ECS 実装は本当に良いので、Godex とともにこの分野で成功してほしい。
ゲームプレイ層のキャッシュ一貫性で、エンジンレベルのボトルネックを直すことはできない。
例えば Godot は ECS 中心ではなく、レンダリングや物理など各専門領域をほぼ独立して処理する自律的なゲームサブシステムである「サーバー」の概念を中心としており、一般的なゲームロジックとは疎結合になっている。
ECS アーキテクチャは、CPU がひどかった PS2/PS3 時代に由来する。小さいキャッシュ、大きな分岐予測ミスのコスト、遅いメモリ、断片化したメモリ空間、ランダムアクセスストレージの欠如のため、開発者は予測可能なメモリアクセスパターンに合わせてゲームを作る必要があり、その結果、ゲームデータを小さな塊としてストリーミングして処理する構造が一般的だった。
こうした方式はいまでも概ね良い慣行だが、超高速 CPU、優れた投機実行、良い分岐予測器、数十 MB のキャッシュが登場したことで、もはや厳格な必須条件ではない。特に現代のゲームが、画面上で起きることの規模を10〜20年前よりそこまで大きく増やしていない点を見ればなおさらだ。アクションゲームで、プレイヤーが一度に十数人を超える敵と戦うケースもまだ珍しい。
画面上に数千個の要素が同時に出るゲームでは、たいてい別の専門ロジックと処理が必要になる。
このゲームを試そうとしている人へのヒント: 解像度スケーリングを動的から固定に変えればよい。
3080 でメインメニューが「プレイ不能な 10fps」程度だったのが、中〜高設定でゲーム内では「問題なく快適に動く」程度に変わった。
参考までに、Crysis の1フレームは、ベンチマークシーン基準でおおよそ 30万頂点または三角形くらいだったと記憶している。となると、記憶がどちらにずれているか、各モデルの頂点/三角形比率がどれほど悪いかによって、丸太の山 3〜10 個分くらいになる。
追記: RenderDoc で頂点数とポリゴン数を確認した。記事の例のシーンは 1億2,100万頂点と4,000万個超の三角形を処理している。
文体が本当に良かった:
「このパスは約 8.2 ミリ秒、つまりおおよそ馬鹿げて長い時間がかかるため、驚くほど重い…」のような文。
「この丸太の山のメッシュも影レンダリングパスでしか使われないのに、10万個を超える頂点を持っている」とは…でもなぜ?
AAA ゲームにいくつか不完全で最適化されていない要素があるのは、完全に普通のことだ。予算は常に限られ、開発期間は短い。しかも成果が保証されないヒット依存の産業でもある。ゲームの成功確率を高める方法はいくつかあるが、たいてい開発よりも管理側に近く、予想予約売上はあまり当たらない。だからリスクを減らすために妥協しなければならず、可能なところでコストを削り、最も高価な開発優先事項の優先度を下げることになる。こうした妥協は、単一のメッシュが最適化されていないことよりはるかに大きい。メッシュ一つなど何でもない。
このメッシュが LOD0 で、歯のメッシュも LOD0 だというのは面白い事実だ。だがそれだけでゲーム性能が崩壊するわけではなく、実際の性能修正の代わりにこれを直す可能性も低そうだ。このスレッドでこうしたメッシュに執着するのは少し行き過ぎだ。
感情的に熱くなったコメントが多いので、そこにさらに乗せたいわけではなく、文脈を補いたいだけだ。
ゲームが極めて詳細なモデルを使っている一方で、実際のピクセルとして表示されないものを賢く抽象化したりカリングしたりする仕組みを備えていない、という要約で合っている?
それなら簡単に直せるものなのか、それともあまりに中核的な構造なので、解決するには設計からやり直す必要があるのか?
「コツ」自体はよく知られているはずだが、実装が簡単だとは思わない
DOTSはMike Actionのアイデアから生まれた。彼の2014年のCppCon発表「Data-Oriented Design and C++」を見ればいい [1]。ただしTwitterによると、MikeはUnityを去ったらしい
[1] https://www.youtube.com/watch?v=rX0ItVEVjHc
UE5のNaniteがあれば何とか持ちこたえられたかもしれないが、あの程度に過剰なジオメトリは他のすべてを崩壊させたはず
Proton Experimentalで1080pにして、空のマップでさえ数fps以上出そうと40分費やしたが、諦めて返金した。ひどい性能が直ったらまた試すつもり
1作目は本当にたくさん楽しんだので、今作をプレイできないのがとても残念
「性能問題がある場合は、性能に影響する問題を解決している間、画面解像度を1080pに下げ、被写界深度とボリュメトリックをオフにし、グローバルイルミネーションを下げることを推奨します」
AMD Radeon RX 5700 XTで滑らかな性能を得るために自分がしたのはこれがすべてだった