記的上次看過UniRx裏的源代碼,說是參考微軟的響應式編程框架,響應式編程裏的一些理論不細說,只單說UniRx裏的事件流裏的事件壓入與執行,與UE4的渲染線程設計有不少相同之處,若是有了解響應式編程相關源碼如UniRx,應該對UE4的渲染線程流程容易理解。編程
在這先說下UniRx相應事件流的處理,讓不瞭解的同窗大體有點印象,如當前線程計劃,通常首先有個隊列,在相應事件響應後,把相應處理方法填充到隊列中,另外一邊則在隊列裏,根據先進先出的原則,不斷執行隊列裏的方法。提及來比較簡單,主要是這裏只拿出UniRx裏的一個執行計劃的事件流來講,另外的相關響應式編程概念與本文無關,也就不提起來講。框架
回到正題,說了UE4渲染流程的設計與上面不少相同,如此,咱們先簡單來講明下相關UE4裏的類,與上面說的來對應。ide
FBaseGraphTask: 上面說到事件流,那麼這個類在這,就是事件流裏的每一個事件。函數
TGraphTask: FBaseGraphTask的一個子類模版類,模版類要求有方法DoTask.(注意這裏,後面要說。相應在此處簡單理解成C#裏的泛型約束,雖然C#直接作不到這點,能夠簡接使用泛型約束加接口實現)oop
FTaskThreadBase: 簡單來講,這個類裏放的是事件流,以及相應處理事件流的一些方法,如ui
EnqueueFromThisThread: 壓入事件流中。this
ProcessTasksUntilQuit: 循環執行事件流裏方法,直到有要求結束信號。spa
IsProcessingTasks: 是否正在執行方法。線程
FNameTaskThread: FTaskThreadBase的子類,簡單來講,UE4裏內置的用這個,如遊戲線程,渲染線程。設計
FTaskThreadAnyThread: FTaskThreadBase的子類,簡單來講,沒有固定用途的用這個,如本身用來作啥作啥。
FRunnable: 說是線程執行體,是否是有點搞暈了,其實你看下面他渲染線程的子類就明白了。
FRenderingThread: FRunnable的子類,主要有方法Run調用執行渲染線程的事件流,上面的FTaskThreadBase::ProcessTasksUntilQuit這個方法。
FRunnableThread: 包含一個FRunnable與相應的TLS實現,TLS搜了一下,簡單來講,相同的變量,每一個線程能夠有不一樣的值。
FWorkerThread: 包含FTaskThreadBase(事件流)與FRunnableThread(線程執行與TLS)的引用。封裝相應對象FTaskThreadBase與FRunnableThread公開。
FTaskGraphInterface: 能夠理解成一個單例管理類,管理全部FWorkerThread(線程與事件流),通常管理類的方法,根據類型獲得對應的FWorkerThread等。
好吧,到這確定有點暈了,你們最好對着相應代碼來理解,那麼這些類是如何組成一個渲染線程。
1。初始準備,FTaskGraphInterface初始化相應的渲染線程所需的FNameTaskThread,以及調用StartRenderingThread,建立渲染線程執行體FRunnable的子類FRenderingThread。注意有個全局變量GIsThreadedRendering開始標爲true。
2。FRenderingThread開始執行RenderingThreadMain,找到渲染線程的FWorkerThread,初始化相應TLS的ID值。如上所說,循環執行FTaskThreadBase::ProcessTasksUntilQuit裏的事件
3。點程序退出等,ProcessTasksUntilQuit中斷,相應渲染線程上的數據開始清理。
看到如上,咱們確定會想到2裏渲染線程執行的事件是如何來的,在這咱們引入一些宏,你們看UE4的源碼時,確定會常見,ENQUEUE_UNIQUE_RENDER_COMMAND,ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER後面多參數的版本等。
這些宏拆開來,都有一個類和一段執行代碼,咱們根據ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER來講,以下:
ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER( ReleaseShaderMap, FMaterial*,Material,this, { Material->SetRenderingThreadShaderMap(nullptr); });
首先生成類EURCMacro_ReleaseShaderMap,繼承於FRenderCommand,根據傳入參數類型生成構造函數,生成一個方法DoTask(見上面TGraphTask類說明),DoTask方法裏執行的就是上面代碼{}裏的一段。
而後生成一段執行碼,簡單來講,就是結合上面的類EURCMacro_ReleaseShaderMap生成模版類TGraphTask<EURCMacro_ReleaseShaderMap>,並使用this來初始化對應類型FMaterial*的變量Material.具體看以下宏。
#define ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER_DECLARE_OPTTYPENAME(TypeName,ParamType1,ParamName1,ParamValue1,OptTypename,Code) \ class EURCMacro_##TypeName : public FRenderCommand \ { \ public: \ EURCMacro_##TypeName(OptTypename TCallTraits<ParamType1>::ParamType In##ParamName1): \ ParamName1(In##ParamName1) \ {} \ TASK_FUNCTION(Code) \ TASKNAME_FUNCTION(TypeName) \ private: \ ParamType1 ParamName1; \ }; #define ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER_DECLARE(TypeName,ParamType1,ParamName1,ParamValue1,Code) \ ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER_DECLARE_OPTTYPENAME(TypeName,ParamType1,ParamName1,ParamValue1,,Code) #define ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER_CREATE(TypeName,ParamType1,ParamValue1) \ { \ if(GIsThreadedRendering || !IsInGameThread()) \ { \ CheckNotBlockedOnRenderThread(); \ TGraphTask<EURCMacro_##TypeName>::CreateTask().ConstructAndDispatchWhenReady(ParamValue1); \ } \ else \ { \ EURCMacro_##TypeName TempCommand(ParamValue1); \ FScopeCycleCounter EURCMacro_Scope(TempCommand.GetStatId()); \ TempCommand.DoTask(ENamedThreads::GameThread, FGraphEventRef() ); \ } \ } #define ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(TypeName,ParamType1,ParamName1,ParamValue1,Code) \ ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER_DECLARE(TypeName,ParamType1,ParamName1,ParamValue1,Code) \ ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER_CREATE(TypeName,ParamType1,ParamValue1)
相應的ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER_CREATE就是執行代碼,在遊戲中,由於只有在渲染線程才執行,因此通常來講生成上面的TGraphTask<EURCMacro_ReleaseShaderMap>::CreateTask().ConstructAndDispatchWhenReady(this);在這段代碼中,CreateTask建立一個FConstructor實例,ConstructAndDispatchWhenReady用參數this生成EURCMacro_ReleaseShaderMap類的實例,並在後面調用相應FNameTaskThread::EnqueueFromThisThread壓入當前FBaseGraphTask到事件流中。
然後在FRenderingThread中,循環執行事件流中的FBaseGraphTask的Execute,就是對應EURCMacro_ReleaseShaderMap裏的ToTask方法。
最後綜合說下ENQUEUE_UNIQUE_RENDER_COMMAND等類宏,聲明時,生成二段代碼,一個是類,類裏方法告訴這個事件應該如何執行。二是一段執行碼,這段執行碼生成一個上面類的TGraphTask模版類,並壓入這個TGraphTask到對應的渲染線程的事件流中,當後面在渲染線程執行到後就執行上面類裏的ToTask方法。
如上渲染線程的流程差很少就介紹到這,還有一個大的問題是,渲染線程如何與遊戲線程同步,畢竟,你遊戲線程若是不一樣步,或者跑的很快,可是畫面仍是之前數據渲染出來的,這樣問題就比較嚴重了啥。以下,先看一段代碼。
class RENDERCORE_API FRenderCommandFence { public: /** * Adds a fence command to the rendering command queue. * Conceptually, the pending fence count is incremented to reflect the pending fence command. * Once the rendering thread has executed the fence command, it decrements the pending fence count. */ void BeginFence(); /** * Waits for pending fence commands to retire. * @param bProcessGameThreadTasks, if true we are on a short callstack where it is safe to process arbitrary game thread tasks while we wait */ void Wait(bool bProcessGameThreadTasks = false) const; // return true if the fence is complete bool IsFenceComplete() const; private: /** Graph event that represents completion of this fence **/ mutable FGraphEventRef CompletionEvent; }; class FFrameEndSync { /** Pair of fences. */ FRenderCommandFence Fence[2]; /** Current index into events array. */ int32 EventIndex; public: /** * Syncs the game thread with the render thread. Depending on passed in bool this will be a total * sync or a one frame lag. */ ENGINE_API void Sync( bool bAllowOneFrameThreadLag ); }; // FEngineLoop::Tick 渲染快結束時 { SCOPE_CYCLE_COUNTER( STAT_FrameSyncTime ); // this could be perhaps moved down to get greater parallelizm // Sync game and render thread. Either total sync or allowing one frame lag. static FFrameEndSync FrameEndSync; static auto CVarAllowOneFrameThreadLag = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.OneFrameThreadLag")); FrameEndSync.Sync( CVarAllowOneFrameThreadLag->GetValueOnGameThread() != 0 ); }
主要有二個類,FRenderCommandFence與FFrameEndSync,以及每楨結束前的一段代碼,只貼出相應類的聲明,相應實現你們有興趣能夠本身去看。
FRenderCommandFence:同步遊戲線程與渲染線程
BeginFence: 插入一個事件到渲染線程中。
Wait: 遊戲線程等待上面插入的事件已經執行完成,不然遊戲線程暫停執行。
FFrameEndSync:讓遊戲線程最多比渲染線程快一楨。
在RenderingThread.cpp中,咱們很容易看下以下代碼。
// ensure the thread has actually started and is idling FRenderCommandFence Fence; Fence.BeginFence(); Fence.Wait();
能夠看到,由於隊列的先進先出原則,當調用BeginFence時,必然在渲染隊列的最後面,那麼wait須要等到整個渲染隊列執行完,遊戲線程才能繼續。
在FEngineLoop::Tick等遊戲線程每楨執行完後,必然壓入不少命令到渲染線程中,那麼這時調用beginFence的命令必然在隊列最後,若是保持遊戲線程與渲染線程同步,只須要是調用前面的beginFence的實例調用wait,這樣遊戲線程必須要等到渲染線程執行完才能繼續,若是容許遊戲線程比渲染線程快一楨,就是上面FFrameEndSync所作,生成二個FFrameEndSync,第一楨結尾第一個調用beginFence,須要等到第二楨結尾才調用對應實例的wait,這樣就能讓遊戲線程比渲染線程快一楨。至於渲染線程比遊戲線程快,這個是沒問題的,由於渲染的畫面一直是最新的數據。
如上就是UE4簡單的渲染流程與同步解決方法。