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[];
/** 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() != );
}
主要有二个类,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简单的渲染流程与同步解决方法。
UE4里的渲染线程的更多相关文章
- (转\整)UE4游戏优化  多人大地型游戏的优化(二)渲染线程的优化
		
施主分享随缘,评论随心,@author:白袍小道 小道暗语: 1.因为小道这里博客目录没自己整,暂时就用随笔目录结构,所以二级目录那啥就忽略了.标题格式大致都是(原or转) 二级目录 (标题) 2.因 ...
 - UE4实现风格化渲染(一):UserNormalTranslator工具的使用
		
最近会在UE4上实现风格化渲染的需求,所以也借机写一下相关的制作教程.对应日系风格化渲染技法来说,关键还是法线的处理. 法线处理以前翻译的资料很多了,所以也不多做解释了,比如下图是最新的UE4上的 ...
 - UE4-PS4开发渲染线程优化方法及记录
		
先说方法: Launch 到 PS4 Devkit上,在PS4上输入Stat unit 看瓶颈在哪里.我们发现Frame 和Draw数值几乎一样,其余两项相对较小,这表明瓶颈在渲染线程上. 关于渲染线 ...
 - Libgdx多线程与渲染线程
		
http://www.leestorm.com/post/115.html ——————————————————————————————————————————————————————————‘ 大部 ...
 - (转)electron主线程中通过mainWindow.webContents.send发送事件,渲染线程接收不到
		
转自 https://segmentfault.com/q/1010000015599245/ 准备实现的功能: 页面1(渲染进程1)中点击按钮,发送事件给到主进程.主进程成功接收事件后,通过main ...
 - UE4里的自定义深度功能
		
转自:http://www.52vr.com/article-1866-1.html 随着物理渲染系统的发布,虚幻引擎4同时引进了一个新的深度缓存功能,它叫作“自定义深度”,可以用于诸如编辑器里的选择 ...
 - 主线程MainThread与渲染线程RenderThread
		
在Android 5.0之前,Android应用程序的主线程同时也是一个Open GL线程.但是从Android 5.0之后,Android应用程序的Open GL线程就独立出来了,称为Render ...
 - 翻译 Tri-Ace:在Shader里近似渲染公式
		
继上一篇:次世代基于物理渲染的反射模型,本篇是Tri-Ace 在cedec2014上最近发布的, 主要内容如名称所示,解释了他们在实现基于物理渲染时,对shader的渲染公式所做的近似工作. ...
 - Java 里如何实现线程间通信(转载)
		
出处:http://www.importnew.com/26850.html 正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程 ...
 
随机推荐
- HDU 5678 ztr loves trees
			
这题也是一眼标算..... 先搞一次dfs,把树转换成序列,对每个节点看子树的中位数,也就是看某段区间的中位数,这样就可以主席树求区间第k大值解决. 注意:询问的次数有1000000次,每次去询问会T ...
 - HUST 1027 Enemy Target!
			
求二分图的最小点覆盖集,并输出 对于每一个a[i][j]=1,我们从行i-->列j建立一条边 显然,这张图是一张二分图.左边的节点代表删除哪一行,右边的节点代表删除哪一列.中间的边代表所有a[i ...
 - 现在都是python 单独开发框架 执行脚本,处理结果,发报告之类的
			
现在都是python 单独开发框架 执行脚本,处理结果,发报告之类的
 - 在mac本上删除mysql
			
The steps: First you need to edit the file in: /etc/hostconfig and remove the line Since this is a s ...
 - 比较全的JavaScript倒计时脚本
			
JavaScript倒计时在Web中用得非常广泛,比如常见的团购啊.还有什么值得期待的事情,都可以用到倒计时.现在举了四个例子,比如时间长的倒计时,小时倒计时,最简的倒计时,还有秒表等等,应该可以满足 ...
 - Android面试题随笔1
			
1.如何让一个应用在手机上产生两个或多个图标? 在清单文件中的activity节点下配置如下:[5,7行代码] <activity android:name=".MainActivit ...
 - android中广播的使用
			
广播消息机制用于进行系统级别的消息通知,每个应用程序可以对感兴趣的广播进行注册,并且将接收广播的方法定义在广播接收器中(Broadcast). 广播可以分为标准广播和有序广播. 注册广播的方法可以动态 ...
 - 常见div+css网页布局(float,absolute)
			
网页布局-常见 1, float布局 (1)常规方法 <div id="warp"> <div id="column&quo ...
 - S3C2440的定时器详解
			
还包含用于大电流驱动的死区发生器 位预分频器是可编程的,并且按存储在TCFG0和TCFG1寄存器中的加载值来分频PCLK 位递减计数器.当递减计数器到达零时,产生定时器中断请求通知CPU定时器操作已经 ...
 - sql2005数据库置疑修复断电崩溃索引损坏  数据库索引错误修复/数据库表损坏/索引损坏/系统表混乱等问题修复
			
sql2005数据库置疑修复断电崩溃索引损坏 数据库索引错误修复/数据库表损坏/索引损坏/系统表混乱等问题修复 客 户 名 称 济南某电子商务公司 数 据 类 型 SQL2005数据库 故 障 检 测 ...