UE4里的渲染線程


  記的上次看過UniRx里的源代碼,說是參考微軟的響應式編程框架,響應式編程里的一些理論不細說,只單說UniRx里的事件流里的事件壓入與執行,與UE4的渲染線程設計有很多相同之處,如果有了解響應式編程相關源碼如UniRx,應該對UE4的渲染線程流程容易理解。

  在這先說下UniRx相應事件流的處理,讓不了解的同學大致有點印象,如當前線程計划,一般首先有個隊列,在相應事件響應后,把相應處理方法填充到隊列中,另一邊則在隊列里,根據先進先出的原則,不斷執行隊列里的方法。說起來比較簡單,主要是這里只拿出UniRx里的一個執行計划的事件流來說,另外的相關響應式編程概念與本文無關,也就不提起來說。

  回到正題,說了UE4渲染流程的設計與上面很多相同,如此,我們先簡單來說明下相關UE4里的類,與上面說的來對應。

  FBaseGraphTask: 上面說到事件流,那么這個類在這,就是事件流里的每個事件。

  TGraphTask: FBaseGraphTask的一個子類模版類,模版類要求有方法DoTask.(注意這里,后面要說。相應在此處簡單理解成C#里的泛型約束,雖然C#直接做不到這點,可以簡接使用泛型約束加接口實現)

  FTaskThreadBase: 簡單來說,這個類里放的是事件流,以及相應處理事件流的一些方法,如

    EnqueueFromThisThread: 壓入事件流中。

    ProcessTasksUntilQuit: 循環執行事件流里方法,直到有要求結束信號。

    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);
        });
ReleaseShaderMap

  首先生成類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

  相應的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 );
        }
View Code

   主要有二個類,FRenderCommandFence與FFrameEndSync,以及每楨結束前的一段代碼,只貼出相應類的聲明,相應實現大家有興趣可以自己去看。

  FRenderCommandFence:同步游戲線程與渲染線程

    BeginFence: 插入一個事件到渲染線程中。

    Wait: 游戲線程等待上面插入的事件已經執行完成,否則游戲線程暫停執行。

  FFrameEndSync:讓游戲線程最多比渲染線程快一楨。

  在RenderingThread.cpp中,我們很容易看下如下代碼。

    // ensure the thread has actually started and is idling
    FRenderCommandFence Fence;
    Fence.BeginFence();
    Fence.Wait();
View Code

  可以看到,因為隊列的先進先出原則,當調用BeginFence時,必然在渲染隊列的最后面,那么wait需要等到整個渲染隊列執行完,游戲線程才能繼續。

  在FEngineLoop::Tick等游戲線程每楨執行完后,必然壓入很多命令到渲染線程中,那么這時調用beginFence的命令必然在隊列最后,如果保持游戲線程與渲染線程同步,只需要是調用前面的beginFence的實例調用wait,這樣游戲線程必需要等到渲染線程執行完才能繼續,如果允許游戲線程比渲染線程快一楨,就是上面FFrameEndSync所做,生成二個FFrameEndSync,第一楨結尾第一個調用beginFence,需要等到第二楨結尾才調用對應實例的wait,這樣就能讓游戲線程比渲染線程快一楨。至於渲染線程比游戲線程快,這個是沒問題的,因為渲染的畫面一直是最新的數據。

  如上就是UE4簡單的渲染流程與同步解決方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM