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()調用永遠都隔了一幀,從而可以使主線程可以領先渲染線程一幀(如果主線程跑的足夠快的話).