UE4渲染任務的產生及入隊

渲染任務是如何產生並壓入到渲染隊列的呢
還記得ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER宏嗎,該宏的作用就是生成渲染任務並壓入渲染隊列。
這是筆者知道的一種方式,應該還有其他方式。

ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER宏展開後會得到下面的核心代碼,
它的作用是把創建任務並壓入渲染隊列:
TGraphTask<EURCMacroBeginDrawingCommand>::CreateTask().ConstructAndDispatchWhenReady(this);

先介紹TGraphTask:它是一個模板類,繼承自FBaseGraphTask,裏面維護了具體的任務,作用相當於具體任務的託管.
CreateTask() 創建了TGraphTask對象,同時也創建了具體的任務(EURCMacroBeginDrawingCommand).
下面展示的代碼都是圍繞渲染線程執行任務展開的,並且只是展示核心的代碼。

看看ConstructAndDispatchWhenReady裏做了什麼
FGraphEventRef ConstructAndDispatchWhenReady(T&&... Args)
{
	new ((void *)&Owner->TaskStorage) TTask(Forward<T>(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的核數不斷在增加,主頻卻比以前低了不少,充分利用這些核是一種提高遊戲效率不可或缺的途徑,所以,多線程渲染必將成爲遊戲引擎的主流方式。











發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章