FRenderCommandFence類

FRenderCommandFence用於game線程和渲染線程的同步,UE4最多允許game領先渲染線程一幀,也就是渲染線程跑第N幀的時候,game線程最多跑第N+1幀,這是因爲game線程跑的太快沒多大意義,還會耗光內存。因爲game線程不斷的產生數據傳遞給渲染線程,如果渲染線程消費數據遠遠慢於產生數據,就會有越來越多的數據存於內存中。

先介紹一下FRenderCommandFence 使用方式.
FRenderCommandFence Fence;
Fence.BeginFence();
Fence.Wait();
上面的代碼會使game線程掛起,渲染線程不斷執行任務,直到遇到某個任務,這個任務就是喚醒game線程。那麼當game線程醒來的時候,渲染線程進度已經跟上來了。
看看BeginFence做了什麼
void FRenderCommandFence::BeginFence()
{
	if (!GIsThreadedRendering)
	{
		return;
	}
	{
		//向渲染線程(RenderThread)隊列發送一個task(FNullGraphTask),當前執行該接口的線程爲GameThread
		CompletionEvent = TGraphTask<FNullGraphTask>::CreateTask(NULL, ENamedThreads::GameThread).ConstructAndDispatchWhenReady(
			GET_STATID(STAT_FNullGraphTask_FenceRenderCommand), ENamedThreads::RenderThread);
	}
}
TGraphTask<FNullGraphTask>::CreateTask(NULL, ENamedThreads::GameThread)表示創建一個FNullGraphTask任務,並明確指明現在所在的線程是GameThread. 
ConstructAndDispatchWhenReady(GET_STATID(STAT_FNullGraphTask_FenceRenderCommand), ENamedThreads::RenderThread) 將把FNullGraphTask對象會被放到渲染線程的任務隊列裏,該任務相當於一個哨兵,主線程調用Wait後,只有當該任務被執行了,Wait纔會返回,這時該任務之前的任務也被執行完了(這些任務都是在渲染線程中執行的)。
返回值是一個FGraphEventRef,賦予CompletionEvent,用於判斷該任務是否完成了.
判斷CommandFence是否完成了
bool FRenderCommandFence::IsFenceComplete() const
{
	if (!GIsThreadedRendering)
	{
		return true;
	}
	check(IsInGameThread() || IsInAsyncLoadingThread());
	CheckRenderingThreadHealth();
	if (!CompletionEvent.GetReference() || CompletionEvent->IsComplete())
	{
		// this frees the handle for other uses, the NULL state is considered completed
		CompletionEvent = NULL; 
		return true;
	}
	return false;
}
Wait()使game線程等待CompletionEvent完成,期間game線程不斷掛起,直到渲染線程的當前幀所有任務都執行完,這個作用是爲了防止game線程跑的太快.
void FRenderCommandFence::Wait(bool bProcessGameThreadTasks) const
{
	if (!IsFenceComplete())
	{
		GameThreadWaitForTask(CompletionEvent, bProcessGameThreadTasks);
	}
}

static void GameThreadWaitForTask(const FGraphEventRef& Task, bool bEmptyGameThreadTasks = false)
{
	if (!Task->IsComplete())
	{	
		//創建一個Event,用於掛起game線程,直到任務完成
		FEvent* Event = FPlatformProcess::GetSynchEventFromPool();
		//作用是當Task完成後,將調用Event->Trigger()喚醒game線程,
		FTaskGraphInterface::Get().TriggerEventWhenTaskCompletes(Event, Task, ENamedThreads::GameThread);

		bool bDone;
  //獲取睡眠時長
		uint32 WaitTime = FMath::Clamp<uint32>(GTimeToBlockOnRenderFence, 0, 33);
		do
		{	
			//當game線程掛起WaitTime時間後再次醒來,這時bDone=false,只能不斷循環
			//當Event->Trigger()喚醒game線程,bDone = true,跳出循環. 
			bDone = Event->Wait(WaitTime);
		}
		while (!bDone);

		FPlatformProcess::ReturnSynchEventToPool(Event);
		Event = nullptr;		
	}
}
下面看看引擎是如何使用該類來進行主線程和渲染線程的同步的,這裏涉及到另一個類FFrameEndSync,該類使用兩個FRenderCommandFence來同步主線程和渲染線程,這就是爲什麼主線程可以領先渲染線程一幀
class FFrameEndSync
{
	FRenderCommandFence Fence[2];
	int32 EventIndex;
public:
 //同步主線程和渲染線程
	ENGINE_API void Sync( bool bAllowOneFrameThreadLag );
};
引擎的Tick函數裏都會做一次同步的操作,Tick函數就是引擎每幀的執行體.
void FEngineLoop::Tick()
{
	GEngine->Tick( FApp::GetDeltaTime(), bIdleMode );
	static FFrameEndSync FrameEndSync;
	static auto CVarAllowOneFrameThreadLag = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.OneFrameThreadLag"));
	FrameEndSync.Sync( CVarAllowOneFrameThreadLag->GetValueOnGameThread() != 0 );
}
主要操作在Sync接口裏
void FFrameEndSync::Sync( bool bAllowOneFrameThreadLag )
{
	Fence[EventIndex].BeginFence();
	bool bEmptyGameThreadTasks = !FTaskGraphInterface::Get().IsThreadProcessingTasks(ENamedThreads::GameThread);

	if (bEmptyGameThreadTasks)
	{
		//這裏將允許主線程處理任務,直到空閒纔會往下走.
		FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
	}

	//允許主線程領先於渲染線程一幀.
	if( bAllowOneFrameThreadLag )
	{
		EventIndex = (EventIndex + 1) % 2;
	}

	Fence[EventIndex].Wait(bEmptyGameThreadTasks);  
}
分析下執行流程:
bAllowOneFrameThreadLag = false,只使用Fence[0],這時候調用Fence[0].Wait接口會馬上進行同步
bAllowOneFrameThreadLag = true :
第一幀的時候:Fence[0].BeginFence(); Fence[1].Wait() (Fence[1].Wait()這裏並不會使主線程掛起,會立即返回)
第二幀的時候:Fence[1].BeginFence(); Fence[0].Wait() (如果Fence[0]的事件被執行了,說明渲染線程跑的速度和主線程差不多,這時也是不需要掛起主線程的)
第三幀的時候:Fence[0].BeginFence(); Fence[1].Wait()
由此可見,Fence[index].BeginFence()和Fence[index].Wait()調用永遠都隔了一幀,從而可以使主線程可以領先渲染線程一幀(如果主線程跑的足夠快的話).
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章