UE 多线程渲染管线的分析

时间:2025-02-20 07:51:27

上一篇讲解UE中Shader的运作(编译,存储和使用过程),但是使用的是比较久远的资料,像DrawingPolicy这种概念已经被废弃了,取而代之的是MeshDrawPipeline(我个人的理解就是UE实现了场景几何的种类划分+Instance技术),在知乎有专题将这个话题:
虚幻4渲染编程(Shader篇)【第十二卷:MeshDrawPipline】 - 知乎 ()

这是接着我上一篇参考文献的补充说明,但是因为我个人还没达到作者那样的境界,所以我没有看懂(那就是基础不行),所以我又找其他资料,直到我发现一篇分析多选陈渲染管线的。可以这么说吧两者的讲解殊途同归,前者重视代码流程,后者重视基本概念,如果是我这样的小白还是先看看基础概念,本文就是我看过之后的读书笔记。

UE4的多线程渲染

多线程:GameThread,RenderThread和RHIThread

GameThread:发送命令,真正由程序员书写逻辑的部分,通过某些接口(宏)向渲染线程的Queue入队回调接口(可调用对象),以便渲染线程稍后从中取出这个可调用对象,一个一个的执行生成Command List;

RenderThread:分发渲染任务,用于处理不同pass(取出GameThread放入Queue的可调用对象,生成Command List,发送给RHIThread,以实现与硬件无关的跨平台)

RHIThread:确定当前硬件平台,包装Command List使GPU完成运算。RHI是RenderThread与底层硬件(GPU)之间的中间层(可在细分为执行层+一个上方的薄层):薄层为RHI抽象层(负责接收RenderThread的Command List,然后转化,分发给不同的RHI执行层,再调用对应平台的API(DX,OG等等)向GPU发送硬件指令)

GameThread分析:

系统启动时就被创建(Prelnit阶段),会被设置到任务图表(TaskGraph)中,可在中找到:

int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)

{

    (......)

    // 获取当前线程id, 存储到全局变量中.

    GGameThreadId = FPlatformTLS::GetCurrentThreadId();

    GIsGameThreadIdInitialized = true;

    FPlatformProcess::SetThreadAffinityMask(FPlatformAffinity::GetMainGameMask());

    // 设置游戏线程数据(但很多平台都是空的实现体)

    FPlatformProcess::SetupGameThread();

    (......)

    if (bCreateTaskGraphAndThreadPools)

    {

        SCOPED_BOOT_TIMING("FTaskGraphInterface::Startup");

        FTaskGraphInterface::Startup(FPlatformMisc::NumberOfCores());

        // 将当前线程(主线程)附加到TaskGraph的GameThread命名插槽中. 这样主线程便和TaskGraph联动了起来.

        FTaskGraphInterface::Get().AttachToThread(ENamedThreads::GameThread);

    }

}

PreInit阶段还会创建RenderThread等,在FEngineLoop::PreInitPostStartupScreen会调用StartRenderingThread,后边操作渲染线程只是调用相关接口。

有关线程之间的通信,UE进行了一些设计(不同模块之间在名称上大致对应,并有一定规律):

GameThread