UE5 USkinnedMeshComponent::PrecachePSOs相关调用流程梳理


最近在游戏开发工作上碰到一个问题, 过场动画中一角色的上半身在切换其某个材质时消失了一两帧, 该问题大多数出现于机器第一次打开游戏包体播放该过场动画时, 故初步结论是由于编译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的从材质索引至材质的映射关系, 实际使用起来并不是十分方便, 配置方式有待后续进行迭代.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注