27.8K Views
December 09, 24
スライド概要
CPU最適化についてのプレゼン資料です。
AllowbatchTicks、GCの動作、FPrimitiveSceneDescについて解説しています
Unreal Engineを開発・提供しているエピック ゲームズ ジャパンによる公式アカウントです。 勉強会や配信などで行った講演資料を公開しています。 公式サイトはこちら https://www.unrealengine.com/ja/
UnrealEngine5 のCPU最適化テクニック EPIC GAMES JAPAN 鈴木 孝司
Tick自動バッチ Table of contents Tickのオーバーヘッドを軽減 ガベージコレクション ガベージコレクション時のヒッチを軽減 FPrimitiveSceneDesc プリミティブの直接描画
Tick自動バッチ Table of contents Tickのオーバーヘッドを軽減 ガベージコレクション ガベージコレクション時のヒッチを軽減 FPrimitiveSceneDesc プリミティブの直接描画
Tick自動バッチ 5.5
Myth-busting “Best Practices” in Unreal Engine | Unreal Fest Seattle 2024 https://www.youtube.com/live/lchh4c9spMw?t=18451s Myth-busting “Best Practices” in Unreal Engine @ Epic Developer Community https://dev.epicgames.com/community/learning/tutorials/l3E0/myth-busting-best-practices-in-unreal-e ngine
TaskGraph の動作 Actor TickFunction Component TickFunction
TaskGraph の動作 Actor TaskGraph TickFunction Queue Component TickFunction
TaskGraph の動作 Actor TaskGraph TickFunction Queue Component Tick Tick Tick Tick GameThread TickFunction Tick Tick Tick Tick Tick Tick Tick Tick WorkerThread WorkerThread
テストプロジェクト // Sets default values AMyTickTestActor::AMyTickTestActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; } // 空のTick void AMyTickTestActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); } x1000 x1000
大量のTickの動作
Tickのオーバーヘッド
Tickのオーバーヘッド Tickのオーバーヘッドは非常に小さいが0ではない ※ 0.3~0.5μs @Ryzen-3975wx DevelopmentEditor
5.5 tick.AllowBatchedTicks
tick.AllowBatchedTicks ● 自動でバッチ可能なTickFunctionを 一つのタスクにまとめる ● TaskGraphに詰むタイミングで判定 ● Prerequisiteも考慮される ● 条件 ○ ○ ○ HighPriority = false TickGroupを跨がない bIsBatch = true 5.5
効果
効果 ● 2000個のTickで0.6ms程度負荷削減 ○ おおよそ1tickあたり約0.3μs
利点 注意点 自動で大量のTickを合成して タスクグラフのオーバーヘッドを軽減 ● RunOnAnyThreadが考慮されない ○ ● bIsBatchで除外 デフォルトでOFF
Tick自動バッチ Table of contents Tickのオーバーヘッドを軽減 ガベージコレクション ガベージコレクション時のヒッチを軽減 FPrimitiveSceneDesc プリミティブの直接描画
ガベージコレクション
リリースノートより ● 5.4 UE5.4 ○ Added support for incremental Gather Unreachable Objects (post reachability analysis phase of garbage collection). It's now possible to split this phase into multiple frames to avoid hitches on slow or single-threaded platforms. ○ Added experimental support for incremental reachability analysis (a phase of garbage collection that determines which objects are to be destroyed). It's now possible to split reachability analysis into multiple frames with the specified per-frame soft time limit.
ガベージコレクション ● UObject継承クラスのオブジェクトについて、 参照されなくなったものを自動的に検出してメモリ 解放する仕組み ● 半自動的に様々なタイミングでトリガーされるが しばしばヒッチやフレームレート低下の原因となり 頭を悩ませることが良くあった ● メモリ解放が暗黙的に行われることでGC前後で不 正アクセスを生み出す原因になることもある
ガベージコレクションの流れ Reachability Analysis GC起動! Gather Unhash Destroy
ReachabilityAnalysis ● GCの起点となるルートオブジェクトから 参照が繋がっているかを走査 接続されているものにフラグを立てていく ● オブジェクト数の増大に応じて負荷が上昇 ● VerifyGCAssumption ○ GCの処理前に前提条件を検証 ○ パッケージビルド かつ 非Shippingビルドで動作 Reachability Analysis Gather Unhash Destroy
Gather Reachability Analysis ● ルートオブジェクトから 到達できないオブジェクトをリストに収集 Gather Unhash Destroy
Unhash Reachability Analysis ● UObject::ConditionalBeginDestroyを 呼び出す ○ Gather RenderResource解放などの Async処理をトリガー Unhash Destroy
Destory Reachability Analysis ● UObject::FinishDestroyを呼び出してメモリ を解放する Gather Unhash Destroy
フルパージ vs インクリメンタルGC ● この4つのフェーズを全て1Tickで処理してしまうも のがフルパージ ○ ● GEngine->ForceGarbageCollection(true) 設定にしたがってGCを中断し複数のフレームに跨っ て処理を継続していくのがインクリメンタルGC ○ GEngine->ForceGarbageCollection(false)
IncrementalGC ● GarbageCollectionが複数フレームに跨って 処理される
IncrementalGC ReachabilityAnalysis
IncrementalGC Gather
IncrementalGC
デフォルトのIncrementalGC動作 Reachability Analysis ● gc.IncrementalGCTimePerFrame 0.002 ● gc.LowMemory.MemoryThresholdMB 0 ● gc.LowMemory.IncrementalGCTimePerFrame 0.002 Gather Unhash Incremental Destroy
IncrementalGCの改善(Experimental) 5.4 ● gc.AllowIncrementalReachability 0 ● gc.IncrementalReachabilityTimeLimit 0.005 ● gc.AllowIncrementalGather 0 ● 注意点 ○ ルートオブジェクトからの走査範囲によっ ては指定時間を大きく超えることがある ○ GCと並列して動作している処理のタイミ ングによっては想定していないメモリ解放 が起こる可能性がある Incremental Reachability Analysis Incremental Gather Unhash Incremental Destroy
トリガー条件について パーシスタントレベル移動 FullPurge 一定時間毎のGC IncrementalGC レベルがストリームアウトした時 ※後述
レベルがストリーミングアウトした時の動作 (非WorldPartition) ● s.ForceGCAfterLevelStreamedOut 1 (Default) ○ レベルがアンロードされるたびにFullPurge ● s.ForceGCAfterLevelStreamedOut 0 ○ 解放待ちのレベルが s.ContinuouslyIncrementalGCWhileLevelsPendingPurge (デフォルト値:1)で設定された値を超える度にIncrementalGC ○ ただし残メモリが少ない場合は設定値に関わらず毎回トリガーされる サブレベルのアンロードが頻繁に起こるレベル構成を用いている場合 これらのコンソール変数の調整が必要
レベルがストリーミングアウトした時の動作 (WorldPartition) ● s.ForceGCAfterLevelStreamedOut が0にセットされる ○ WPセルのアンロード時にIncrementalGCがトリガーされることがある ● wp.Runtime.LevelStreamingContinuouslyIncrementalGCWhileLevelsPendingPurgeForWP ○ ○ デフォルト値 : 64 この値で s.ContinuouslyIncrementalGCWhileLevelsPendingPurgeをオーバーライドする デフォルトではアンロード待ちのWPセルが64個を超えると IncrementalGCがトリガーされる
Tick自動バッチ Table of contents Tickのオーバーヘッドを軽減 ガベージコレクション ガベージコレクション時のヒッチを軽減 FPrimitiveSceneDesc プリミティブの直接描画
FPrimitiveSceneDesc 5.4
How Small Open Doors Can Lead to Better CPU Utilization and Bigger Games | Unreal Fest 2024 https://www.youtube.com/watch?v=JaCf2Qmvy18
いくつかの変更について 既にUE5.4に取り込み済み
しかし
Lightweight Mesh Rendering: Render Static Mesh without Component https://godofpen.notion.site/Lightweight-Mesh-Rendering-Render-Static-Mesh-without-Component-d94 54730ab7749f4b3dfb93dde2f7bc9
GameThread アクターがレンダリングされるまでの流れ RenderThread
GameThread アクターがレンダリングされるまでの流れ Sp SpawnActor ee ria liz e n D Level Actor aw Component RenderThread
World SpawnActor Level GameThread アクターがレンダリングされるまでの流れ Actor AddActor Component RegisterComponent RenderThread
World SpawnActor GameThread アクターがレンダリングされるまでの流れ Renderer Actor CreateSceneProxy Level SceneProxy Component RenderThread
World SpawnActor Level GameThread アクターがレンダリングされるまでの流れ Renderer Actor SceneProxy Component GPUへ RenderThread
GameThread コンポーネントが変化した時 Renderer Actor SendAllEndOfFrameUpdates (更新 or 再作成) SceneProxy Component RenderThread
World SpawnActor Level GameThread ただ描画したいだけなら・・・ Renderer Actor SceneProxy Component RenderThread
直接SceneProxyを RenderThreadに渡した い!
GameThread SceneProxy直接投入 Renderer SceneProxy RenderThread
GameThread SceneProxy直接投入 Renderer StaticMeshCompoentを 代替するなにか SceneProxy RenderThread
FPrimitiveSceneDesc 5.4
関係クラス /構造体 FStaticMeshSceneProxyDesc UStaticMeshComponentからレンダリングに必要な情報だけを取り出した構造体 UStaticMeshComponentをコンストラクタに与えて生成できるほか、 独自の方法で設定してもよい FPrimitiveSceneDesc FStaticMeshSceneProxyDescへの参照をメンバに持ち さらに個々のインスタンスについてランタイムで必要な姿勢行列やバウンズなどを 保持する FPrimitiveSceneProxy いわゆるシーンプロキシーの基底クラス。 様々なコンポーネントに対応した構造体が実装されている。 FScene FPrimitiveSceneDescを受け取って処理を行うAPIを持っている UWorldを経由して取得する
FSceneの関連メンバ関数 // FPrimtiveDesc version for primitive/light scene interactions virtual void AddPrimitive(FPrimitiveSceneDesc* Primitive) = 0; virtual void RemovePrimitive(FPrimitiveSceneDesc* Primitive) = 0; virtual void ReleasePrimitive(FPrimitiveSceneDesc* Primitive) = 0; virtual void UpdatePrimitiveTransform(FPrimitiveSceneDesc* Primitive) = 0; virtual void BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*> InPrimitives) = 0; virtual void BatchRemovePrimitives(TArrayView<FPrimitiveSceneDesc*> InPrimitives) = 0; virtual void BatchReleasePrimitives(TArrayView<FPrimitiveSceneDesc*> InPrimitives) = 0; virtual void UpdateCustomPrimitiveData(FPrimitiveSceneDesc* Primitive, const FCustomPrimitiveData& CustomPrimitiveData) = 0; virtual void UpdatePrimitiveInstances(FInstancedStaticMeshSceneDesc* Primitive) = 0;
UStaticMeshComponent 独自のデータセット GameThread FPrimitveSceneDescを使った描画の流れ FStaticMeshSceneProxyDesc RenderThread
独自のデータセット FStaticMeshSceneProxyDesc Cr ea te S ce ne P ro xy UStaticMeshComponent GameThread StaticMeshSceneProxyDesc SceneProxy RenderThread FPrimitiveSceneDesc
UStaticMeshComponent GameThread StaticMeshSceneProxyDesc 独自のデータセット Renderer UWorld FStaticMeshSceneProxyDesc SceneProxy r im itive FScene FPrimitiveSceneDesc RenderThread Add P SceneProxy
コンポーネントを使わない 描画コード
コード例 (1/5) ● FPrimitiveSceneInfoData ○ レンダラーが書き込む変数群 ○ 描画インスタンス毎に必要 struct FSMSPSceneDesc : public FPrimitiveSceneDesc { public: FSMSPSceneDesc() { PrimitiveSceneData = &SceneInfoData; } FPrimitiveSceneInfoData SceneInfoData; };
コード例 アクター定義 (2/5)
//StaticMeshと配置情報の配列
USTRUCT(BlueprintType)
struct FStaticMeshInstance
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
UStaticMesh*
StaticMesh;
UPROPERTY()
//重いのでレベルエディタでは編集しない
TArray<FTransform> InstanceTransforms;
};
UCLASS()
class SMSPDESCSAMPLE_API ASMSPDescActor : public AActor
{
GENERATED_BODY()
public:
ASMSPDescActor();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void AddToScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void RemoveAllFromScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void SetupRandomInstances();
//ランダム配置用
UPROPERTY(Category = "Default", EditAnywhere )
bool bAutoInstanciate = false;
//メッシュおよび配置情報
UPROPERTY(EditDefaultsOnly, Category = "Default" )
TArray<FStaticMeshInstance>
Descs;
//ランタイムで利用するバッファ
TArray<FStaticMeshSceneProxyDesc>
SMSPDescs;
TArray<FSMSPInstance>
SceneDescs;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};
コード例 アクター定義 (2/5)
//StaticMeshと配置情報の配列
USTRUCT(BlueprintType)
struct FStaticMeshInstance
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
UStaticMesh*
StaticMesh;
UPROPERTY()
//重いのでレベルエディタでは編集しない
TArray<FTransform> InstanceTransforms;
};
FPrimitiveSceneProxyDesc配列
※StaticMesh毎
UCLASS()
class SMSPDESCSAMPLE_API ASMSPDescActor : public AActor
{
GENERATED_BODY()
public:
ASMSPDescActor();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void AddToScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void RemoveAllFromScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void SetupRandomInstances();
//ランダム配置用
UPROPERTY(Category = "Default", EditAnywhere )
bool bAutoInstanciate = false;
//メッシュおよび配置情報
UPROPERTY(EditDefaultsOnly, Category = "Default" )
TArray<FStaticMeshInstance>
Descs;
//ランタイムで利用するバッファ
TArray<FStaticMeshSceneProxyDesc>
FPrimitiveSceneDesc配列
※描画インスタンス毎
SMSPDescs;
TArray<FSMSPInstance>
SceneDescs;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};
コード例
描画その1 (3/5)
●
FObjectCacheEventSink
○ 分散処理させたときに
ボトルネックになるので
キューイングする
void ASMSPDescActor::AddToScene()
{
TRACE_CPUPROFILER_EVENT_SCOPE(ASMSPDescActor::AddToScene);
//テンプレート用のコンポーネント
UStaticMeshComponent*
TargetComponent = NewObject<UStaticMeshComponent>(this);
TargetComponent->RegisterComponentWithWorld(GetWorld());
FSceneInterface* Scene = GetWorld()->Scene;
int32 NumTotalInstances = 0;
FTransform RootTransform = RootComponent->GetComponentTransform();
//バッファを確保
for (const FStaticMeshInstance& Desc : Descs)
{
NumTotalInstances += Desc.InstanceTransforms.Num();
}
SceneDescs.AddDefaulted(NumTotalInstances);
SMSPDescs.Reserve(Descs.Num());
int32 DestStartIndex = 0;
#if WITH_EDITOR
FObjectCacheEventSink::BeginQueueNotifyEvents();
#endif
・・・次ページにつづく・・・
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif
コード例
描画その2 (4/5)
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif
コード例
描画その2 (4/5)
テンプレート用コンポーネントに
StaticMeshをセットし、そのテンプレートから
StaticMeshSceneProxyDescを作成
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif
コード例
描画その2 (4/5)
ParallelForによる並列化が可能
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif
コード例
描画その2 (4/5)
FOptionalTaskTagScoepは
ワーカスレッドで実行されるタスクが
どのスレッドに属しているかを指示する
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif
コード例
描画その2 (4/5)
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif
コード例
描画その2 (4/5)
ここでSceneProxyを作成している
CreateSceneProxy関数はこのあとに解説
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif
コード例
描画その2 (4/5)
BatchAddPrimitiveで
複数のプリミティブを一撃で登録
コード例
CreateSceneProxy
FPrimitiveSceneProxy* ASMSPDescActor::CreateSceneProxy( const FStaticMeshSceneProxyDesc& SceneProxyDesc )
{
const UStaticMesh* StaticMesh = SceneProxyDesc.StaticMesh;
if (StaticMesh == nullptr)
{
UE_LOG(LogStaticMesh, Verbose, TEXT("Skipping CreateSceneProxy for SceneProxyDesc %s (StaticMesh is null)"), *GetFullName());
return nullptr;
}
if (StaticMesh->IsCompiling())
{
UE_LOG(LogStaticMesh, Verbose, TEXT("Skipping CreateSceneProxy for SceneProxyDesc %s (StaticMesh is not ready)"), *GetFullName());
return nullptr;
}
if (StaticMesh->GetRenderData() == nullptr)
{
UE_LOG(LogStaticMesh, Verbose, TEXT("Skipping CreateSceneProxy for SceneProxyDesc %s (RenderData is null)"), *GetFullName());
return nullptr;
}
if (!StaticMesh->GetRenderData()->IsInitialized())
{
UE_LOG(LogStaticMesh, Verbose, TEXT("Skipping CreateSceneProxy for SceneProxyDesc %s (RenderData is not initialized)"),
*GetFullName());
return nullptr;
}
Nanite::FMaterialAudit NaniteMaterials{};
// Is Nanite supported, and is there built Nanite data for this static mesh?
const bool bUseNanite = SceneProxyDesc.ShouldCreateNaniteProxy(&NaniteMaterials);
if (bUseNanite)
{
return ::new Nanite::FSceneProxy(NaniteMaterials, SceneProxyDesc, false/*Instanced*/);
}
return ::new FStaticMeshSceneProxy(SceneProxyDesc, false/*StaticLighting*/);
};
UStaticMeshComponent::CreateSceneProxy
を参考にして記述
動作例
比較用サンプルプロジェクト ● PCGFrameworkで配置してUnlinkしたレベル ○ StaticMeshComponentを持ったアクター x10000 ○ ISM未使用なことに注意 ● PrimitiveSceneDescで直接描画するレベル ○ 10000インスタンス
パフォーマンス比較 SMCx10000
パフォーマンス比較 SMCx10000
パフォーマンス比較 直接描画
パフォーマンス比較 直接レンダリング
パフォーマンス比較 StaticMeshComponent x10000 直接描画 ロード&生成 89ms+(AsyncLoading) 37ms+(PostLoad@GT) 2.1ms (AsyncLoading) RegisterComponent 34ms+ AddPrimitive 30ms+ 12.5ms+ メモリ (Actor+Component)x10000 Actor+配置情報
コリジョン 物理界には投入されないので独自にコリジョン界に投 入するか、別にコリジョンアクターを配置して代替す る HLOD 懸案事項 HLODビルダーはStaticMeshComponentを収集する のでカスタムのHLODビルダーを実装して FPrimitiveSceneDescを収集する必要がある ライティング Movable扱い ライトベイク時にも考慮されない InstancedStaticMeshComponent ISMはすでに高度に最適化されているが よりプロジェクト特化なデータ構造などを実現できる 可能性がありそう
まとめ ● PrimitiveSceneDescによる直接描画はコンポーネントの処理を迂回できる ○ 省メモリ化 ○ レベルロード時のオーバーヘッドの軽減 ○ カスタマイズ ● 制限や運用時に注意しなければいけない点が増えるため、どのようなケースでも利用できるわけではな いことに注意 ○ ワークフローの構築にはかなりの実装コストを見積もる必要がある
その他資料 Unreal Fest Seattle 2024 | Livestream 1, Day 1 https://www.youtube.com/watch?v=s1qdbJtjUI0 Optimizing the Game Thread | Unreal Fest 2024 https://www.youtube.com/watch?v=KxREK-DYu70
Thank you! — Epic Games Japan 2024