找回密码
 立即注册
查看: 635|回复: 2

虚幻4笔记-渲染线程源码和TaskGraph多线程机制源码实现分析

[复制链接]
发表于 2021-2-4 22:14 | 显示全部楼层 |阅读模式
本来想了解渲染线程的工作原理,但是直接看感觉是看不明白,所以先需要看明白Unreal多线程的原理。
想要用Unreal提供的多线程机制,离不开下面两个类:
FRunnable
FRunnableThread
FRunnableThread实现,是在ThreadingBase里面。
FRunnableThread持有着FRunnable的实例,而我们需要做的是继承FRunnable,并把该实例放进FRunnableThread里面,重点是要实现其Run函数。


FRunnableThread才是正常意义上的线程,在不同平台下,Unreal有不同的实现,就拿Windows平台来说:
通过FRunnableThread::Create创建相应平台的线程,主要通过下面的方法。
NewThread = FPlatformProcess::CreateRunnableThread();
FPlatformProcess有多平台处理,进入Windows平台下的CreateRunnableThread,可以看见实际创建的是FRunnableThreadWin。


跟进FRunnableThreadWin,有下面的函数
NewThread->CreateInternal(InRunnable,ThreadName,InStackSize,InThreadPri,InThreadAffinityMask)
这里已经进入到Windows的线程创建了,可以看出线程函数为_ThreadProc
在这里调用GuardedRun方法,进而调用FRunnable实例的Run方法。
基本上,Unreal自带最基础的多线程流程就是这样实现了:
平台Thread创建->FRunnableThread.GuardedRun->FRunnableThread.Run->FRunnable.Run。


看完最基础的多线程使用后,回过头来看Unreal的渲染线程创建。
在前面的文章提到过,在引擎启动的过程中,在EngineLoop的PreInit方法里面,会调用StartRenderingThread创建渲染线程FRenderingThread。
GRenderingThreadRunnable = new FRenderingThread();
GRenderingThread = FRunnableThread::Create(GRenderingThreadRunnable, *BuildRenderingThreadName(ThreadCount), 0, FPlatformAffinity::GetRenderingThreadPriority(), FPlatformAffinity::GetRenderingThreadMask());
到这里就很熟悉,在上面就分析过Unreal多线程的最基本使用,实际就是会调用到FRenderingThread的Run函数。跳过去再看看里面,发现其实际调用的是RenderingThreadMain:
在这里有两个关键代码
FTaskGraphInterface::Get().AttachToThread(ENamedThreads::RenderThread);
FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(ENamedThreads::RenderThread);
第一句代码的作用,就是把当前线作为渲染线程挂接到TaskGraph,而第二句就是告诉TaskGraph系统,使用该线程一直处理渲染任务,直到请求退出。


到这里又来一个问题,TaskGraph究竟是个什么个原理?怎么执行呢?
那么继续开始分析第一句AttachToThread:
这里可以粗略理解成,通过不同平台,通过不同的线程表示,来为线程进行标记,使得可以通过ENamedThreads::Type来操作对应的线程。


第二句ProcessThreadUntilRequestReturn:
进入ProcessTasksUntilQuit
原理其实也是比较清晰明了,其实就是一个while循环,一直在向对应Queue拿出相应的FBaseGraphTask* Task,进行处理,直到对应队列的QuitForReturn为真。
具体逻辑在ProcessTaskNamedThread里面:
其中,如果没有任务,在Windows平台下,则通过信号量对线程进行挂起,也就是下面函数,对Event调用Wait方法。
Queue(QueueIndex).StallRestartEvent->Wait(MAX_uint32, bCountAsStall);
对应Event的win平台实现WaitForSingleObject( Event, WaitTime ):
如果有任务,则调用Task实例的Execute方法:
Task->Execute(NewTasks, ENamedThreads::Type(ThreadId | (QueueIndex << ENamedThreads::QueueIndexShift)));
到这里其实也大概明白到TaskGraph系统的基础运行原理,渲染线程就是在这个基础之上运行,也就顺势搞懂了渲染线程的原理了:
渲染任务->TaskGraph中的渲染任务队列
渲染线程->激活->查找TaskGraph中的渲染任务队列中的任务进行执行->挂起->激活........


那么,分析完UE多线程渲染后,剩下的是,渲染任务是怎么入队的呢?
重点在ENQUEUE_RENDER_COMMAND这个宏,举个例子,看在FPrimitiveSceneProxy下的一个调用:


ENQUEUE_RENDER_COMMAND(SetEditorVisibility)(
[PrimitiveSceneProxy, InHiddenEditorViews](FRHICommandListImmediate& RHICmdList)
{
PrimitiveSceneProxy->SetHiddenEdViews_RenderThread(InHiddenEditorViews);
});
其中ENQUEUE_RENDER_COMMAND定义为
#define ENQUEUE_RENDER_COMMAND(Type) \
struct Type##Name \
{ \
static const char* CStr() { return #Type; } \
static const TCHAR* TStr() { return TEXT(#Type); } \
}; \
EnqueueUniqueRenderCommand<Type##Name>
其实这里就是创建了一个SetEditorVisibilityName的结构体,然后变成
EnqueueUniqueRenderCommand <SetEditorVisibilityName>(
[PrimitiveSceneProxy, InHiddenEditorViews](FRHICommandListImmediate& RHICmdList)
{
PrimitiveSceneProxy->SetHiddenEdViews_RenderThread(InHiddenEditorViews);
});
再看看EnqueueUniqueRenderCommand的实现


这里判断是渲染线程,就立刻执行。不是的话,通过TGraphTask<EURCType>::CreateTask().ConstructAndDispatchWhenReady(Forward<LAMBDA>(Lambda));
来创建GraphTask,其中EURCType是TEnqueueUniqueRenderCommandType的别名,看看TEnqueueUniqueRenderCommandType的实现。


继承自FRenderCommand,而DoTask是调用外部传入的Lambda,同时把RHICmdList作为参数传入。
再看看FRenderCommand的实现。
其实很简单,就是把这个任务假如到之前所说的渲染线程队列。基本上就是渲染任务的入队操作。
结合上述渲染线程在TaskGraph的执行,在加上做逻辑线程渲染任务的入队分析,Unreal的整体渲染框架基本有个明了了。
那么还有一个问题,就是如何把UE的World里面的Actor与实现的渲染任务绑定再一起呢?
这个需要等后续分析理解。


除了TashGraph系统和自带的FRunnable,还有一个FQueuedThread,实现原理类似。
FQueuedThread::Run()
{
           bool bContinueWaiting = true;
            //在这个循环里,Wait()使线程不断挂起.
while( bContinueWaiting )
{               
bContinueWaiting = !DoWorkEvent->Wait( 10 );
}
            。。。。。。。
}
进行wait等待
QueuedPool->AddQueuedWork(InQueuedWork)
{
       //取出一个空闲的线程执行InQueuedWork,没有则添加到QueuedWork中
if (QueuedThreads.Num() > 0)
{
int32 Index = 0;
Thread = QueuedThreads[Index];
QueuedThreads.RemoveAt(Index);
}
if (Thread != nullptr)
{
//DoWork实际上只是触发了event,从而激活等待的线程
Thread->DoWork(InQueuedWork);
}
        else
QueuedWork.Add(InQueuedWork);
}
FQueuedThread::DoWork(IQueuedWork* InQueuedWork)
{
       DoWorkEvent->Trigger();
}
调用DoWorkEvent->Trigger()之后,线程的Run()得以继续执行,接下来的工作便是不断取出Task来执行.
FQueuedThread::Run()
{
       。。。。。。。。
           IQueuedWork* LocalQueuedWork = QueuedWork         
while (LocalQueuedWork)
{
                //最终会执行Task.DoWork();
LocalQueuedWork->DoThreadedWork();
//取出下一个待执行的task或者把该FQueuedThread添加回空闲线程池中
LocalQueuedWork = OwningThreadPool->ReturnToPoolOrGetNextJob(this);
}
}
当有任务添加的时候,如果有空闲线程,则激活其执行任务,执行完成后把其放回线程池,然后继续等待执行。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-2-4 22:18 | 显示全部楼层
不愧是A神
发表于 2021-2-4 22:27 | 显示全部楼层
很棒的文章,超赞
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-15 19:42 , Processed in 0.091812 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表