本片文章作为介绍UE4渲染框架的第一篇文章,主要是介绍一下UE4主线程如何与渲染线程同步,这一点对于理解UE4的渲染框架是很有用的,这篇文章所使用的UE4版本为UE4.22.1。
在UE4中,主线程和渲染线程的任务是相对独立的,主线程处理游戏逻辑相关的任务,渲染线程处理渲染相关的任务。在UE4中渲染线程的任务主要包括视锥剔除、遮挡剔除、生成MeshDrawCommand、根据材质属性将其分发到不同的MeshPass、设置管线状态和数据、启动管线等任务。虽然渲染线程与主线程相对独立,但是渲染线程并不是完全独立的,它需要和主线程进行同步,接收来自主线程的任务。
由于UE4线程管理是通过TaskGraph的,自然主线程和渲染线程的同步也是通过TaskGraph,当然本篇文章的目的也不是介绍TaskGraph,所以如果使用到了TaskGraph的时候会稍微提一下,以后有时间的话争取整理一下TaskGraph相关的内容。
为了理解渲染线程与主线程的同步,我们可以先看一下TaskGraph的Benchmark代码中看看UE4是怎么做多线程同步的。下图中创建了10个Task,放在数据Tasks里面,其中的每一个Task都会创建100个InnerTask。需要注意的是每个Task执行之前都需要它的100个InnerTask全部执行完毕。需要注意的是红色方框里面的代码,它重新创建了一个Task命名为Join,将Tasks作为自己的前缀,然后等待Join任务执行完毕,这样只要Join执行完毕了,所以的任务就执行完毕了,这样就完成同步了。
通过上面的分析可以看到UE4中线程的同步可以创建一个FNullGraphTask,然后将之前的线程作为该线程的前置条件,然后等待该线程完成就可以了,接下来我们进入正题:
首先在Tick函数快结束之前调用FrameEndSync.Sync函数,在看Sync函数之前我们先看一下FFrameEndSync类的定义:
FFrameEndSync类中的Sync函数是通过FRenderCommandFence完成的,在FRenderCommandFence::BeginFence()函数中创建一个任务CompletionEvent。
在Wait函数中,先判断任务是否已经完成了,如果没有则执行GameThreadWaitForTask
在GameThreadWaitForTask函数中会创建一个Event作为等待Task执行结束的事件,函数后面的部分会等待该事件结束,然后返回。
通过上面的代码,我们已经了解到了Fence的BeginFence函数主要是创建了一个任务,然后加入到渲染线程的任务队列中;在Wait函数中等待该事件触发。
这里同步的逻辑是:往渲染线程的任务队列里面加入一个空任务,然后等待该任务结束,等到该任务结束的时候也就说明当前渲染线程已经处理完了之前加入的任务了。
至此,渲染线程和主线程的同步就完成了。