UE4渲染任务的产生及入队

渲染任务是如何产生并压入到渲染队列的呢
还记得ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER宏吗,该宏的作用就是生成渲染任务并压入渲染队列。
这是笔者知道的一种方式,应该还有其他方式。

ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER宏展开后会得到下面的核心代码,
它的作用是把创建任务并压入渲染队列:
TGraphTask::CreateTask().ConstructAndDispatchWhenReady(this);

先介绍TGraphTask:它是一个模板类,继承自FBaseGraphTask,里面维护了具体的任务,作用相当于具体任务的托管.
CreateTask() 创建了TGraphTask对象,同时也创建了具体的任务(EURCMacroBeginDrawingCommand).
下面展示的代码都是围绕渲染线程执行任务展开的,并且只是展示核心的代码。

看看ConstructAndDispatchWhenReady里做了什么
FGraphEventRef ConstructAndDispatchWhenReady(T&&... Args)
{
	new ((void *)&Owner->TaskStorage) TTask(Forward(Args)...);
	//这个Owner就是TGraphTask对象,这里Prerequisites和CurrentThreadIfKnown都为null
	return Owner->Setup(Prerequisites, CurrentThreadIfKnown);
}
Setup中调用了SetupPrereqs
void SetupPrereqs(const FGraphEventArray* Prerequisites, ENamedThreads::Type CurrentThreadIfKnown, bool bUnlock)
{
	//这个Task就是具体的任务,对应上面的EURCMacroBeginDrawingCommand实例
	TTask& Task = *(TTask*)&TaskStorage;
	//设置任务期望在哪个线程被执行,GetDesiredThread这里获取的值是ENamedThreads::RenderThread,即渲染线程!
	SetThreadToExecuteOn(Task.GetDesiredThread());
	int32 AlreadyCompletedPrerequisites = 0;
	
	PrerequisitesComplete(CurrentThreadIfKnown, AlreadyCompletedPrerequisites, bUnlock);
}

void PrerequisitesComplete(ENamedThreads::Type CurrentThread, int32 NumAlreadyFinishedPrequistes, bool bUnlock = true)
{
	QueueTask(CurrentThread);
}

QueueTask里也很简单
void QueueTask(ENamedThreads::Type CurrentThreadIfKnown)
{
//这三个实参分别为:入队的task,task期待被执行的线程Id(这里为渲染线程),当前的线程(这里为AnyThread)
	FTaskGraphInterface::Get().QueueTask(this, ThreadToExecuteOn, CurrentThreadIfKnown);
}
FTaskGraphInterface::Get()得到子类FTaskGraphImplementation
可见最终会调用到FTaskGraphImplementation的QueueTask(),该接口的作用是把任务放入正确的队列
virtual void QueueTask(FBaseGraphTask* Task, ENamedThreads::Type ThreadToExecuteOn, ENamedThreads::Type InCurrentThreadIfKnown = ENamedThreads::AnyThread) final override
{

	ENamedThreads::Type CurrentThreadIfKnown;
	if (ENamedThreads::GetThreadIndex(InCurrentThreadIfKnown) == ENamedThreads::AnyThread)
	{
		//在这里会执行这一步,GetCurrentThread得到的是主线程
		CurrentThreadIfKnown = GetCurrentThread();
	}
	else
	{
		CurrentThreadIfKnown = ENamedThreads::GetThreadIndex(InCurrentThreadIfKnown);
		checkThreadGraph(CurrentThreadIfKnown == ENamedThreads::GetThreadIndex(GetCurrentThread()));
	}
	{
		int32 QueueToExecuteOn = ENamedThreads::GetQueueIndex(ThreadToExecuteOn);
		ThreadToExecuteOn = ENamedThreads::GetThreadIndex(ThreadToExecuteOn);
		FTaskThreadBase* Target = &Thread(ThreadToExecuteOn);
		//这里ThreadToExecuteOn是渲染线程,CurrentThreadIfKnown是主线程
		if (ThreadToExecuteOn == ENamedThreads::GetThreadIndex(CurrentThreadIfKnown))
		{	//压入和期待被执行的线程相同
			Target->EnqueueFromThisThread(QueueToExecuteOn, Task);
		}
		else
		{	//压入和期待被执行的线程不同,所以执行这里
			Target->EnqueueFromOtherThread(QueueToExecuteOn, Task);
		}
	}
}

EnqueueFromOtherThread里主要是把task压入渲染线程的IncomingQueue队列里,IncomingQueue的优先级最低
还有两个队列,分别是 PrivateQueueHiPri和PrivateQueue,PrivateQueueHiPri里的任务会优先执行
virtual bool EnqueueFromOtherThread(int32 QueueIndex, FBaseGraphTask* Task) override
{	
	//把任务放入IncomingQueue队列里
       //从这里可以看出IncomingQueue队列存储的是压入线程不同于期待被执行的线程的task
	Queue(QueueIndex).IncomingQueue.ReopenIfClosedAndPush(Task);
}
至此,结合前面几篇博客,UE4的整个渲染系统框架的来龙去脉大致摸清楚了!
总结:
在game线程中每帧通过逻辑运算会产生很多渲染task, 其中一种方式是调用ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER,然后发送到渲染线程维护的队列里。 之后,渲染线程不断调用ProcessTasksNamedThread处理任务,这里的任务大都是执行渲染指令。通过这种方式达到了渲染和逻辑运算分离并同时运行的目的(并不是同时运行同一帧,而是渲染线程跑第N帧,game线程跑N+1帧),这也就是我们常说的多线程渲染。 以前的引擎架构大都数单线程的,先通过游戏逻辑算出需要渲染的数据,再把这些数据封装成渲染指令,之后进行渲染。这种架构优点是简单清晰,各种流程都易于控制;但缺点也很明显, 无法利用cpu的多个核。而现在的手机,电脑cpu的核数不断在增加,主频却比以前低了不少,充分利用这些核是一种提高游戏效率不可或缺的途径,所以,多线程渲染必将成为游戏引擎的主流方式。











你可能感兴趣的:(Unreal,Engine,4)