『FINAL FANTASY VII REMAKE』におけるプロファイリング 詳細な処理負荷の回避と最適化に向けたワークフロー
5月17日から22日の日程で、エピック・ゲームズ・ジャパンは『UNREAL FEST EXTREME 2021 SUMMER』を開催した。本稿では、21日にスクウェア・エニックスの第一開発事業本部が実施した「『FINAL FANTASY VII REMAKE』におけるプロファイリングと最適化事例」にて行われた解説を記す。
最適化を進めるワークフロー
解説はリードテクニカルプログラマーの波能智人氏。
処理負荷計測の環境と最適化のワークフロー
本作のプロファイリングにはCPU ProfilerとGPU Profilerを使用。処理負荷を計測するに際し、スペックは上限を30FPS、動的解像度を1080p~900p、利用した環境はTestビルドしたパッケージとした(Stat FPS、Stat Unitは常時表示)。
計測のために行った最適化のワークフローは、QA(品質保証)からバグとして報告されたレギュレーションを守っていない箇所を、プロファイリング担当が原因の解析と改善方法を提案。そして修正を担当するプログラマーやアーティストが改善していくという流れになっている。
ローレベル最適化
スレッドの優先度とアフィニティーマスクの調整
アフィニティーマスクを使用して、フレームレートに対して影響が強いスレッドを優先的に避けるようにしてハードウェアのCPUを調整していく。GameThreadやRenderingThreadと同等かそれ以上に優先度の高いスレッドがCPUのコアを専有してしまうと、フレームレートに影響し、フレーム完了までに時間がかかる。
UNREAL ENGINE 4(UE4)における効率的なタスク並列化
UE4における並列化で問題となるのは、TickGroup実行中に依存関係をつけてタスクを実行すると動的な依存関係先への同期を解決のためにTaskGraphのスレッドでNullTaskが生成されてしまうところにある。NullTaskが生成されてしまうと他のタスク実行を妨げてしまうため、「FTickFunction::AddPrerequisite()」で依存関係をつける機能によって、TickGroup実行前に依存関係が解決できるため、NullTaskなしに並列化させることができたそうだ。
ヒッチ(処理落ち)対策
ヒッチの主な原因にはレベルストリーミング、ActorのSpawn、ガベージコレクション(GC)があり、これらのうちGCの対策について語られた。処理負荷の低減には、UE4でのアセット制作で生成されるUObjectの抑制も含まれる。UObjectが多いほどGCの検索数が増え、負荷も増えてしまう。
そこでDisregard for DCで対応してみると、UObject数を約10万ほど削減でき、負荷も3地点平均で約15ms(ミリ秒)の低減となった。続けてGC Clusterでの対応を追加すると、そこからさらに約7万ほど削減でき、3地点平均でも約10msの低減となったという。
またUE4.20から導入されたIncremantalBeginDestroy、UE4.23から導入されたAsync destructionを使用すると、さらなるGC破棄の改善が見られた。
コンパイラーによる最適化
コンパイラーによる最適化では、LTO(リンク時の最適化)とPGO(プロファイルに基づく最適化)を使用した。4地点でトレーニングランのQAを計測した結果、適用後にFrame Rateが約2.4FPS、Gameが約2.1ms、Drawが約1.5msの低減となった。
スレッドの比較ではGame Threadが約3.1ms、Render Threadが約2.5ms、RHI Threadが約1.1ms、全体では約8.1msの低減となっている。
ロード高速化
ロードの高速化では、レベルストリーミングにおけるロード時間の問題があり、タイムスライスのタイミリミット変更、負荷の軽減、PostLoadのAsync対応で改善を試みた。その結果、ロード時間はPlayStation®5での計測で約6秒の軽減となった。
さらにUE4.25から導入されたIOStoreとUnversioned Property Serialization(UPS)を使用すると、そこから約4.5秒の軽減となった。
アニメーション
ここで、解説はリードアニメーションプログラマーの原龍氏にバトンタッチ。
Character MovementとSkeletal Mesh Componentの並列化
本作では効率化として、Task Threadで実行されるCharacter Movement Component Tick内でUpdate Animationも同じタイミングにより並列実行、Game Threadで実行されるPost Character Movement Update内でシーンをクラスター分割して、必要なもの同士のみで押し当たり実行した。それからTask ThreadでSkeletal Mesh Component Tickを、Game ThreadでSkeletal Mesh Component Post Tickを実行する。
なおアニメーションマルチスレッド対応の心得として、可能な限りコンテキストやAPIの呼び出しをロックしないこと、Game Thread以外では自身に関するコンテキストのみ自由に読み書きしてもよいこと、Task ThreadからGame Threadといった組み合わせが基本になること、Task ThreadからTrace(Raycast)しないことなどが示された。
Trace Request Pool
IK(逆運動学)ではTrace(Raycast)が必要なので次のフレームで結果を受け取るTrace Request Poolで対応し、Task Thread全体の合計で約25msの軽減となった(Async Traceでの対応でも良さそうだが、こちらでは更新するフレームを間引けない)。
Skeleton Mesh関連のコンテキスト共有
コンテキストの共有は、メモリ使用量を抑制するために行う。その際Context PoolはSkeletal MeshとAnim Instance単位で用意。ボーンやブレンド率のマッピングなど、ランタイムでのアニメーション制御負荷を抑制するためのキャッシュ情報を共有している。
Inetial Blending
UE4.24から追加された慣性補間。モーションアセットのEvaluate回数が少なくなるためCPUコストが減る一方、メモリ使用量は増える。慣性補間とは直接関係しないが、試しにキャラクターのまぶたのブレンドスペースで全員分のEvaluateを行うと、Task Threadの合計が約3.7ms増えることからEvaluate回数を減らすことが負荷軽減に繋がることがわかる。
Animation Budget Allocator
アニメーションの更新に使用するために定めたバジェット内に収めるよう、動的に更新の有無や間隔を切り替える。本作では判定条件や停止処理のために改良したものを追加して、キャラクターに対してのみ使用している。結果としてTask Threadの合計が約3msの低減となった。
Animation Pack GC Cluster
パック内に含まれるモーションは特殊ケースを除いて単発ロードしないというルールに基づき、パックをGC Cluster Rootに設定してGCコストを抑えている。GC Cluster Rootとして扱っているのは、パックの自動生成持に各モーションのリファレンスを検索して、全てのリファレンスがパックのみとなっているパックに限定している。
Animaiton Compression Library
ACLはオープンソースの圧縮ライブラリー。本作では圧縮が不具合につながる場合、Bitwise Compress Onlyに変更しているが、ほとんどACLで圧縮している(ACLの方が40~50%圧縮率が高い)。