最近在游戏开发工作上碰到一个问题, 过场动画中一角色的上半身在切换其某个材质时消失了一两帧, 该问题大多数出现于机器第一次打开游戏包体播放该过场动画时, 故初步结论是由于编译PSOs导致的. 经过一番排查, 确认是编译PSOs导致的问题, 故以本文梳理USkinnedMeshComponent::PrecachePSOs相关的调用流程, 方便后续在碰到类似问题时进行复习.
参考材料
1. [UE5.3] PSO Cache&PreCache 源码阅读
关于PSO PreCache的介绍可参考Epic官方文档(PSO预缓存), 本文基于UE5.5.3梳理USkinnedMeshComponent::PrecachePSOs相关的调用流程.
1. 过场动画中的调用起点
调用起点主要有三个, 分别为USkinnedMeshComponent::PostLoad, USkinnedMeshComponent::OnRegister与UMeshComponent::SetMaterial.
2. USkinnedMeshComponent::PrecachePSOs
没有override父类的虚函数PrecachePSOs, 故本质上调用的即为UPrimitiveComponent::PrecachePSOs.
void UPrimitiveComponent::PrecachePSOs()
{
#if UE_WITH_PSO_PRECACHING
// Only request PSO precaching if app is rendering and per component PSO precaching is enabled
// Also only request PSOs from game thread because TStrongObjectPtr is used on the material to make
// it's not deleted via garbage collection when PSO precaching is still busy. TStrongObjectPtr can only
// be constructed on the GameThread
if (!FApp::CanEverRender() || !IsComponentPSOPrecachingEnabled() || !IsInGameThread())
{
return;
}
// clear the current request data
MaterialPSOPrecacheRequestIDs.Empty();
PSOPrecacheRequestPriority = EPSOPrecachePriority::Medium;
// Collect the data from the derived classes
FPSOPrecacheParams PSOPrecacheParams;
SetupPrecachePSOParams(PSOPrecacheParams);
FMaterialInterfacePSOPrecacheParamsList PSOPrecacheDataArray;
CollectPSOPrecacheData(PSOPrecacheParams, PSOPrecacheDataArray);
FGraphEventArray GraphEvents;
PrecacheMaterialPSOs(PSOPrecacheDataArray, MaterialPSOPrecacheRequestIDs, GraphEvents);
RequestRecreateRenderStateWhenPSOPrecacheFinished(GraphEvents);
#endif
}
主要分为4步.
2.1 SetupPrecachePSOParams
void UPrimitiveComponent::SetupPrecachePSOParams(FPSOPrecacheParams& Params)
{
Params.bRenderInMainPass = bRenderInMainPass;
Params.bRenderInDepthPass = bRenderInDepthPass;
Params.bStaticLighting = HasStaticLighting();
Params.bUsesIndirectLightingCache = Params.bStaticLighting && IndirectLightingCacheQuality != ILCQ_Off && (!IsPrecomputedLightingValid() || GetLightmapType() == ELightmapType::ForceVolumetric);
Params.bAffectDynamicIndirectLighting = bAffectDynamicIndirectLighting;
Params.bCastShadow = CastShadow;
// Custom depth can be toggled at runtime with PSO precache call so assume it might be needed when depth pass is needed
// Ideally precache those with lower priority and don't wait on these (UE-174426)
Params.bRenderCustomDepth = bRenderInDepthPass;
Params.bCastShadowAsTwoSided = bCastShadowAsTwoSided;
Params.SetMobility(Mobility);
Params.SetStencilWriteMask(FRendererStencilMaskEvaluation::ToStencilMask(CustomDepthStencilWriteMask));
TArray UsedMaterials;
GetUsedMaterials(UsedMaterials);
for (const UMaterialInterface* MaterialInterface : UsedMaterials)
{
if (MaterialInterface)
{
if (MaterialInterface->GetRelevance_Concurrent(GMaxRHIFeatureLevel).bUsesWorldPositionOffset)
{
Params.bAnyMaterialHasWorldPositionOffset = true;
break;
}
}
}
}
关键点在于通过GetUsedMaterials获取使用的所有材质(包括Overlay材质), 若发现含有使用WPO的材质, 则将Params的属性bAnyMaterialHasWorldPositionOffset值置为true.
2.2 CollectPSOPrecacheData
void USkinnedMeshComponent::CollectPSOPrecacheData(const FPSOPrecacheParams& BasePrecachePSOParams, FMaterialInterfacePSOPrecacheParamsList& OutParams)
{
if (GetSkinnedAsset() == nullptr ||
GetSkinnedAsset()->GetResourceForRendering() == nullptr)
{
return;
}
// TODO: Nanite-Skinning
ERHIFeatureLevel::Type FeatureLevel = GetWorld() ? GetWorld()->GetFeatureLevel() : GMaxRHIFeatureLevel;
int32 MinLODIndex = ComputeMinLOD();
bool bCPUSkin = bRenderStatic || ShouldCPUSkin();
FPSOPrecacheVertexFactoryDataPerMaterialIndexList VFsPerMaterials = GetSkinnedAsset()->GetVertexFactoryTypesPerMaterialIndex(this, MinLODIndex, bCPUSkin, FeatureLevel);
bool bAnySectionCastsShadows = GetSkinnedAsset()->GetResourceForRendering()->AnyRenderSectionCastsShadows(MinLODIndex);
FPSOPrecacheParams PrecachePSOParams = BasePrecachePSOParams;
PrecachePSOParams.bCastShadow = PrecachePSOParams.bCastShadow && bAnySectionCastsShadows;
// Skinned assets shouldn't need dynamic indirect lighting but MDCs for LumenCardCapture can still be setup and created (but not actually used) causing PSO precache misses
//PrecachePSOParams.bAffectDynamicIndirectLighting = false;
for (FPSOPrecacheVertexFactoryDataPerMaterialIndex& VFsPerMaterial : VFsPerMaterials)
{
UMaterialInterface* MaterialInterface = GetMaterial(VFsPerMaterial.MaterialIndex);
if (MaterialInterface == nullptr)
{
MaterialInterface = UMaterial::GetDefaultMaterial(MD_Surface);
}
FMaterialInterfacePSOPrecacheParams& ComponentParams = OutParams[OutParams.AddDefaulted()];
ComponentParams.MaterialInterface = MaterialInterface;
ComponentParams.VertexFactoryDataList = VFsPerMaterial.VertexFactoryDataList;
ComponentParams.PSOPrecacheParams = PrecachePSOParams;
}
UMaterialInterface* OverlayMaterialInterface = GetOverlayMaterial();
if (OverlayMaterialInterface && VFsPerMaterials.Num() != 0)
{
// Overlay is rendered with the same set of VFs
FMaterialInterfacePSOPrecacheParams& ComponentParams = OutParams[OutParams.AddDefaulted()];
ComponentParams.MaterialInterface = OverlayMaterialInterface;
ComponentParams.VertexFactoryDataList = VFsPerMaterials[0].VertexFactoryDataList;
ComponentParams.PSOPrecacheParams = PrecachePSOParams;
ComponentParams.PSOPrecacheParams.bCastShadow = false;
}
}
USkinnedMeshComponent override父类UPrimitiveComponent的虚函数CollectPSOPrecacheData, 将所有材质的PSO Precache参数收集至FMaterialInterfacePSOPrecacheParamsList& OutParams中.
2.3 PrecacheMaterialPSOs
void PrecacheMaterialPSOs(const FMaterialInterfacePSOPrecacheParamsList& PSOPrecacheParamsList, TArray& OutMaterialPSOPrecacheRequestIDs, FGraphEventArray& OutGraphEvents) { for (const FMaterialInterfacePSOPrecacheParams& MaterialPSOPrecacheParams : PSOPrecacheParamsList) { if (MaterialPSOPrecacheParams.MaterialInterface) { OutGraphEvents.Append(MaterialPSOPrecacheParams.MaterialInterface->PrecachePSOs(MaterialPSOPrecacheParams.VertexFactoryDataList, MaterialPSOPrecacheParams.PSOPrecacheParams, MaterialPSOPrecacheParams.Priority, OutMaterialPSOPrecacheRequestIDs)); } } }
私以为这是最为重要的一步: 为每个材质尝试创建Precache PSOs的Graph Events, 在下一小节中会详细介绍该流程.
2.4 RequestRecreateRenderStateWhenPSOPrecacheFinished
void UPrimitiveComponent::RequestRecreateRenderStateWhenPSOPrecacheFinished(const FGraphEventArray& PSOPrecacheCompileEvents)
{
#if UE_WITH_PSO_PRECACHING
// If the proxy creation strategy relies on knowing when the precached PSO has been compiled,
// schedule a task to mark the render state dirty when all PSOs are compiled so the proxy gets recreated.
if (UsePSOPrecacheRenderProxyDelay() && GetPSOPrecacheProxyCreationStrategy() != EPSOPrecacheProxyCreationStrategy::AlwaysCreate)
{
LatestPSOPrecacheJobSet++;
if(!PSOPrecacheCompileEvents.IsEmpty())
{
TGraphTask::CreateTask(&PSOPrecacheCompileEvents).ConstructAndDispatchWhenReady(this, LatestPSOPrecacheJobSet);
}
else
{
// No graph events to wait on, the job set can be considered complete.
LatestPSOPrecacheJobSetCompleted = LatestPSOPrecacheJobSet;
}
}
bPSOPrecacheCalled = true;
#endif //UE_WITH_PSO_PRECACHING
}
创建一类型为FPSOPrecacheFinishedTask的Graph Task, 将上述步骤创建得到的类型为FGraphEventArray的PSOPrecacheCompileEvents作为其依赖的Graph Tasks, 并放入Task Graph中等待执行. 其中, FPSOPrecacheFinishedTask::DoTask主要是为关联的UPrimitiveComponent对象调用MarkRenderStateDirty函数.
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
if (UPrimitiveComponent* PC = WeakPrimitiveComponent.Get())
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_PSOPrecacheFinishedTask);
int32 CurrJobSetCompleted = PC->LatestPSOPrecacheJobSetCompleted.load();
while (CurrJobSetCompleted < JobSetThatJustCompleted && !PC->LatestPSOPrecacheJobSetCompleted.compare_exchange_weak(CurrJobSetCompleted, JobSetThatJustCompleted)){}
PC->MarkRenderStateDirty();
}
}
3. PrecacheMaterialPSOs
在执行2.3小节所示的代码时, 会对PSOPrecacheParamsList中每一个MaterialPSOPrecacheParams的MaterialInterface调用其PrecachePSOs函数; 当该MaterialInterface为一UMaterialInstance对象时, 将调用其UMaterialInstance::PrecachePSOs函数.
3.1 UMaterialInstance::PrecachePSOs
FGraphEventArray UMaterialInstance::PrecachePSOs(const FPSOPrecacheVertexFactoryDataList& VertexFactoryDataList, const FPSOPrecacheParams& InPreCacheParams, EPSOPrecachePriority Priority, TArray& OutMaterialPSORequestIDs) { FGraphEventArray GraphEvents; if (FApp::CanEverRender() && (PipelineStateCache::IsPSOPrecachingEnabled() || IsPSOShaderPreloadingEnabled()) && Parent) { // Make sure material is initialized. ConditionalPostLoad(); if (bHasStaticPermutationResource) { EMaterialQualityLevel::Type ActiveQualityLevel = GetCachedScalabilityCVars().MaterialQualityLevel; uint32 FeatureLevelsToCompile = GetFeatureLevelsToCompileForRendering(); while (FeatureLevelsToCompile != 0) { const ERHIFeatureLevel::Type FeatureLevel = (ERHIFeatureLevel::Type)FBitSet::GetAndClearNextBit(FeatureLevelsToCompile); FMaterialResource* StaticPermutationResource = FindMaterialResource(StaticPermutationMaterialResources, FeatureLevel, ActiveQualityLevel, true/*bAllowDefaultMaterial*/); if (StaticPermutationResource) { GraphEvents.Append(StaticPermutationResource->CollectPSOs(FeatureLevel, VertexFactoryDataList, InPreCacheParams, Priority, OutMaterialPSORequestIDs)); } } } else { GraphEvents = Parent->PrecachePSOs(VertexFactoryDataList, InPreCacheParams, Priority, OutMaterialPSORequestIDs); } } return GraphEvents; }
无论当前UMaterialInstance对象是否包含静态变体, 最终均会调用FMaterial::CollectPSOs.
3.2 FMaterial::CollectPSOs
FGraphEventArray FMaterial::CollectPSOs(ERHIFeatureLevel::Type InFeatureLevel, const FPSOPrecacheVertexFactoryDataList& VertexFactoryDataList, const FPSOPrecacheParams& PreCacheParams, EPSOPrecachePriority Priority, TArray& OutMaterialPSORequestIDs) { TRACE_CPUPROFILER_EVENT_SCOPE(FMaterial::CollectPSOs); FGraphEventArray GraphEvents; if (GameThreadShaderMap == nullptr) { return GraphEvents; } for (const FPSOPrecacheVertexFactoryData& VFData : VertexFactoryDataList) { if (!VFData.VertexFactoryType->SupportsPSOPrecaching()) { continue; } FMaterialPSOPrecacheParams Params; Params.FeatureLevel = FeatureLevel; Params.Material = this; Params.VertexFactoryData = VFData; Params.PrecachePSOParams = PreCacheParams; FMaterialPSOPrecacheRequestID RequestID = PrecacheMaterialPSOs(Params, Priority, GraphEvents); if (RequestID != INDEX_NONE) { OutMaterialPSORequestIDs.AddUnique(RequestID); // Verified in game thread above FScopeLock ScopeLock(&PrecachedPSORequestIDsCS); PrecachedPSORequestIDs.AddUnique(RequestID); } } return GraphEvents; }
对于VertexFactoryDataList(该数组长度一般为1) 中每一份类型为FPSOPrecacheVertexFactoryData的数据, 均会通过调用PrecacheMaterialPSOs函数得到类型为FMaterialPSOPrecacheRequestID的RequestID, 实质上是调用GMaterialPSORequestManager的PreloadShaderMap函数.
3.3 FMaterialPSORequestManager::PrecachePSOs
FMaterialPSOPrecacheRequestID PrecachePSOs(const FMaterialPSOPrecacheParams& Params, EPSOPrecachePriority Priority, FGraphEventArray& OutGraphEvents)
{
FMaterialPSOPrecacheRequestID RequestID = INDEX_NONE;
if (GetPSOPrecacheMode() == EPSOPrecacheMode::PreloadShader)
{
PreloadShaders(Params, OutGraphEvents);
}
else
{
// Fast check first with read lock if it's not requested or completely finished already
{
FRWScopeLock ReadLock(RWLock, SLT_ReadOnly);
FPrecacheData* FindResult = MaterialPSORequestData.Find(Params);
if (FindResult != nullptr && FindResult->State == EState::Completed)
{
return RequestID;
}
}
// Offload to background job task graph if threading is enabled
// Don't use background thread in editor because shader maps and material resources could be destroyed while the task is running
// If it's a perf problem at some point then FMaterialPSOPrecacheRequestID has to be used at material level in the correct places to wait for
bool bUseBackgroundTask = GPSOUseBackgroundThreadForCollection && FApp::ShouldUseThreadingForPerformance() && !GIsEditor;
FGraphEventRef CollectionGraphEvent;
// Now try and add with write lock
{
FRWScopeLock WriteLock(RWLock, SLT_Write);
FPrecacheData* FindResult = MaterialPSORequestData.Find(Params);
if (FindResult != nullptr)
{
// Update the list of compiling PSOs and update the internal state
bool bBoostPriority = (Priority == EPSOPrecachePriority::High && FindResult->Priority != Priority);
CheckCompilingPSOs(*FindResult, bBoostPriority);
if (FindResult->State != EState::Completed)
{
// If there is a collection graph event than task is used for collection and PSO compiles
// The collection graph event is extended until all PSOs are compiled and caller only has to wait
// for this event to finish
if (FindResult->CollectionGraphEvent)
{
OutGraphEvents.Add(FindResult->CollectionGraphEvent);
}
else
{
for (FPSOPrecacheRequestResult& Result : FindResult->ActivePSOPrecacheRequests)
{
OutGraphEvents.Add(Result.AsyncCompileEvent);
}
}
RequestID = FindResult->RequestID;
}
return RequestID;
}
else
{
// Add to array to get the new RequestID
RequestID = MaterialPSORequests.Add(Params);
// Add data to map
FPrecacheData PrecacheData;
PrecacheData.State = EState::Collecting;
PrecacheData.RequestID = RequestID;
PrecacheData.Priority = Priority;
if (bUseBackgroundTask)
{
CollectionGraphEvent = FGraphEvent::CreateGraphEvent();
PrecacheData.CollectionGraphEvent = CollectionGraphEvent;
// Create task the clear mark fully complete in the cache when done
uint32 RequestLifecycleID = LifecycleID;
FFunctionGraphTask::CreateAndDispatchWhenReady(
[this, Params, RequestLifecycleID]
{
MarkCompilationComplete(Params, RequestLifecycleID);
},
TStatId{}, CollectionGraphEvent
);
}
MaterialPSORequestData.Add(Params, PrecacheData);
}
}
if (bUseBackgroundTask)
{
// Make sure the material instance isn't garbage collected or destroyed yet (create TStrongObjectPtr which will be destroyed on the GT when the collection is done)
TStrongObjectPtr* MaterialInterface = new TStrongObjectPtr(Params.Material->GetMaterialInterface());
FGraphEventArray Prereqs;
// Create and kick off the PSO collection task.
TGraphTask::CreateTask(&Prereqs).ConstructAndDispatchWhenReady(MaterialInterface, Params, CollectionGraphEvent, LifecycleID);
// Need to wait for collection task which will be extended during run with the actual async compile events.
OutGraphEvents.Add(CollectionGraphEvent);
}
else
{
// Collect pso data. Note we don't explicitly collect and preload shaders here since we're not using background tasks
// and doing so in separate phases wouldn't benefit anything.
FPSOPrecacheDataArray PSOPrecacheData = Params.Material->GetGameThreadShaderMap()->CollectPSOPrecacheData(Params);
// Start the async compiles
FPSOPrecacheRequestResultArray PrecacheResults = RequestPrecachePSOs(PSOPrecacheData);
// Mark collection complete
MarkCollectionComplete(Params, PSOPrecacheData, PrecacheResults, LifecycleID);
// Add the graph events to wait for
for (FPSOPrecacheRequestResult& Result : PrecacheResults)
{
check(Result.IsValid());
OutGraphEvents.Add(Result.AsyncCompileEvent);
}
}
}
return RequestID;
}
该函数在开头处
FRWScopeLock ReadLock(RWLock, SLT_ReadOnly);
FPrecacheData* FindResult = MaterialPSORequestData.Find(Params);
if (FindResult != nullptr && FindResult->State == EState::Completed)
{
return RequestID;
}
便会根据传入的类型为FMaterialPSOPrecacheParams的Params在MaterialPSORequestData这个巨大的TMap中查找是否已有相同的Precache PSOs请求, 若有且已存在的Precache PSOs请求Task已处于完成状态, 则直接返回对应的RequestID, 防止重复Precache已存在的PSOs; 否则创建对应的类型为FMaterialPSOPrecacheCollectionTask的Graph Task, 并放入Task Graph中等待执行.
void FMaterialPSOPrecacheCollectionTask::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialPSOPrecacheCollectionTask);
#if WITH_ODSC
FODSCSuspendForceRecompileScope ODSCSuspendForceRecompileScope;
#endif
// Make sure task is still relevant
if (RequestLifecycleID != GMaterialPSORequestManager.GetLifecycleID())
{
CollectionGraphEvent->DispatchSubsequents();
MaterialInterface->Reset();
return;
}
FTaskTagScope ParallelGTScope(ETaskTag::EParallelGameThread);
// Collect pso data
FPSOPrecacheDataArray PSOPrecacheData;
if (PrecacheParams.Material->GetGameThreadShaderMap())
{
PSOPrecacheData = PrecacheParams.Material->GetGameThreadShaderMap()->CollectPSOPrecacheData(PrecacheParams);
}
// Start the async compiles
FPSOPrecacheRequestResultArray PrecacheResults = RequestPrecachePSOs(PSOPrecacheData);
// Mark collection complete
GMaterialPSORequestManager.MarkCollectionComplete(PrecacheParams, PSOPrecacheData, PrecacheResults, RequestLifecycleID);
// Won't touch the material interface anymore - PSO compile jobs take refs to all RHI resources while creating the task
MaterialInterface->Reset();
// Extend MyCompletionGraphEvent to wait for all the async compile events
if (PrecacheResults.Num() > 0)
{
for (FPSOPrecacheRequestResult& Result : PrecacheResults)
{
check(Result.IsValid());
CollectionGraphEvent->DontCompleteUntil(Result.AsyncCompileEvent);
}
}
CollectionGraphEvent->DispatchSubsequents();
}
4. USkinnedMeshComponent::PrecachePSOs相关调用流程示意图

5. 相关Bug
5.1 Bug背景
过场动画中一角色的上半身在切换其某个材质时消失了一两帧, 该问题大多数出现于机器第一次打开游戏包体播放该过场动画时, 故初步结论是由于编译PSOs导致的.
5.2 定位流程
将Engine\Source\Runtime\Engine\Private\PSOPrecacheValidation.cpp中CVarPSOPrecachingBreakOnMaterialName的Flags修改为ECVF_Default, 方便运行时调试. 同时, 我们需要在游戏的启动参数中加入-clearPSODriverCache, 确保测试过程中不会受到缓存PSOs的影响. 启动游戏后, 通过在控制台中输入r.PSOPrecache.BreakOnMaterialName为变量GPSOPrecachingBreakOnMaterialName赋予一个非空值, 如此一来, UE便会自动在函数ConditionalBreakOnPSOPrecacheMaterial内打上断点, 从而每次编译材质相关的PSOs时, 我们可以通过断点查询到对应的材质信息(包括材质名等).
void ConditionalBreakOnPSOPrecacheMaterial(const FMaterial& Material, int32 PSOCollectorIndex)
{
if (!GPSOPrecachingBreakOnMaterialName.IsEmpty())
{
int PSOCollectorIndexToDebug = FPSOCollectorCreateManager::GetIndex(GetFeatureLevelShadingPath(GMaxRHIFeatureLevel), *GPSOPrecachingBreakOnPassName);
FString MaterialName = Material.GetAssetName();
// Support debugging specific materials and their passes, or a specific shader hash at runtime.
if (MaterialName.Contains(GPSOPrecachingBreakOnMaterialName) && PSOCollectorIndex == PSOCollectorIndexToDebug)
{
UE_DEBUG_BREAK();
}
}
}
测试发现, 每次过场动画中角色的上半身消失一两帧时, 都恰好是UE编译材质相关的PSOs的时机, 导火索为编译PSOs的几率大大增加.
5.3 解决方案
目前尚不清楚为何Bundled PSO Cache未生效, 我们可以转而简要讨论一下为何Precache PSOs未生效. 由上述关于USkinnedMeshComponent::PrecachePSOs的相关调用流程的分析可知, 其调用起点主要有三个, 分别为USkinnedMeshComponent::PostLoad, USkinnedMeshComponent::OnRegister与UMeshComponent::SetMaterial. 其中, 在USkinnedMeshComponent::PostLoad或USkinnedMeshComponent::OnRegister中调用USkinnedMeshComponent::PrecachePSOs根本无法Precache即将切换的材质相关的PSOs, 因为这是纯业务相关的, 且在无其它信息的前提下, 对于引擎而言亦是不可预测的; 另一方面, 在UMeshComponent::SetMaterial中再调用USkinnedMeshComponent::PrecachePSOs的时机一般来说又较晚. 如此一来, 我们仅需提供可被业务层提前调用的可缓存指定材质相关的PSOs的接口即可.
void USkinnedMeshComponent::PrecacheMutablePSOs(const TMap& MaterialIndexToMutableMaterialMap, UMaterialInterface* OverlayMaterialInterface) { #if UE_WITH_PSO_PRECACHING // Only request PSO precaching if app is rendering and per component PSO precaching is enabled // Also only request PSOs from game thread because TStrongObjectPtr is used on the material to make // it's not deleted via garbage collection when PSO precaching is still busy. TStrongObjectPtr can only // be constructed on the GameThread if (!FApp::CanEverRender() || !IsComponentPSOPrecachingEnabled() || !IsInGameThread()) { return; } PSOPrecacheRequestPriority = EPSOPrecachePriority::High; // Collect the data from the derived classes FPSOPrecacheParams PSOPrecacheParams; PSOPrecacheParams.bRenderInMainPass = bRenderInMainPass; PSOPrecacheParams.bRenderInDepthPass = bRenderInDepthPass; PSOPrecacheParams.bStaticLighting = HasStaticLighting(); PSOPrecacheParams.bUsesIndirectLightingCache = PSOPrecacheParams.bStaticLighting && IndirectLightingCacheQuality != ILCQ_Off && (!IsPrecomputedLightingValid() || GetLightmapType() == ELightmapType::ForceVolumetric); PSOPrecacheParams.bAffectDynamicIndirectLighting = bAffectDynamicIndirectLighting; PSOPrecacheParams.bCastShadow = CastShadow; // Custom depth can be toggled at runtime with PSO precache call so assume it might be needed when depth pass is needed // Ideally precache those with lower priority and don't wait on these (UE-174426) PSOPrecacheParams.bRenderCustomDepth = bRenderInDepthPass; PSOPrecacheParams.bCastShadowAsTwoSided = bCastShadowAsTwoSided; PSOPrecacheParams.SetMobility(Mobility); PSOPrecacheParams.SetStencilWriteMask(FRendererStencilMaskEvaluation::ToStencilMask(CustomDepthStencilWriteMask)); TArray UsedMaterials; GetUsedMaterials(UsedMaterials); if (OverlayMaterialInterface != nullptr) { UsedMaterials.Emplace(OverlayMaterialInterface); } for (int32 UsedMaterialIndex = 0; UsedMaterialIndex < UsedMaterials.Num(); ++UsedMaterialIndex) { UMaterialInterface* MaterialInterface = nullptr; const auto& MutableMaterialPtr = MaterialIndexToMutableMaterialMap.Find(UsedMaterialIndex); MaterialInterface = (MutableMaterialPtr != nullptr ? *MutableMaterialPtr : UsedMaterials[UsedMaterialIndex]); if (MaterialInterface) { if (MaterialInterface->GetRelevance_Concurrent(GMaxRHIFeatureLevel).bUsesWorldPositionOffset) { PSOPrecacheParams.bAnyMaterialHasWorldPositionOffset = true; break; } } } FMaterialInterfacePSOPrecacheParamsList PSOPrecacheDataArray; // CollectPSOPrecacheData USkinnedAsset* LocalSkinnedAsset = GetSkinnedAsset(); if (LocalSkinnedAsset != nullptr && LocalSkinnedAsset->GetResourceForRendering() != nullptr) { // TODO: Nanite-Skinning ERHIFeatureLevel::Type FeatureLevel = GetWorld() ? GetWorld()->GetFeatureLevel() : GMaxRHIFeatureLevel; int32 MinLODIndex = ComputeMinLOD(); bool bCPUSkin = bRenderStatic || ShouldCPUSkin(); FPSOPrecacheVertexFactoryDataPerMaterialIndexList VFsPerMaterials = LocalSkinnedAsset->GetVertexFactoryTypesPerMaterialIndex(this, MinLODIndex, bCPUSkin, FeatureLevel); bool bAnySectionCastsShadows = LocalSkinnedAsset->GetResourceForRendering()->AnyRenderSectionCastsShadows(MinLODIndex); FPSOPrecacheParams PrecachePSOParams = PSOPrecacheParams; PrecachePSOParams.bCastShadow = PrecachePSOParams.bCastShadow && bAnySectionCastsShadows; // Skinned assets shouldn't need dynamic indirect lighting but MDCs for LumenCardCapture can still be setup and created (but not actually used) causing PSO precache misses //PrecachePSOParams.bAffectDynamicIndirectLighting = false; int32 VFsPerMaterialsNum = VFsPerMaterials.Num(); if (VFsPerMaterialsNum != 0) { for (const auto& [MaterialIndex, MaterialInterface] : MaterialIndexToMutableMaterialMap) { if (MaterialIndex >= VFsPerMaterialsNum || MaterialInterface == nullptr) { continue; } FMaterialInterfacePSOPrecacheParams& ComponentParams = PSOPrecacheDataArray[PSOPrecacheDataArray.AddDefaulted()]; ComponentParams.MaterialInterface = MaterialInterface; ComponentParams.VertexFactoryDataList = VFsPerMaterials[MaterialIndex].VertexFactoryDataList; ComponentParams.PSOPrecacheParams = PrecachePSOParams; } if (OverlayMaterialInterface != nullptr) { // Overlay is rendered with the same set of VFs FMaterialInterfacePSOPrecacheParams& ComponentParams = PSOPrecacheDataArray[PSOPrecacheDataArray.AddDefaulted()]; ComponentParams.MaterialInterface = OverlayMaterialInterface; ComponentParams.VertexFactoryDataList = VFsPerMaterials[0].VertexFactoryDataList; ComponentParams.PSOPrecacheParams = PrecachePSOParams; ComponentParams.PSOPrecacheParams.bCastShadow = false; } } } // PrecacheMaterialPSOs FGraphEventArray GraphEvents; for (const FMaterialInterfacePSOPrecacheParams& MaterialPSOPrecacheParams : PSOPrecacheDataArray) { if (MaterialPSOPrecacheParams.MaterialInterface) { GraphEvents.Append(MaterialPSOPrecacheParams.MaterialInterface->PrecachePSOs(MaterialPSOPrecacheParams.VertexFactoryDataList, MaterialPSOPrecacheParams.PSOPrecacheParams, MaterialPSOPrecacheParams.Priority, MaterialPSOPrecacheRequestIDs)); } } RequestRecreateRenderStateWhenPSOPrecacheFinished(GraphEvents); #endif }
5.4 测试结果
在过场动画中角色切换材质前提前调用上述函数, 角色切换材质时未在函数ConditionalBreakOnPSOPrecacheMaterial内触发断点, 表明切换材质相关的PSOs已成功Precache. 与此同时, 过场动画中角色切换材质也未再出现消失一两帧的问题.
5.5 Todo
由新增函数USkinnedMeshComponent::PrecacheMutablePSOs的传入参数可知, 调用者需要提供一个类型为TMap