UE4里的渲染线程

  记的上次看过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);
        });
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简单的渲染流程与同步解决方法。

相关文章
相关标签/搜索