大規模スケールにおけるパフォーマンス:Unreal Engine 4.26 のシーケンサー

Epic Games シニア プログラマー Andrew Rodham
リアルタイム シネマティック コンテンツの複雑度やスケール、忠実度の高まりに伴い、Unreal Engine についてもさらなる品質向上が期待されるなか、当社では Unreal Engine のシネマティック ツール「シーケンサー」のラインタイム機能をじっくりと厳密に評価し、さらなる最適化の対象となり得る領域を特定する取り組みを始めました。

UE 4.26 では、大規模スケールのシネマティックスおよび UI アニメーションの同時実行において、以前よりも飛躍的に高いパフォーマンスを実現できるよう、内部構造の改善を通じてシーケンサーが大幅に最適化されています。この技術ブログ記事では、データ指向のデザイン原則に従って、シーケンサーのデータ構造およびランタイム ロジックを再編することで、膨大なデータを扱い、より多くのサードパーティ拡張機能を利用する上で、潜在的な改善点を含むさまざまなメリットをもたらす仕組みについてご紹介します。これらの改善や最適化により、将来的にはシーケンサーを使ってよりインタラクティブでダイナミックなコンテンツの作成も可能になります。
 

シーケンサーとは?

「シーケンサー」とは Unreal Engine のノンリニア編集ツールで、カットシーン、ゲーム内イベント、UMG ウィジェット、オフラインでレンダリングされたムービーなどのアニメーションをキーフレーム化することができます。このツールは、主に「Object Bindings (オブジェクト バインディング)」、「Tracks (トラック)」、「Sections (セクション)」という 3 つの要素で構成されています。
 
図 1 -- シーケンサー内の各要素の配置場所
 
セクションには一般的に、キーフレーム データと、タイムラインに沿ってそれぞれのトランフォーム、プロパティ、またはアニメーションの希望の状態を定義する他のプロパティが含まれています。ゲーム内でシーケンスを再生する際のエンジンの役割は、これらのトラックとセクションをすべて受け取り、ゲーム ワールドから解決された目的のオブジェクトに正しい状態とプロパティを適用することです。

4.26 より前のバージョンでは、これらのトラックとセクションのそれぞれには、特定のタイミングで評価を行い、解決されたオブジェクトにプロパティを適用するために必要なデータとロジックの両方を含む独自のランタイム インスタンスがありました。これらのインスタンスは従来のオブジェクト指向のパラダイムを使用するよう設計されており、仮想 API を通じてアクセスします。このアプローチでは、少数のトラックについては許容範囲のパフォーマンスを提供できますが、トラックの数が増加して相対的な複雑度が増すとうまく対応できなくなります。さらに、膨大な数のアニメーションまたはシーケンスを並行して実行すると、それぞれのインスタンスのパイプラインを初期化するための、高レベルのパフォーマンス コストが増加します。

広範なユース ケースの CPU プロファイル データへのアクセスにより、最適化のための努力の結果、仮想関数呼び出し固有のオーバーヘッドと低レベルのキャッシュ局所性が原因で、大規模スケールにおいては収穫逓減が生じることが明らかになりました。さらに、ブレンド、部分的にアニメートされたトランスフォーム、トランスフォームの相対的原点、アタッチメント、コンポーネント速度などに対する役割が増すにつれて、トランスフォーム トラックなどのトラックが負担となりつつありました。

コンソールまたはモバイル ハードウェア上のその他すべてのゲーム システムとともに、このようなコンテンツをリアルタイムで実行するために必要な最適化を実現するには、さらに多くのシステム再設計が必要であることが明らかになりました。
 

スピード重視のデザイン

シーケンサー ランタイムの新しい基礎について、当社ではデザインに関して次の各目標に重点を置きました。

スケーラビリティ
新しいランタイムでは、数百ものトラックまたはシーケンスを含むコンテンツの作成が可能になり、このコンテンツ全体の評価ロジックを最適化することができるようになります。これには以下が含まれます。
  • アクティブなシーケンサー トラックの数によってパフォーマンスがすぐに低下しないよう、データを割り当てて整理する。
  • 不要なロジックとブランチを完全に削除する機能。フレームごとにかかるこれらのコストを省くことができます。最速のコードというものは存在しません。これは、最上位のレベルだけでなく、シーケンサー評価コードのあらゆる面で適用できるはずです。
  • 評価ロジックには、複雑または非効率的な抽象化を通じてメモリとやり取りする必要なく、バッチ内の必要なデータへの無制限で直接的な高効率のアクセスが必要。

同時並行性
評価ロジックの記述はシンプルで、アップストリーム/ダウンストリームの依存関係 (すべてのカーブを並行して評価するなど) についての柔軟な表現の定義を含む、複数のコアに安全・効果的に自然な形で拡張されるものである必要があります。これにより、パイプラインを一度だけ設定すればよいという利点によって、多数の小さくて軽量のアニメーション、さらには巨大なシーケンスに関連してメリットがもたらされます。

拡張性
コア システムを再実装せずに、ロジックをビルトインの機能に加えることができる必要があります。また、コア機能とやり取りするアップストリームまたはダウンストリームの機能の追加も合理的にある程度可能である必要があります。これには以下が含まれます。
  • 現在のフレームのデータは、パイプラインのあらゆる時点ですべて透明で可変である必要がある。
  • 信頼のおける依存関係管理。
これらのデザイン目標は、問題のあるシーケンサー領域も併せて、データ指向のデザイン原則が適していることを示しています。これには次の理由が挙げられます。
  • ほとんどのシーケンサー データは一様であり、連続してレイアウトすることが可能。
  • 一般的に、各データ トランスフォーメーションのロジックは自己完結型であり、コンテキストに依存しない (例:カーブに対して f(x) を実行)。
  • 制御およびデータ フローはリニアであり、循環依存または再帰依存は存在しない (例:計算済みの値の再計算を必要とするロジックはない)。
  • スレッド化について制限があるのは、初期の設定と最終的なプロパティのセッターのみ。

4.26 は、エンティティ コンポーネント システムのパターンに基づき、評価フレームワークを介して、キャッシュ効率が高く透明な方法で評価データがレイアウトされるように再デザインされています。トラック インスタンス (内部では「テンプレート」と呼ばれる) は、評価において必須ではなくなりました (ただし、レガシー ランタイムは新しいシステムとともに実行されます)。代わりに、トラックのそれぞれのタイプのデータとロジックが分離されました。エンティティがソース トラック データに関連するコンポーネントのパーツを表し、システムが特定のコンポーネントまたはコンポーネントの組み合わせに関連する単一のロジカル データ トランスフォーメーションを表すようになりました。システムでは、クエリに一致するすべてのデータをバッチで処理するようになりました。これにより、例えばすべてのトランスフォーム プロパティや、すべての浮動小数点チャンネルの評価を行うキャッシュ効率の高いアプリケーションが、アニメートされるトランスフォーム プロパティの数にかかわらず、キャッシュミスもほとんどなしに単一の関数呼び出しのみで有効になります。

結果として、4.26 以降のバージョンから移行したトラック タイプの大規模コンテンツに対する、シーケンサーによる評価にかかるオーバーヘッドが大幅に低下しました。 

さらに、このアプローチでは多くのシーケンスまたはウィジェット アニメーションの評価データを融合することもでき、実行中の一連のアニメーション全体に対する最適化の可能性がさらに広がります。これにより、以前よりも多くのウィジェット アニメーションを同時に実行し、必要に応じて、複数の個別のアニメーションまたはシーケンスを一緒にブレンドできるようになります。

パイプラインの例 
すべてのトランスフォーム トラックを評価するためのパイプラインの例を見てみましょう。シーケンサーのトランスフォーム トラックは、そのコンポジット浮動小数点 (位置 - X/Y/Z、回転 - ロール/ピッチ/ヨー、スケール - X/Y/Z) に対する 1 ~ 9 個のキーフレーム チャンネルで構成されています。トランスフォームをオブジェクトに適用するには、まず現在の時点におけるこれらのカーブを評価し、次にその結果とともに USceneComponent::SetRelativeTransform を呼び出します。4.26 より前のバージョンでは、チャンネル データは、最大 9 個のチャンネルと、これらのチャンネルを評価してオブジェクトに適用するロジックを含むテンプレート オブジェクトにコピーされていました。
Figure 2
図 2 -- 4.26 より前のバージョンのトランスフォーム トラック評価
 
このデザインでは、仮想関数呼び出し (ダイナミック ディスパッチ) の数は、トラックの数とそれ自体 (キャッシュミスの可能性) に伴って直線的にスケーリングされます。

新しいフレームワークでは、このトラックがそのオブジェクトに対して解決された際に、トランスフォーム エンティティがポインタとともにコンポジット浮動小数点チャンネル、各チャンネルの評価結果の浮動小数点、USceneComponent::SetRelativeTransform への関数ポインタ、エンティティがトランスフォーム プロパティであることを示すタグに作成されます。Figure 3
図 3 -- 4.26 バージョン以降の新しいトランスフォーム トラック評価
以下に示す理由により、新しいアプローチには大幅な改善がなされていることがわかります。
  • 関心、データ、インストラクション キャッシュ局所性がよりうまく分離され、大規模スケールにおいてより高速であることが実証されている。 
  • 仮想関数呼び出しの数は、個別のトラック数ではなく、アクティブなシステム タイプの数に伴ってスケーリングされる。 
  • キャッシュミスの数は、データ全体のサイズではなく、コンポーネント データの組み合わせの数に比例している。 
  • さらに、最適化がそれぞれのトラックに限定されるのではなく、すべてのデータ セットにわたってこれらのメリットを得られる可能性がある。 
  • ロジックの大幅な変更なしに、パイプラインのパーツを並行して作成できるようになった。
例えば、上記の例で示されている「Evaluate Float Channels」(浮動小数点チャンネルの評価) システムでは、アニメートするプロパティや、組み合わせまたは適用方法にかかわらず、現在のフレームの浮動小数点チャンネルをすべて同時に評価することができます。

さらに、複数のトランスフォームを単一のオブジェクトにブレンドするものなど、中間データ トランスフォーメーションの導入がよりシンプルになり、より区分化されるようになります。

4.26 より前のバージョンでは、この機能は、すべてのプロパティ タイプのすべての入力/出力を格納して処理する専用クラス内に実装されていました。これにより、小さな割り当ての最適化とダイナミック ディスパッチにかかるコストが原因で、格納と CPU のオーバーヘッドが生じました。また、さらに複雑度が増し、共通コード パス (ブレンドなし) に降格されたブレンド コード パスを理解できるよう、プロパティの適用を表現する必要もありました。
Figure 4
図 4 -- 4.26 より前のバージョンでのトランスフォーム トラック評価のブレンド
新しいパイプラインでは、このブレンド コードを固有の ID を割り当てられる入力と出力とともに、コード独自のシステム内に維持することができます。並行処理されることとデータ モデルの効率上のメリットによって処理が高速化されるだけでなく、ブレンドが不要な場合にはシステム自体が存在しないため、ランタイム オーバーヘッドがゼロになります。 
Figure 5
図 5 -- 4.26 バージョン以降のトランスフォーム トラック評価のブレンド

メモリのレイアウト

シーケンサーのメモリ内における評価データの様子を詳しく見てみましょう。当社では、データ レイアウトに対するさまざまなアプローチを試した結果、メモリ サイズ、キャッシュ効率、再割り当てコスト、同時アクセスのバランスがうまく取れている下記のデータ モデルを採用することにしました。 

各エンティティは、「エンティティ割り当て」と呼ばれるバッチごとにグループ化されます。それぞれの割り当てには少なくとも 1 つのエンティティが含まれています。ここで重要なのは、割り当てに含まれるすべてのエンティティには、同じタイプのコンポーネントが同じ数だけ含まれている点です。コンポーネントを特定のエンティティに追加または削除すると、このコンポーネントは異なる (または新規の) 割り当てに移行されます。それぞれの割り当てのサイズは、予約されているキャパシティに応じてダイナミックに設定されており、malloc への呼び出しなしに新しいエンティティを追加できます (必要なのはコンポーネント データの初期化のみです)。
Figure 6
図 6 -- FEntityAllocation および FComponentHeader の割り当て例
例えば、次には、これらのチャンネルの評価に関連するサンプル データとロジックを含む Float Result 0、Float Channel 0 の割り当てと Eval Time コンポーネントが示されています。
Figure 7
図 7 -- エンティティ割り当てからのサンプル データ
ご覧のとおり、割り当て内では同じタイプのコンポーネントがすべて連続して並べられています。コンポーネント A、B、C を含む割り当てでは「AAA..A、BBB..B、CCC..C」というようにレイアウトされ、それぞれのタイプはキャッシュ行に沿って配置されます。データをこのように整理することで、エンティティ ID に影響を及ぼすことなく、そのエンティティ データの位置を制御できます。また、タイプ間の優れたパッキングを維持しながら、各コンポーネント配列に対する読み取りまたは書き込みアクセスを制御することも可能です (現時点ではミューテックスを使用しますが、競合が問題になる場合は将来において専用のスケジューラに拡張される可能性があります)。コンポーネント A と B のすべてのコンポーネント データをシステムで読み取る場合は、(ビット マスクで示された) 割り当てのタイプを確認するだけで済みます。このことから、システムでは、含まれる (数千にも及ぶ可能性のある) エンティティが、目的とするタイプと一致するかどうかを瞬時に判断できることがわかります。 

この「パターン マッチング」のアプローチでは、データ トランスフォーメーション ロジックをデータのコンポジションから完全に切り離すため、システムでは、関連しないパーツに関する情報なしに、大規模スケールでのパフォーマンスを維持しながら、コンポーネント データのさまざまな多数の組み合わせを並行して処理することができます。上記の例をさらに展開すると、すべてのトランスフォーム トラックをトランスフォーム原点に相対付けるシステムの挿入は簡単で、コア トランスフォーム システムへの煩わしい変更は必要ありません。
Figure 8
図 8 -- 4.26 以降のバージョンでの、トランスフォーム原点を含むトランスフォーム トラック評価

フェーズのアップデート

各シーケンサー システムは、独自の設定と制限を持ち、コンテキストに応じて実行される複数のさまざまなフェーズ内で実行することができます。これらの境界は、フレームごとに実行されるほとんどのシステムに対する非同期の評価ロジックを可能にしたまま、シーケンサーの順序付けに関する一部の厳格な要件を強制する上で役立つものです。これらは、境界を超えた場合またはバインディングが無効化されている場合 (スポーンおよびインスタンス化の各フェーズ) にのみ実行されるシステムと、フレームごとに実行されるシステム (評価および最終化) の 2 つのセクションに大別されます。 
  • スポーン:バインディングの解決前または後に実行する必要のあるスポーン可能なロジックとイベントのみを保管します。非同期タスクをディスパッチすることはできません。
  • インスタンス化:エンティティの作成/破棄を行うシステムを含むか、リンカ -> エンティティ マネージャ内のコンポーネントとタグを追加/削除します。このフェーズは、エンティティ マネージャの構造が変更された場合、またはオブジェクト バインディングが無効化された場合にのみ実行されます。非同期タスクをディスパッチすることはできません。
  • 評価:評価済みの状態を作り出すために各フレームで実行する必要のあるシステム。エンティティ マネージャの構造を変更することはできません (例:エンティティまたはコンポーネントの追加または削除)。ほとんどのシステムはここで実行されます。
  • 最終化:フレームの最後に実行されます。

これらのフェーズの違いにより、およそ 90% の共通コード パスの効率を可能な限り高く維持しながら、必要な場合にのみ、システムでより高コストの設定/解体ロジックを実行することができます。

スポーンとインスタンス化の両フェーズは、エンティティ マネージャ内で構造的な変更があった場合にのみ実行されます。例えば、新しいセクションが評価されている場合や、セクションが評価されなくなった場合、またはバインディングが無効な場合などです。一般的に、これらのフェーズではプロパティの解決や、エンティティ構造の変更、事前にアニメートされている値のキャッシングなど、高コストのタスクが実行されます。評価および最終化の各フェーズでは、(コンポーネント データを読み取る/書き込むことは可能であっても) エンティティ マネージャがロックされて構造的な変更は行えませんが、スポーンおよびインスタンス化の各フェーズではエンティティ マネージャを変更することが可能です。

下記の図では、シーケンサー評価を実行するための一般的なシーケンス フローを示しています。ご覧のとおり、どのシーケンスでも境界を超えていない場合は、評価および最終化フェーズのみが実行されます。 Figure 9
図 9 -- シーケンサー評価システム フェーズのアップデート

スレッド化

読み取り元と書き込み先のコンポーネントのタイプについてロジックを定義できるようになったので、コンポーネント データに対するアップストリームおよびダウンストリームの依存関係も定義できるようになりました。これにより、合理的な場合には、これらのデータ トランスフォーメーションをタスク グラフに安全かつ自動的に、そして非同期でディスパッチできるようになります。複雑なキーフレーム カーブを多数含むコンテンツなど、大規模なデータ セットを評価する際は、他の計算処理とともにこのようなカーブの評価を非同期で実行することができ、プラットフォームで許容されている並列処理を行うことができます。コンテキスト切り替えまたはスレッド プリエンプションの潜在的なコストと比べて見合うものであれば、これにはメリットがありますが、スレッドセーフな設計が施されたシーケンサーでは、この判断を内部で、またはユーザーが行うことができます。

シーケンサーで作業するプログラマー向けの包括的なアーキテクチャ詳細情報については、近日公開予定です。
 

旧版ランタイム システムと 4.26 以降のビヘイビアの違い

全般的に、ビヘイビアについては大きな変化はありません。スポーン可能なものやアタッチメント、トランスフォームやその他のプロパティは以前と同様に機能します。ただし、次に示すような細かな点は変更されました。
  • 同じタイプの複数のアクティブ シーケンスがすべて同時に評価されるようになりました。4.26 より前のバージョンでは、2 つの異なるアクティブ シーケンスが同じオブジェクトの同じプロパティでアニメートされる場合、それぞれが個別に評価されるため、そのプロパティのコントロールを巡って競合が生じます。4.26 ではこれらのシーケンスが一緒に評価され、自然な形で互いにブレンドされます。将来のリリースでは、シーケンスのブレンドまたはオーバーライド方法を制御できるよう機能拡張される予定です。
    • すべてのアクティブなウィジェット アニメーションがまとめて評価されるようになりました。
    • すべてのアクティブなレベル シーケンスがまとめて評価されるようになりました。
  • シーケンスまたはウィジェット アニメーションに対する手動での再生、停止、または位置設定時に、場合によっては、当該の関数呼び出し内でシーケンスが同期的に再評価されないようになりました。このような場合は、シーケンスの再生ヘッドを再生または移動するためのリクエストが次のフレームまで延期されることがあります。
    • パフォーマンス コスト (負荷) はかかりますが、シーケンスの [Async Evaluation (非同期評価)] オプションを無効にすることで、特定のシーケンスに対してこれを無効にすることができます。このように設定すると、同期的な再評価が強制的に実行され、前述のブレンド ビヘイビアは無効になります。
Figure 10
図 10 -- オンに設定されている [Async Evaluation] オプション 
  • 緩やかなカーブを含む Absolute Blend (絶対ブレンド) トラックがオブジェクトの初期値からブレンドされるようになりました。4.26 より前のバージョンでは、対象となる要素が 1 つだけであっても、ブレンドのウェイトは常に正規化されていました。つまり、相対から絶対へのブレンドには 2 つのセクションが必要でした。
    4.26 では、相対から絶対へのブレンドはイーズ イン/アウトを含むシンプルな絶対セクションとして表現することができます。下記の図 11 には、イーズ イン/アウトを含む絶対ブレンドの例が示されています。 

 
図 11 -- 絶対ブレンドの例
 
  • シーケンス評価に関して再入可能性が生じるイベント トラック (他のシーケンスを再生するイベントや再生状態を変更するイベント、再生ヘッドを移動するイベントなど) は、Post Evaluation 位置に限定されるようになりました。例えば、PreSpawn または PostSpawn では許可されません。これが、新しいイベント トラックのデフォルトです。4.26 より前のバージョンでは、イベント トラックのデフォルトは PostSpawn に設定されていました。既存のトラックについてはデフォルト設定として PostSpawn が維持されますが、新しいトラックではデフォルトで PostEval に設定されます。 
Figure 12図 12 -- イベント トラックの位置を [At End of Evaluation] に変更する

API に関する変更の最新情報

UE::MovieScene 名前空間
新しいシーケンサー コードベースは、クラスの命名にプレフィックスを使用する頻度を抑えるために、UE::MovieScene 名前空間に含まれています。一貫性を維持して新しいコーディング スタンダードに準拠できるように、この変更の一環として、既存の MovieScene 名前空間に含まれているすべてのコードは UE::MovieScene に移動されました。

ほとんどないと考えられますが、もう使用していない MovieScene 名前空間を参照するサードパーティのコードがある場合は、UE::MovieScene を使用するよう、そのタイプや関数を変更する必要があります。

UMovieSceneTrack
カスタム トラックが実装されている場合、トラック テンプレートのコンパイルに関連する API は、それぞれ異なる評価方法をより分離するために、別のインターフェース (IMovieSceneTrackTemplateProducer) に移行されています。CreateTemplateForSectionCustomCompilePostCompile などの関数を定義するには、このインターフェースを UMovieSceneTrack タイプに追加してください。これらの関数はもはやベース クラスには存在しないためです。

Track/Segment Blenders
GetRowSegmentBlenderGetTrackSegmentBlender は廃止され、呼び出せないようになりました。トラックの評価に関連するフィールドは変更時またはアセットへの保存時に再生成されるため、これれらをコンパイル時に再計算する必要はありません。

カスタム オーバーラップ ビヘイビアの定義には、UMovieSceneTrack::PopulateEvaluationTreeFEvaluationTreePopulationRules で利用可能なビルトイン アルゴリズムとともに使用します。

UMovieSceneSection
テンプレートの定義を IMovieSceneTrackTemplateProducer::CreateTemplateForSection で行うにあたり、UMovieSceneSection::GenerateTemplate が削除されました。

ChannelProxy
サードパーティ セクション タイプにダイナミック チャンネル レイアウトが含まれる稀有なケース (コンストラクタ外で ChannelProxy が再作成される場合など) では、その構築を新しい関数である virtual EMovieSceneChannelProxyType CacheChannelProxy() 内で定義する必要があります。 まとめ 新しいランタイム システムに移行されたのはスポーン可能なもの、アタッチメント、イベント、2D/3D トランスフォーム、浮動小数点プロパティのみですが、これらの機能を多用するコンテンツではすでに大幅なスピードアップが確認されています。データ指向のデザイン原則を問題のあるシーケンサー領域に適用することで、最適化を図るための改善点を全体的に明らかにできるだけでなく、ランタイム内で関心をよりうまく分離することができ、高パフォーマンスを維持しながら、キーフレーム/トラック/サブシーケンスのパラメータ化、グローバル ブレンド、より優れたデバッグ ツールなどの新しい機能をビルドするための基礎となります。
以下の例では、手入力した独自のアニメーションを使って 500 台のスポーツ カー アクタが 4.25 および 4.26 で実行されています。手動で SetRelativeTransform を呼び出すオーバーヘッドは約 2.5 ミリ秒なので、シーケンサー オーバーヘッドがおよそ 7.5x であることと比べると、大幅に改善されていることがわかります。
 
図 13 -- SetRelative Transform コストにおけるパフォーマンスの違いを示すデモンストレーション
 
本記事をお読みくださりありがとうございました。本記事の情報がユーザーの皆さんにとって興味深く、役立つものであることを願っています。シーケンサーの詳細については、シーケンサーのドキュメント ページを参照してください。