array 发表于 2008-9-24 13:51:54

OSG原创教程:最长的一帧(29)

当前位置:osg/ OperationThread.cpp第382行,osg:: OperationThread::run ()
首先要注意的是,当我们使用GraphicsContext::createGraphicsThread创建线程时,得到的是一个osg::GraphicsThread线程对象;而使用Camera::createCameraThread创建线程时,得到的是osg::OperationThread对象,它是GraphicsThread的父类。

这一微小的区别使得这两类线程之间不会存在太大的差异。事实上,图形设备所用的GraphicsThread线程只是在每次运行时(即GraphicsThread::run函数)保证设备的渲染上下文RC设置正确,即,在恰当的时机使用GraphicsContext::makeCurrent和releaseContext函数操作RC设备,并在RC设备正确关联之后执行OperationThread::run函数。

那么我们现在就来看一下OperationThread线程的执行内容,它事实上也是GC线程和摄像机线程在启动以后要反复完成的工作。流程如下:

1、取得任务队列(OperationQueue),注意这里要使用Mutex互斥锁,避免用户追加任务时与线程的执行产生冲突。

2、获取任务队列中的一个任务(OperationQueue::getNextOperation)。这个函数看似简单,只要从std::list列表中取出一个osg:: Operation对象就可以了。但是其中还是有诸多的注意事项:

首先,如果任务列表是空的,渲染线程将选择暂时阻塞自己(使用block函数),直到有新的Operation操作加入到队列中为止。

其次,我们有一个任务列表迭代器_currentOperationIterator,如果这个迭代器已经到达列表的末尾,则自动将其转至列表首部,这样就可以在线程中循环执行任务列表中的内容。
如果迭代器取得了一个Operation操作任务,那么我们需要判断这个任务是否将被反复执行,即,迭代器转至任务列表首部之后,是否还可以取得这个任务。判断所用的函数是Operation::getKeep,这个函数返回true时,任务将允许反复执行(例如场景筛选和绘制的任务),否则任务将被随即从列表中移除,我们也不会再取得这个Operation对象(除非再次将其加入列表)。

3、现在我们得到了一个Operation对象,那么“悬疑列表”中的问题也就迎刃而解了:Operation对象在线程中的应用时机就在此处了。线程运行中将执行Operation:: operator()操作,并将当前的GC设备或者摄像机作为传入参数传入。

那么下面我们将关注这样一个问题:到底有哪些操作会被传入渲染线程的任务列表呢?这些传入的Operation操作又分别起到什么作用呢?

当前位置:osgViewer/ViewerBase.cpp第361行,osgViewer::ViewerBase::startThreading()
向任务列表传入新的Operation任务的函数为OperationThread::add,如果需要的话,我们也可以向GC线程或者摄像机线程传递自己定义的任务,只是对于并不熟悉线程编程的开发者而言,这一操作极具危险性。

在第十三日我们曾提到过DatabasePager::CompileOperation这个操作任务,它是在数据分页线程的执行过程中,从DatabaseThread::run传递给对应的GC线程的。而它的执行函数operator()的内容也很简单:无非是执行DatabasePager::compileAllGLObjects函数,对当前GC设备中待编译的对象执行预编译(使用compileGLObjects函数),第十八日中曾对这一过程作出过介绍,此处不再重复。

注意这个CompileOperation的getKeep属性为false,因此执行完一次之后,它将被剔除出GC线程的任务列表。

除此之外,GC线程的主要工作任务设置都是在startThreading这个函数中完成的。而这个startThreading将在Viewer::realize函数中执行,因此我们尽量不要在执行realize之后再作多余的工作,因为此时渲染线程已经启动了。

在startThread函数中,根据线程模型的不同,以下几种任务对象将可能被添加到GC线程或者摄像机线程中:

osg::BarrierOperation:事实上也就是前文中反复提到的启动栅栏_startRenderingBarrier和结束栅栏_endRenderingDispatchBarrier,它们同时也可以作为任务对象被添加到线程中,这就使得线程的同步控制变得十分方便:只要任务队列执行到启动栅栏或者结束栅栏,就自动使用block阻塞线程,直到栅栏被冲开(也就是全部线程都被阻塞的那一刻),才会继续执行后面的任务。

osg::RunOperations:这个任务将负责执行GraphicsContext::runOperations函数,而就像我们在第十七日中介绍的那样,runOperations函数将通过执行渲染器的Renderer:: operator()操作,完成场景的绘制(或者筛选加上绘制,Renderer::cull_draw)工作。

osg::SwapBuffersOperation:这个任务将负责执行双缓存交换的动作,以实现场景的平滑浏览,相关函数为GraphicsContext::swapBuffersImplementation。

osgViewer::Renderer:没错,渲染器本身也是可以作为一个任务存在的,它将根据相应的设置直接执行场景的绘制(Renderer::draw)或者筛选加绘制(Renderer::cull_draw)操作。

那么,这几种线程模型,在任务的添加和处理顺序上各自有什么要求呢?那么就从CullDrawThreadPerContext开始:

CullDrawThreadPerContext模式的渲染开始栅栏和结束栅栏的强度都设为contexts()+1,即GC线程的数目加一。而任务列表中任务的顺序为:

[*]1、_startRenderingBarrier任务(startThreading函数,380行)。由于getKeep()设置为true,这个任务不会从列表中删除。
[*]2、osg::RunOperations任务(393行),它将同时负责场景的筛选和绘制(即最终执行Renderer::cull_draw函数)。该任务不会从列表中删除。
[*]3、swapReadyBarrier任务(401行),它实质上是一个BarrierOperation栅栏对象,强度等于GC线程的数目。它的作用是在交换双缓存之前对所有GC线程执行一次同步。该任务不会从列表中删除。
[*]4、swapOp任务(404行),它是一个SwapBuffersOperation对象,用于执行绘制后的双缓存交换操作。该任务不会从列表中删除。
[*]5、_endRenderingDispatchBarrier任务(409行),显然这就是渲染结束栅栏的位置。该任务不会从列表中删除。


由此我们可以判断出,对于CullDrawThreadPerContext模式:它的渲染过程中将三次对各个GC线程进行同步,分别是场景筛选和绘制开始前,场景筛选和绘制完毕后,以及双缓存交换完毕后;其它时候则交由各个GC线程自由完成针对各个GC设备的场景渲染工作。

下面的示例图中,三个GC线程分别绘制到三个不同的图形设备,由于场景筛选所需时间以及渲染数据量等种种差异,三个线程完成筛选(CULL)和绘制(DRAW)的时间都不相同,但是CullDrawThreadPerContext将保证它们在运行时的同步性。
附图1

图中三个用于同步线程的栅栏,依次分别是_startRenderingBarrier,swapReadyBarrier和_endRenderingDispatchBarrier(从左至右)。

下一日我们将接着分析另外两种线程模型的任务列表及其执行流程。

解读成果:
OperationThread,CullDrawThreadPerContext模式。
悬疑列表:
无。

[ 本帖最后由 array 于 2008-9-24 13:52 编辑 ]

shengrendan2 发表于 2008-9-28 13:44:51

:call:
页: [1]
查看完整版本: OSG原创教程:最长的一帧(29)