UE4.27.2 着色器的延迟编译的调试分析
2025-05-20 22:58:09

发起编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Compiles the shaders for a material and caches them in this shader map.
* @param Material - The material to compile shaders for.
* @param InShaderMapId - the set of static parameters to compile for
* @param Platform - The platform to compile to
*/
void FMaterialShaderMap::Compile(
FMaterial* Material,
const FMaterialShaderMapId& InShaderMapId,
const TRefCountPtr<FSharedShaderCompilerEnvironment>& MaterialEnvironment,
const FMaterialCompilationOutput& InMaterialCompilationOutput,
EShaderPlatform InPlatform,
EMaterialShaderPrecompileMode PrecompileMode)
{
...

if (PrecompileMode != EMaterialShaderPrecompileMode::None)
{
EShaderCompileJobPriority CompilePriority = EShaderCompileJobPriority::Low;

...

SubmitCompileJobs(CompilingId, Material, MaterialEnvironment, CompilePriority);
}

// Compile the shaders for this shader map now if the material is not deferring and deferred compiles are not enabled globally
if (PrecompileMode == EMaterialShaderPrecompileMode::Synchronous)
{
TArray<int32> CurrentShaderMapId;
CurrentShaderMapId.Add(CompilingId);
GShaderCompilingManager->FinishCompilation(
GetFriendlyName(),
CurrentShaderMapId);
}
}

简单的判断逻辑。一开始发起一个异步 job。如果当前模式为同步模式,那么发起之后立即阻塞,直到编译结束。

FShaderCompilingManager::FinishCompilation 是实现阻塞编译的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void FShaderCompilingManager::FinishCompilation(const TCHAR* MaterialName, const TArray<int32>& ShaderMapIdsToFinishCompiling)
{
...

TMap<int32, FShaderMapFinalizeResults> CompiledShaderMaps;
CompiledShaderMaps.Append( PendingFinalizeShaderMaps );
PendingFinalizeShaderMaps.Empty();
BlockOnShaderMapCompletion(ShaderMapIdsToFinishCompiling, CompiledShaderMaps);

bool bRetry = false;
do
{
bRetry = HandlePotentialRetryOnError(CompiledShaderMaps);
}
while (bRetry);

ProcessCompiledShaderMaps(CompiledShaderMaps, FLT_MAX);

...
}

FShaderCompilingManager::BlockOnShaderMapCompletion 等待编译完成,然后把编译完成的结果,插入到一个列表。这个列表的初始状态是已经完成但还未处理的 ShaderMap 编译结果 PendingFinalizeShaderMaps

当前编译完成的,和之前已经编译完成的,一起做最终的处理 Finalize

可见,这里暗示了,编译之后的 shader 还需要处理

完成编译的时候,着色器是怎么替换的?应该这里的最终处理就是实现这个事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void FShaderCompilingManager::ProcessCompiledShaderMaps(
TMap<int32, FShaderMapFinalizeResults>& CompiledShaderMaps,
float TimeBudget)
{
#if WITH_EDITOR
TMap<TRefCountPtr<FMaterial>, TRefCountPtr<FMaterialShaderMap>> MaterialsToUpdate;

... // 判断、获取那些编译成功的 shader 对应的材质

if (MaterialsToUpdate.Num() > 0)
{
FMaterial::SetShaderMapsOnMaterialResources(MaterialsToUpdate);

for (const auto& It : MaterialsToUpdate)
{
It.Key->NotifyCompilationFinished();
}

if (FApp::CanEverRender())
{
PropagateMaterialChangesToPrimitives(MaterialsToUpdate);

FEditorSupportDelegates::RedrawAllViewports.Broadcast();
}
}

...
#endif // WITH_EDITOR
}

可见,它是对那些编译成功的 shader 进行处理,找出这些 shader 对应的 material,把这些 material 的更新应用到图元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void FShaderCompilingManager::PropagateMaterialChangesToPrimitives(const TMap<TRefCountPtr<FMaterial>, TRefCountPtr<FMaterialShaderMap>>& MaterialsToUpdate)
{
TArray<UMaterialInterface*> UsedMaterials;
TIndirectArray<FComponentRecreateRenderStateContext> ComponentContexts;

for (TObjectIterator<UPrimitiveComponent> PrimitiveIt; PrimitiveIt; ++PrimitiveIt)
{
UPrimitiveComponent* PrimitiveComponent = *PrimitiveIt;

if (PrimitiveComponent->IsRenderStateCreated())
{
UsedMaterials.Reset();
bool bPrimitiveIsDependentOnMaterial = false;

// Note: relying on GetUsedMaterials to be accurate, or else we won't propagate to the right primitives and the renderer will crash later
// FPrimitiveSceneProxy::VerifyUsedMaterial is used to make sure that all materials used for rendering are reported in GetUsedMaterials
PrimitiveComponent->GetUsedMaterials(UsedMaterials);

if (UsedMaterials.Num() > 0)
{
... // 判断当前 PrimitiveComponent 是否依赖于 MaterialsToUpdate 中的 Material

if (bPrimitiveIsDependentOnMaterial)
{
ComponentContexts.Add(new FComponentRecreateRenderStateContext(PrimitiveComponent));

...
}
}
}
}

ComponentContexts.Empty();
}

这里是对所有需要更新材质的 PrimitiveComponent 建一个 FComponentRecreateRenderStateContext 放到列表

单独看这个函数的话,一个简单的猜测是构造 FComponentRecreateRenderStateContext 的时候不做材质更新,结束的时候清空列表,统一析构,析构的时候调用材质更新逻辑

实际进去这个类里面看会发现,它是构造和析构的时候都调用 UpdateAllPrimitiveSceneInfosForSingleComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/** Destroys render state for a component and then recreates it when this object is destroyed */
class FComponentRecreateRenderStateContext
{
private:
/** Pointer to component we are recreating render state for */
UActorComponent* Component;

TSet<FSceneInterface*>* ScenesToUpdateAllPrimitiveSceneInfos;

public:
FComponentRecreateRenderStateContext(UActorComponent* InComponent, TSet<FSceneInterface*>* InScenesToUpdateAllPrimitiveSceneInfos = nullptr)
: ScenesToUpdateAllPrimitiveSceneInfos(InScenesToUpdateAllPrimitiveSceneInfos)
{
check(InComponent);
checkf(!InComponent->IsUnreachable(), TEXT("%s"), *InComponent->GetFullName());

if (InComponent->IsRegistered() && InComponent->IsRenderStateCreated())
{
InComponent->DestroyRenderState_Concurrent();
Component = InComponent;

UpdateAllPrimitiveSceneInfosForSingleComponent(InComponent, ScenesToUpdateAllPrimitiveSceneInfos);
}
else
{
Component = nullptr;
}
}

~FComponentRecreateRenderStateContext()
{
if (Component && !Component->IsRenderStateCreated() && Component->IsRegistered())
{
Component->CreateRenderState_Concurrent(nullptr);

UpdateAllPrimitiveSceneInfosForSingleComponent(Component, ScenesToUpdateAllPrimitiveSceneInfos);
}
}
};

构造时调用

目的是通知渲染线程“我即将重建这个组件的渲染状态”,让渲染线程做好准备(比如暂停使用旧的渲染数据,避免渲染时访问到不一致的数据)。

这相当于告诉渲染线程“这个组件的渲染数据马上要变了,先做一些同步或锁定操作”。

析构时调用

目的是完成渲染状态的重建,将组件的最新材质和渲染数据提交给渲染线程。

这相当于告诉渲染线程“渲染状态已经重建完成,可以开始使用新的数据了”。