虚幻4笔记-渲染线程源码和TaskGraph多线程机制源码实现分析
本来想了解渲染线程的工作原理,但是直接看感觉是看不明白,所以先需要看明白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)(
(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>(
(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;
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);
}
}
当有任务添加的时候,如果有空闲线程,则激活其执行任务,执行完成后把其放回线程池,然后继续等待执行。 不愧是A神 很棒的文章,超赞
页:
[1]