UE4.27.2 不透明物体延迟渲染 Draw Call 的调试分析
2025-05-20 22:58:09

ENQUEUE_RENDER_COMMAND

一个最常见的东西

ENQUEUE_RENDER_COMMAND 传进来,就是接受一个 lambda,然后内层

1
2
template<typename TSTR, typename LAMBDA>
FORCEINLINE_DEBUGGABLE void EnqueueUniqueRenderCommand(LAMBDA&& Lambda)

根据当前是渲染线程还是 game 线程来决定如何执行这个 lambda

谁调用了 BasePass

因为截帧的时候看到 BasePass 里面是处理 mesh 的材质

于是打算从这里开始看

Rendering Thread

1
2
3
4
5
6
7
void FDeferredShadingSceneRenderer::RenderBasePassInternal(
FRDGBuilder& GraphBuilder,
const FRenderTargetBindingSlots& BasePassRenderTargets,
FExclusiveDepthStencil::Type BasePassDepthStencilAccess,
FRDGTextureRef ForwardScreenSpaceShadowMask,
bool bParallelBasePass,
bool bRenderLightmapDensity)

打断点,是在 FRunnableThreadWin 触发

于是可以看到,UE 是把 Game 线程和 Render 线程分开的

从堆栈底层往上,看到 void RenderingThreadMain( FEvent* TaskGraphBoundSyncEvent )

这里面除了性能分析插桩、事件,主要部分是,把 TaskGraph 的当前线程设置为 render 线程,然后 TaskGraph 进入任务处理循环,直到 return

于是看到 ENQUEUE_RENDER_COMMAND 的 lambda 最终是加到了 task 里面,处理 task 就是处理 render command

那么 base pass 这个相关的 render command 是 FRendererModule::BeginRenderingViewFamily 里面发起的

FRendererModule::BeginRenderingViewFamily

之前的分析已经可以看到,render thread 的工作就是不停地执行 TaskGraph 内的任务

所以渲染框架的逻辑还是在 game thread

之前看到一个 base pass 相关的 render command 是在 FRendererModule::BeginRenderingViewFamily 里面发起的

于是现在在 FRendererModule::BeginRenderingViewFamily 打断点,看看 game thread 这边的逻辑

从底层上来是 UGameEngine::Tick,这里也就是各个功能的 tick,不出所料,gameobject, slate 等

然后是 FViewport::Draw,这里更多是准备 canvas

UGameViewportClient::Draw 再对 canvas 做一些操作,然后就到了 FRendererModule::BeginRenderingViewFamily

那其实 FSceneViewFamily* ViewFamily 这个东西已经包含了渲染世界所需要的信息了

查看这个变量,确实

alt text

既有 World 又有 Primitives、Lights,已经够了

FRendererModule::BeginRenderingViewFamily 内,有一个 World->SendAllEndOfFrameUpdates(); 更新 component 的状态,然后就是发送 render command

1
2
3
4
5
6
7
8
ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
[SceneRenderer, DrawSceneEnqueue](FRHICommandListImmediate& RHICmdList)
{
...

RenderViewFamily_RenderThread(RHICmdList, SceneRenderer);
FlushPendingDeleteRHIResources_RenderThread();
});

从 game thread 进到 render thread:RenderViewFamily_RenderThread

于是查看 RenderViewFamily_RenderThread

前面是等待所有未完成的渲染任务、更新延迟资源,处理鼠标点击拾取物体的功能,然后是渲染场景

1
SceneRenderer->Render(RHICmdList);

奇怪的是这后面还写了头发的渲染

感觉这个调用层级有点不协调,算了,在看框架的时候这不是重点

然后这个 render 就到了 FDeferredShadingSceneRenderer::Render

FDeferredShadingSceneRenderer::Render

一开始,Scene->UpdateAllPrimitiveSceneInfos 应该是更新 mesh 相关的渲染信息?

然后是视口矩形、天空大气、多 GPU、等待 RT 可写、VT 分配、深度缓冲,暂时略过不看

然后是找到所有的可见物体

1
2
3
4
5
6
7
8
// Find the visible primitives.
RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

bool bDoInitViewAftersPrepass = false;
{
SCOPED_GPU_STAT(RHICmdList, VisibilityCommands);
bDoInitViewAftersPrepass = InitViews(RHICmdList, BasePassDepthStencilAccess, ILCTaskData);
}

然后是有一个 GPU 场景更新 UpdateGPUScene(RHICmdList, *Scene); 不细看的话,暂时不知道是指什么场景

然后是 Pre Z Pass、延迟渲染的 GBuffer 相关的计算、Early occlusion queries、Early Shadow depth rendering、体积云初始化、大气 LUT、用于间接光照的 Light Propagation Volumes、体积雾计算、体积云计算、头发计算、forward 阴影渲染等

然后是 BasePass

1
RenderBasePass(GraphBuilder, BasePassDepthStencilAccess, SceneColorTexture.Target, SceneDepthTexture.Target, DepthLoadAction, ForwardScreenSpaceShadowMaskTexture);

然后是速度矢量渲染(TAA 相关)、毛发渲染 BasePass、天光 RayTracing、Pre-lighting composition lighting stage(用于 SSAO 和延迟贴花等)、用于延迟渲染的毛发渲染 BasePass、自定义纹理的重建(用于 velocity, custom depth, and SSAO)、然后又是一些反射和天空光照渲染、体积云等

然后是渲染半透明物体,这里可以看到半透明物体也是可以渲染速度向量的

然后是后处理 pass,之后应该没有啥了

FDeferredShadingSceneRenderer::RenderBasePass

FDeferredShadingSceneRenderer::Render 发起了很多渲染对象的渲染,不透明,半透明,体积云,体积雾,头发,阴影

最简单的还是看不透明物体是怎么渲染的

于是进入 FDeferredShadingSceneRenderer::RenderBasePass

可以看到,它也只是一层包装。前面是关于如何 clear 纹理,然后进入 impl

1
RenderBasePassInternal(GraphBuilder, BasePassRenderTargets, BasePassDepthStencilAccess, ForwardShadowMaskTexture, bDoParallelBasePass, bRenderLightmapDensity);

进来,显而易见是两个 View.ParallelMeshDrawCommandPasses[EMeshPass::BasePass].DispatchDraw 可能在画不透明物体,因为其他的绘制命令都是 EditorPrimitivesSkyPass,一看就不相关

我调试的时候是进入了

1
2
3
4
5
6
7
8
9
10
GraphBuilder.AddPass(
RDG_EVENT_NAME("BasePassParallel"),
PassParameters,
ERDGPassFlags::Raster | ERDGPassFlags::SkipRenderPass,
[this, &View, PassParameters](FRHICommandListImmediate& RHICmdList)
{
Scene->UniformBuffers.UpdateViewUniformBuffer(View);
FRDGParallelCommandListSet ParallelCommandListSet(RHICmdList, GET_STATID(STAT_CLP_BasePass), *this, View, FParallelCommandListBindings(PassParameters));
View.ParallelMeshDrawCommandPasses[EMeshPass::BasePass].DispatchDraw(&ParallelCommandListSet, RHICmdList);
});

BasePass 内的逻辑

分发绘制 command

于是进到 void FParallelMeshDrawCommandPass::DispatchDraw(FParallelCommandListSet* ParallelCommandListSet, FRHICommandList& RHICmdList) const

这个函数是负责拆分渲染任务,均分到多个线程并行工作

前面是准备上传顶点缓冲到 GPU Scene 然后是均分 task

分配循环内的循环体内的这两句,分配 task,应该会进到下一层,负责实际绘制逻辑把

1
2
3
FGraphEventRef AnyThreadCompletionEvent = TGraphTask<FDrawVisibleMeshCommandsAnyThreadTask>::CreateTask(&Prereqs, RenderThread)
.ConstructAndDispatchWhenReady(*CmdList, TaskContext.MeshDrawCommands, TaskContext.MinimalPipelineStatePassSet, PrimitiveIdsBuffer, BasePrimitiveIdsOffset, TaskContext.bDynamicInstancing, TaskContext.InstanceFactor, TaskIndex, NumTasks);
ParallelCommandListSet->AddParallelCommandList(CmdList, AnyThreadCompletionEvent, NumDraws);

它分配完了 task 之后就退出来了

这个 task 实际执行还是在 render thread ProcessTasksUntilQuit

最终调用到 FDrawVisibleMeshCommandsAnyThreadTaskDoTask,它也只是

1
SubmitMeshDrawCommandsRange(VisibleMeshDrawCommands, GraphicsMinimalPipelineStateSet, PrimitiveIdsBuffer, BasePrimitiveIdsOffset, bDynamicInstancing, StartIndex, NumDraws, InstanceFactor, RHICmdList);

的包装

但是我突然意识到,这里的 VisibleMeshDrawCommands 已经是涉及到了要画什么 mesh 了

可能是我的 mesh 比较少吧,我这一个 FDrawVisibleMeshCommandsAnyThreadTaskVisibleMeshDrawCommands 就已经包含所有要画的 mesh,因为我通过 renderdoc 看到的就是这个数量

SubmitMeshDrawCommandsRange 里面就是遍历这个传入的 VisibleMeshDrawCommands,对每一个 command 提交

1
FMeshDrawCommand::SubmitDraw(*VisibleMeshDrawCommand.MeshDrawCommand, GraphicsMinimalPipelineStateSet, PrimitiveIdsBuffer, PrimitiveIdBufferOffset, InstanceFactor, RHICmdList, StateCache);

发起 draw call

前面看的是怎么分配

现在这个 FMeshDrawCommand::SubmitDraw 就是怎么绘制网格了

前面是获取并设置图形管线状态、设置模板测试参考值、设置顶点流、设置着色器绑定(绑定常量缓冲、纹理、采样器等资源)

然后就是 draw call

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
if (MeshDrawCommand.IndexBuffer)
{
if (MeshDrawCommand.NumPrimitives > 0)
{
RHICmdList.DrawIndexedPrimitive(
MeshDrawCommand.IndexBuffer,
MeshDrawCommand.VertexParams.BaseVertexIndex,
0,
MeshDrawCommand.VertexParams.NumVertices,
MeshDrawCommand.FirstIndex,
MeshDrawCommand.NumPrimitives,
MeshDrawCommand.NumInstances * InstanceFactor
);
}
else
{
RHICmdList.DrawIndexedPrimitiveIndirect(
MeshDrawCommand.IndexBuffer,
MeshDrawCommand.IndirectArgs.Buffer,
MeshDrawCommand.IndirectArgs.Offset
);
}
}
else
{
if (MeshDrawCommand.NumPrimitives > 0)
{
RHICmdList.DrawPrimitive(
MeshDrawCommand.VertexParams.BaseVertexIndex + MeshDrawCommand.FirstIndex,
MeshDrawCommand.NumPrimitives,
MeshDrawCommand.NumInstances * InstanceFactor);
}
else
{
RHICmdList.DrawPrimitiveIndirect(
MeshDrawCommand.IndirectArgs.Buffer,
MeshDrawCommand.IndirectArgs.Offset
);
}

这熟悉的结构

可能别的我都不熟,但是这个有 index buffer 就 draw indexed 否则直接 draw 的 draw call 形式,在我自己写的渲染器都是这样的,感动了

DrawCall Debug

可以看到他 FMeshDrawCommand::SubmitDraw 这里还有 debug 示例

MeshDrawCommand.DebugData.MaterialNameMeshDrawCommand.DebugData.ResourceName 就可以看到 mesh 来源,还有材质名称,方便定位问题

BasePass DrawCall 是如何绑定渲染着色器资源的

现在我有一些自己的着色器资源,有纹理,有 uniform 等等,它们是怎么传进来的?

从 draw call 找着色器绑定,没找到

于是看到 FMeshDrawCommand::SubmitDraw 的绑定着色器相关

1
MeshDrawCommand.ShaderBindings.SetOnCommandList(RHICmdList, MeshPipelineState.BoundShaderState.AsBoundShaderState(), StateCache.ShaderBindings);

进到 FMeshDrawShaderBindings::SetOnCommandList,可以看到他只是根据一个 frequency 变量确认 shader 类型

最终不同的 shader 类型都是要调用 SetShaderBindings,但是参数不同

于是看到 FMeshDrawShaderBindings::SetShaderBindings

一开始看到 Uniform Buffers 绑定

  1. 从 SingleShaderBindings 中获取所有 Uniform Buffer 指针和对应的参数信息。

  2. 遍历所有 Uniform Buffer,检查当前绑定状态是否与缓存不同,避免重复绑定。

  3. 绑定 Uniform Buffer。

  4. 更新缓存状态

这个利用缓存的逻辑之后也是一样的

所以就可以看一下各自的命令了

Uniform Buffers 绑定

1
RHICmdList.SetShaderUniformBuffer(Shader, Parameter.BaseIndex, UniformBuffer);

Sampler 绑定

1
RHICmdList.SetShaderSampler(Shader, Parameter.BaseIndex, Sampler);

SRV(Shader Resource View)绑定

1
RHICmdList.SetShaderResourceViewParameter(Shader, Parameter.BaseIndex, SRV);

Texture 绑定

1
RHICmdList.SetShaderTexture(Shader, Parameter.BaseIndex, Texture);

还有一个 LooseParameter,不知道干啥的,先跳过吧

但是当我对这些绑定的代码打断点的时候,发现代码从来没击中断点

一开始我还以为是,这些绑定只在物体新添加到场景的时候绑定一次,之后就利用缓存了

于是重新启动游戏,发现我的断点从来没有击中过

那就非常神奇了,不知道是谁绑定了纹理?

从 PSSetShaderResources 找纹理绑定,没找到

因为我在 RenderDoc 可以看到它绑定纹理的 API 是 PSSetShaderResources

于是去 UE 源码查他这个绑定的 API

发现他在源码里面的使用都是在 UE 的 UI 库 slate 中才有使用

要不然就是一个 ClearShaderResource 有在使用

这,完全找不到是怎么绑定的……

从着色器资源类找

看别人的博客,发现这方面也有讲述

FShaderParameter 是着色器的寄存器绑定参数, 它的类型可以是float1/2/3/4,数组, UAV等.

FShaderResourceParameter 是着色器资源绑定(纹理或采样器)

FRWShaderParameter 与 UAV or SRV 相关

TShaderUniformBufferParameter 与 uniform 相关

但是我在 FShaderResourceParameter::BindFRWShaderParameter::SetTexture 打断点,都没有命中

后来我开 Editor 发现可以命中,但是命中的都是 uniform shader parameter,堆栈里面显示的是 editor primitives 的绘制

感觉这并不是 Editor 或者 Game 的构建配置的问题,纯粹是 Editor 有一些特殊调用而已

这种异常看上去是编译器优化了什么东西

于是使用 PRAGMA_DISABLE_OPTIMIZATION PRAGMA_ENABLE_OPTIMIZATION 但是还是没有效果

从 SetShaderParameters 找

看别人博客 https://logins.github.io/graphics/2021/03/31/UE4ShadersIntroduction.html,他说

1
2
template<typename TRHICmdList, typename TShaderClass, typename TShaderRHI>
inline void SetShaderParameters(TRHICmdList& RHICmdList, const TShaderRef<TShaderClass>& Shader, TShaderRHI* ShadeRHI, const typename TShaderClass::FParameters& Parameters)

是最常用的绑定函数

看了一下,确实有很多绑定

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
for (const FShaderParameterBindings::FResourceParameter& ParameterBinding : Bindings.ResourceParameters)
{
EUniformBufferBaseType BaseType = (EUniformBufferBaseType)ParameterBinding.BaseType;
switch (BaseType)
{
case UBMT_TEXTURE:
{
auto ShaderParameterRef = *(FRHITexture**)(Base + ParameterBinding.ByteOffset);
RTBindingsWriter.SetTexture(ParameterBinding.BaseIndex, ShaderParameterRef);
}
break;
case UBMT_SRV:
{
FRHIShaderResourceView* ShaderParameterRef = *(FRHIShaderResourceView**)(Base + ParameterBinding.ByteOffset);
RTBindingsWriter.SetSRV(ParameterBinding.BaseIndex, ShaderParameterRef);
}
break;
case UBMT_UAV:
{
FRHIUnorderedAccessView* ShaderParameterRef = *(FRHIUnorderedAccessView**)(Base + ParameterBinding.ByteOffset);
RTBindingsWriter.SetUAV(ParameterBinding.BaseIndex, ShaderParameterRef);
}
break;
case UBMT_SAMPLER:
{
FRHISamplerState* ShaderParameterRef = *(FRHISamplerState**)(Base + ParameterBinding.ByteOffset);
RTBindingsWriter.SetSampler(ParameterBinding.BaseIndex, ShaderParameterRef);
}
break;
case UBMT_RDG_TEXTURE:
{
auto GraphTexture = *reinterpret_cast<FRDGTexture* const*>(Base + ParameterBinding.ByteOffset);
checkSlow(GraphTexture);
GraphTexture->MarkResourceAsUsed();
RTBindingsWriter.SetTexture(ParameterBinding.BaseIndex, GraphTexture->GetRHI());
}
break;
case UBMT_RDG_TEXTURE_SRV:
case UBMT_RDG_BUFFER_SRV:
{
auto GraphSRV = *reinterpret_cast<FRDGShaderResourceView* const*>(Base + ParameterBinding.ByteOffset);

checkSlow(GraphSRV);
GraphSRV->MarkResourceAsUsed();
RTBindingsWriter.SetSRV(ParameterBinding.BaseIndex, GraphSRV->GetRHI());
}
break;
case UBMT_RDG_TEXTURE_UAV:
case UBMT_RDG_BUFFER_UAV:
{
auto UAV = *reinterpret_cast<FRDGUnorderedAccessView* const*>(Base + ParameterBinding.ByteOffset);

checkSlow(UAV);
UAV->MarkResourceAsUsed();
RTBindingsWriter.SetUAV(ParameterBinding.BaseIndex, UAV->GetRHI());
}
break;
default:
checkf(false, TEXT("Unhandled resource type?"));
break;
}
}

但是我查找了一下它的引用,怎么都是 RayTracing 在用?没有别人在用了。

打断点,发现还是有一个 UpdateGPUScene(RHICmdList, *Scene); 在用,最终到 FComputeShaderUtils::Dispatch。但是似乎和 Base Pass 怎么绑定 mesh 的没有关系

在 FShaderResourceParameter 加 Log

1
2
3
void FShaderResourceParameter::Bind(const FShaderParameterMap& ParameterMap,const TCHAR* ParameterName,EShaderParameterFlags Flags)
{
UE_LOG(LogTemp, Warning, TEXT("FShaderResourceParameter::Bind here!!!!!"));

也没有输出

重新看一下渲染流程

看了

https://github.com/donaldwuid/unreal_source_explained/blob/master/main/rendering.md

也没有解决我的问题,就是纹理是从哪里加载过来的

从 UTexture2D 出发

那些地方都打不到断点,于是从 UTexture2D 出发打断点

研究了一下,觉得 UTexture::SetResource 很像是跟渲染资源相关的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void UTexture::SetResource(FTextureResource* InResource)
{
check (!IsInActualRenderingThread() && !IsInRHIThread());

// Each PrivateResource value must be updated in it's own thread because any
// rendering code trying to access the Resource from this UTexture will
// crash if it suddenly sees nullptr or a new resource that has not had it's InitRHI called.

PrivateResource = InResource;
ENQUEUE_RENDER_COMMAND(SetResourceRenderThread)([this, InResource](FRHICommandListImmediate& RHICmdList)
{
PrivateResourceRenderThread = InResource;
});
}

这里还说了需要 init 资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UTexture::SetResource(FTextureResource *) Texture.cpp:147
UTexture::UpdateResource() Texture.cpp:190
UTexture2D::UpdateResource() Texture2D.cpp:440
UTexture::PostLoad() Texture.cpp:481
UTexture2D::PostLoad() Texture2D.cpp:377
UObject::ConditionalPostLoad() Obj.cpp:1092
FAsyncPackage::PostLoadObjects() AsyncLoading.cpp:6424
FAsyncPackage::TickAsyncPackage(bool, bool, float &, FFlushTree *) AsyncLoading.cpp:5590
FAsyncLoadingThread::ProcessAsyncLoading(int &, bool, bool, float, FFlushTree *) AsyncLoading.cpp:4098
FAsyncLoadingThread::TickAsyncThread(bool, bool, float, bool &, FFlushTree *) AsyncLoading.cpp:4856
FAsyncLoadingThread::TickAsyncLoading(bool, bool, float, FFlushTree *) AsyncLoading.cpp:4556
FAsyncLoadingThread::FlushLoading(int) AsyncLoading.cpp:7022
FlushAsyncLoading(int) AsyncPackageLoader.cpp:643
LoadPackageInternal(UPackage *, const wchar_t *, unsigned int, FLinkerLoad *, FArchive *, const FLinkerInstancingContext *) UObjectGlobals.cpp:1144
LoadPackage(UPackage *, const wchar_t *, unsigned int, FArchive *, const FLinkerInstancingContext *) UObjectGlobals.cpp:1469

这个调用堆栈也很清晰

感觉上一层的 update 应该是核心逻辑

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
void UTexture::UpdateResource()
{
// Release the existing texture resource.
ReleaseResource();

//Dedicated servers have no texture internals
if( FApp::CanEverRender() && !HasAnyFlags(RF_ClassDefaultObject) )
{
// Create a new texture resource.
FTextureResource* NewResource = CreateResource();
SetResource(NewResource);

if (NewResource)
{
...

// Init the texture reference, which needs to be set from a render command, since TextureReference.TextureReferenceRHI is gamethread coherent.
ENQUEUE_RENDER_COMMAND(SetTextureReference)([this, NewResource](FRHICommandListImmediate& RHICmdList)
{
NewResource->SetTextureReference(TextureReference.TextureReferenceRHI);
});
BeginInitResource(NewResource);

...
}
}
}

这里的 SetResourceSetTextureReference 就是简单的 set,没有做别的事情,很舒服

但是在看的时候,发现他这个注释

1
2
3
4
/** 
* The rendering resource which represents a texture.
*/
class FTextureResource : public FTexture

他的成员

1
2
3
// A FRHITextureReference to update whenever the FTexture::TextureRHI changes.
// It allows to prevent dereferencing the UAsset pointers when updating a texture resource.
FTextureReferenceRHIRef TextureReferenceRHI;

似乎有点关系

也看到 class UTexture : public UStreamableRenderAsset, public IInterface_AssetUserData

的成员

1
2
3
4
5
6
7
/** The texture's resource, can be NULL */
class FTextureResource* PrivateResource;
/** Value updated and returned by the render-thread to allow
* fenceless update from the game-thread without causing
* potential crash in the render thread.
*/
class FTextureResource* PrivateResourceRenderThread;

说明它是考虑了运行时重新加载纹理的

然后在 FRenderResource::InitResource 打断点,蹲到纹理资源的 init

调用到 FStreamableTextureResource::InitRHI

这其中重要的应该是

FTexture2DResource::CreateTexture 还有一个 RHIUpdateTextureReference(TextureReferenceRHI, TextureRHI);

create 就是调用平台特定的 API 去创建 GPU 资源句柄

reference 这里还是不知道干什么的

reload package 之后,再会调用一次 texture 的 UTexture2D::PostLoad,跟之前一样

回来看 UTexture,我想知道 Material 是怎么使用到这个材质的,或者是别的什么,总之,渲染器是怎么获取并绑定这个纹理的

于是看到这个 getter

1
2
3
4
5
/** Get the texture's resource, can be NULL */
ENGINE_API FTextureResource* GetResource();

/** Get the const texture's resource, can be NULL */
ENGINE_API const FTextureResource* GetResource() const;

这个 getter 是绑定了 UTextureTFieldPtrAccessor<FTextureResource> Resource; 成员

不断蹲,蹲到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UTexture::GetResource() Texture.cpp:132
UE4Function_Private::TFunctionRefCaller::Call(void *) Function.h:539
UE4Function_Private::TFunctionRefBase::operator()() Function.h:676
FUniformExpressionSet::FillUniformBuffer(const FMaterialRenderContext &, const FUniformExpressionCache &, unsigned char *, int) MaterialUniformExpressions.cpp:1433
FMaterialRenderProxy::EvaluateUniformExpressions(FUniformExpressionCache &, const FMaterialRenderContext &, FRHICommandList *) MaterialShared.cpp:2956
<lambda_4b8eb6ac...>::operator()(Type) MaterialShared.cpp:3213
UMaterialInterface::IterateOverActiveFeatureLevels<…>(<lambda_4b8eb6ac...>) MaterialInterface.h:861
FMaterialRenderProxy::UpdateDeferredCachedUniformExpressions() MaterialShared.cpp:3205
TEnqueueUniqueRenderCommandType<`FRendererModule::BeginRenderingViewFamily'::`2'::UpdateDeferredCachedUniformExpressionsName,<lambda_af3a665d491aaad33361bc0d189d73fc> >::DoTask(Type,const TRefCountPtr<FGraphEvent> &) RenderingThread.h:183
TGraphTask<TEnqueueUniqueRenderCommandType<`FRendererModule::BeginRenderingViewFamily'::`2'::UpdateDeferredCachedUniformExpressionsName,<lambda_af3a665d491aaad33361bc0d189d73fc> > >::ExecuteTask(TArray<FBaseGraphTask *,TSizedDefaultAllocator<32> > &,Type) TaskGraphInterfaces.h:886
FNamedTaskThread::ProcessTasksNamedThread(int, bool) TaskGraph.cpp:710
FNamedTaskThread::ProcessTasksUntilQuit(int) TaskGraph.cpp:601
FTaskGraphImplementation::ProcessThreadUntilRequestReturn(Type) TaskGraph.cpp:1480
RenderingThreadMain(FEvent *) RenderingThread.cpp:372

这个

1
void FUniformExpressionSet::FillUniformBuffer(const FMaterialRenderContext& MaterialRenderContext, const FUniformExpressionCache& UniformExpressionCache, uint8* TempBuffer, int TempBufferSize) const

看上去像是建立起材质和纹理之间的关系的部分

这里包含多个纹理类型的绑定,取其中 Texture2D 的来看

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
for(int32 ExpressionIndex = 0; ExpressionIndex < GetNumTextures(EMaterialTextureParameterType::Standard2D); ExpressionIndex++)
{
const FMaterialTextureParameterInfo& Parameter = GetTextureParameter(EMaterialTextureParameterType::Standard2D, ExpressionIndex);

const UTexture* Value = nullptr;
GetTextureValue(EMaterialTextureParameterType::Standard2D, ExpressionIndex, MaterialRenderContext, MaterialRenderContext.Material, Value);

// 资源有效性检查和日志警告
...

// 计算纹理和采样器指针在缓冲区中的位置
void** ResourceTableTexturePtr = (void**)((uint8*)BufferCursor + 0 * SHADER_PARAMETER_POINTER_ALIGNMENT);
void** ResourceTableSamplerPtr = (void**)((uint8*)BufferCursor + 1 * SHADER_PARAMETER_POINTER_ALIGNMENT);
BufferCursor = ((uint8*)BufferCursor) + (SHADER_PARAMETER_POINTER_ALIGNMENT * 2);
check(BufferCursor <= TempBuffer + TempBufferSize);

// 允许的纹理类型掩码
const uint32 ValidTextureTypes = MCT_Texture2D | MCT_TextureVirtual | MCT_TextureExternal;

bool bValueValid = false;

// 资源有效性判断
if (Value && Value->Resource && Value->TextureReference.TextureReferenceRHI && (Value->GetMaterialType() & ValidTextureTypes) != 0u)
{
FSamplerStateRHIRef* SamplerSource = &Value->Resource->SamplerStateRHI;

// 根据采样器来源模式选择采样器
const ESamplerSourceMode SourceMode = Parameter.SamplerSource;
if (SourceMode == SSM_Wrap_WorldGroupSettings)
{
SamplerSource = &Wrap_WorldGroupSettings->SamplerStateRHI;
}
else if (SourceMode == SSM_Clamp_WorldGroupSettings)
{
SamplerSource = &Clamp_WorldGroupSettings->SamplerStateRHI;
}

if (*SamplerSource)
{
*ResourceTableTexturePtr = Value->TextureReference.TextureReferenceRHI;
*ResourceTableSamplerPtr = *SamplerSource;
bValueValid = true;
}
else
{
ensureMsgf(false, TEXT("Texture %s had invalid sampler source."), *Value->GetName());
}
}

// 如果资源无效,绑定默认白色纹理和采样器
if (!bValueValid)
{
check(GWhiteTexture->TextureRHI);
*ResourceTableTexturePtr = GWhiteTexture->TextureRHI;
check(GWhiteTexture->SamplerStateRHI);
*ResourceTableSamplerPtr = GWhiteTexture->SamplerStateRHI;
}
}

可见,Material 一开始就存储了 UTexture*,纹理和材质的关系是已经建立好了

现在是要建立 Uniform buffer 和 texture 之间的关系

简单来说就是,计算缓冲区写入位置,写入纹理和采样器指针

如果,我是说如果,这个材质的渲染对我这个 2D 纹理的获取完全是依赖于 UniformExpression 的,那么就可以确定,材质仅仅通过这个路径被 GPU 获取

研究 FMaterialRenderProxy

第一次更新 UniformExpression

第一次调用应该是在 FRendererModule::BeginRenderingViewFamily

在这里有

1
2
3
4
5
ENQUEUE_RENDER_COMMAND(UpdateDeferredCachedUniformExpressions)(
[](FRHICommandList& RHICmdList)
{
FMaterialRenderProxy::UpdateDeferredCachedUniformExpressions();
});

调用到 FMaterialRenderProxy::UpdateDeferredCachedUniformExpressions

更新 UniformExpression 时如何遍历 FMaterialRenderProxy

FMaterialRenderProxy::UpdateDeferredCachedUniformExpressions 这里可以看到是如何遍历 FMaterialRenderProxy

精简之后是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void FMaterialRenderProxy::UpdateDeferredCachedUniformExpressions()
{
...

for (TSet<FMaterialRenderProxy*>::TConstIterator It(DeferredUniformExpressionCacheRequests); It; ++It)
{
FMaterialRenderProxy* MaterialProxy = *It;
UMaterialInterface::IterateOverActiveFeatureLevels([&](ERHIFeatureLevel::Type InFeatureLevel)
{
// Don't bother caching if we'll be falling back to a different FMaterialRenderProxy for rendering anyway
const FMaterial* Material = MaterialProxy->GetMaterialNoFallback(InFeatureLevel);
if (Material && Material->GetRenderingThreadShaderMap())
{
FMaterialRenderContext MaterialRenderContext(MaterialProxy, *Material, nullptr);
MaterialProxy->EvaluateUniformExpressions(MaterialProxy->UniformExpressionCache[(int32)InFeatureLevel], MaterialRenderContext);
}
});
}

DeferredUniformExpressionCacheRequests.Reset();
}

他这里是遍历一个 DeferredUniformExpressionCacheRequests,跳转可见,这是一个类的全局变量

1
2
TSet<FMaterialRenderProxy*> FMaterialRenderProxy::MaterialRenderProxyMap;
TSet<FMaterialRenderProxy*> FMaterialRenderProxy::DeferredUniformExpressionCacheRequests;

FMaterialRenderProxy 全局列表

DeferredUniformExpressionCacheRequests 的增删。Add 的逻辑在 FMaterialRenderProxy::CacheUniformExpressions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void FMaterialRenderProxy::CacheUniformExpressions(bool bRecreateUniformBuffer)
{
// Register the render proxy's as a render resource so it can receive notifications to free the uniform buffer.
InitResource();

...

DeferredUniformExpressionCacheRequests.Add(this);

InvalidateUniformExpressionCache(bRecreateUniformBuffer);

if (!GDeferUniformExpressionCaching)
{
FMaterialRenderProxy::UpdateDeferredCachedUniformExpressions();
}
}

先把自己加到全局变量的列表

如果重建,那么有一个专门的 invalidate 的函数 InvalidateUniformExpressionCache

然后如果设置了 cache,那么之后 update UniformExpression

如果没有设置 cache,那么立即 update UniformExpression

看下他这个 invalidate 的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void FMaterialRenderProxy::InvalidateUniformExpressionCache(bool bRecreateUniformBuffer)
{
...

++UniformExpressionCacheSerialNumber;
for (int32 i = 0; i < ERHIFeatureLevel::Num; ++i)
{
UniformExpressionCache[i].bUpToDate = false;
UniformExpressionCache[i].CachedUniformExpressionShaderMap = nullptr;

...

if (bRecreateUniformBuffer)
{
// This is required if the FMaterial is being recompiled (the uniform buffer layout will change).
// This should only be done if the calling code is using FMaterialUpdateContext to recreate the rendering state of primitives using this material,
// Since cached mesh commands also cache uniform buffer pointers.
UniformExpressionCache[i].UniformBuffer = nullptr;
}
}
}

这个注释似乎指的是,材质重新编译的时候,需要重建 uniform buffer,因为 uniform buffer 的 layout 会发生改变

and 不得不说的是,UE 里面的 uniform buffer 和 glsl 中的 uniform 变量,是不一样的概念。好令人错乱。

然后这个重建函数应该被 FMaterialUpdateContext 调用,因为 mesh command 的缓存里面,缓存了 uniform buffer 的指针,所以重建还需要把 mesh command 的缓存里面的 uniform buffer 的指针缓存给清了

那么他的意思应该是,FMaterialUpdateContext 可以这样清缓存

添加 FMaterialRenderProxy 到全局列表的源头

找谁调用了 FMaterialRenderProxy::CacheUniformExpressions

除了 Material 类自己的方法,还看到一个 FExternalTextureRegistry::RegisterExternalTexture 很酷

1
2
3
4
5
6
7
8
9
10
11
void FExternalTextureRegistry::RegisterExternalTexture(const FGuid& InGuid, FTextureRHIRef& InTextureRHI, FSamplerStateRHIRef& InSamplerStateRHI, const FLinearColor& InCoordinateScaleRotation, const FLinearColor& InCoordinateOffset)
{
...

TextureEntries.Add(InGuid, FExternalTextureEntry(InTextureRHI, InSamplerStateRHI, InCoordinateScaleRotation, InCoordinateOffset));

for (const FMaterialRenderProxy* MaterialRenderProxy : ReferencingMaterialRenderProxies)
{
const_cast<FMaterialRenderProxy*>(MaterialRenderProxy)->CacheUniformExpressions(false);
}
}

它是可以直接遍历所有相关联的 MaterialRenderProxy 然后直接遍历 cache

搜了一下他这个成员 TSet<const FMaterialRenderProxy*> ReferencingMaterialRenderProxies; 的类型 TSet<const FMaterialRenderProxy*>,结果发现只有它有

我还希望 Texture 相关的类型也有这个相关性呢,如果有,那就很好了

回到 Material 相关的搜索结果

1
2
3
4
5
6
7
8
9
/**
* Updates a parameter on the material instance from the game thread.
*/
template <typename ParameterType>
void GameThread_UpdateMIParameter(const UMaterialInstance* Instance, const ParameterType& Parameter)

void FMaterialRenderProxy::CacheUniformExpressions_GameThread(bool bRecreateUniformBuffer)

void SetShaderMapsOnMaterialResources_RenderThread(FRHICommandListImmediate& RHICmdList, FMaterialsToUpdateMap& MaterialsToUpdate)

都感觉很像

随便选了一个 FMaterialRenderProxy::CacheUniformExpressions_GameThread 开始不断找 usage,最终找到 UMaterial::PostLoad

删掉编辑器相关的、兼容性相关的、统计相关的,精简为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void UMaterial::PostLoad()
{
// needed for UMaterial as it doesn't have the InitResources() override where this is called
PropagateDataToMaterialProxy();

// Before caching shader resources we have to make sure all referenced textures have been post loaded
// as we depend on their resources being valid.
for (UObject* Texture : CachedExpressionData.ReferencedTextures)
{
if (Texture)
{
Texture->ConditionalPostLoad();
}
}

CacheResourceShadersForRendering(false);
}

先传播 data,然后保证纹理加载正常,然后缓存 shader 资源用于渲染

其中 UMaterial::CacheResourceShadersForRendering 是和 uniform buffer 相关

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
40
41
42
43
44
45
46
47
48
49
50
51
void UMaterial::CacheResourceShadersForRendering(bool bRegenerateId, EMaterialShaderPrecompileMode PrecompileMode)
{
if (bRegenerateId)
{
// Regenerate this material's Id if requested
FlushResourceShaderMaps();
}

// Resources cannot be deleted before uniform expressions are recached because
// UB layouts will be accessed and they are owned by material resources
FMaterialResourceDeferredDeletionArray ResourcesToFree;

if (FApp::CanEverRender())
{
const EMaterialQualityLevel::Type ActiveQualityLevel = GetCachedScalabilityCVars().MaterialQualityLevel;
uint32 FeatureLevelsToCompile = GetFeatureLevelsToCompileForRendering();

TArray<FMaterialResource*> ResourcesToCache;
while (FeatureLevelsToCompile != 0)
{
const ERHIFeatureLevel::Type FeatureLevel = (ERHIFeatureLevel::Type)FBitSet::GetAndClearNextBit(FeatureLevelsToCompile);
const EShaderPlatform ShaderPlatform = GShaderPlatformForFeatureLevel[FeatureLevel];

// Only cache shaders for the quality level that will actually be used to render
// In cooked build, there is no shader compilation but this is still needed
// to register the loaded shadermap
FMaterialResource* CurrentResource = FindOrCreateMaterialResource(MaterialResources, this, nullptr, FeatureLevel, ActiveQualityLevel);
check(CurrentResource);

ResourcesToCache.Reset();
ResourcesToCache.Add(CurrentResource);
CacheShadersForResources(ShaderPlatform, ResourcesToCache, PrecompileMode);
}

...

RecacheUniformExpressions(true);
}

if (ResourcesToFree.Num())
{
ENQUEUE_RENDER_COMMAND(CmdFreeUnusedMaterialResources)(
[ResourcesToFreeRT = MoveTemp(ResourcesToFree)](FRHICommandList&)
{
for (int32 Idx = 0; Idx < ResourcesToFreeRT.Num(); ++Idx)
{
delete ResourcesToFreeRT[Idx];
}
});
}
}

可以看出,这里是先缓存 FMaterialResource 类型的对象,然后缓存 UniformExpression

UMaterial::RecacheUniformExpressions 内部对默认的 Material 实例调用我之前想找的 CacheUniformExpressions_GameThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void UMaterial::RecacheUniformExpressions(bool bRecreateUniformBuffer) const
{
bool bUsingNewLoader = EVENT_DRIVEN_ASYNC_LOAD_ACTIVE_AT_RUNTIME && GEventDrivenLoaderEnabled;

// Ensure that default material is available before caching expressions.
if (!bUsingNewLoader)
{
UMaterial::GetDefaultMaterial(MD_Surface);
}

if (DefaultMaterialInstance)
{
DefaultMaterialInstance->CacheUniformExpressions_GameThread(bRecreateUniformBuffer);
}
}

看上去,一个材质是一个抽象的概念,实际逻辑一定有一个实例来完成,是这个意思?