第五日
当前位置 osgViewer/Viewer.cpp463,osgViewer::Viewer::realize()
下面我们再次遍历所有GraphicsContext设备,对于每个GraphicsContext指针gc,判断它是否为GraphicsWindow对象,并执行GraphicsWindow::grabFocusIfPointerInWindow函数。阅读GraphicsWindowWin32类(即GraphicsContext的具体实现者)的同名函数可以发现,这个函数不过是负责把鼠标焦点转到当前窗口上而已。
下一步工作的代码如下:
osg::Timer::instance()->setStartTick();
setStartTick(osg::Timer::instance()->getStartTick());
setUpThreading();
首先调用osg::Timer::setStartTick函数,启动OSG内部定时器并开始计时。
随后,Viewer::setStartTick函数的工作是找到当前视景器和所有GraphicsContext设备的事件队列_eventQueue,并设定它们的启动时刻为当前时间。
下一行是调用ViewerBase::setUpThreading函数……设置线程,对于一向以多线程渲染而闻名的OSG而言,这一定是个值得深究的话题。
当前位置 osgViewer/ViewerBase.cpp122osgViewer::ViewerBase:: setUpThreading()
OSG的视景器包括四种线程模型,可以使用setThreadingModel进行设置,不同的线程模型在仿真循环运行时将表现出不同的渲染效率和线程控制特性。通常而言,这四种线程的特性如下:
SingleThreaded:单线程模型。OSG不会创建任何新线程来完成场景的筛选和渲染,因而也不会对渲染效率的提高有任何助益。它适合任何配置下使用。
CullDrawThreadPerContext:OSG将为每一个图形设备上下文(GraphicsContext)创建一个图形线程,以实现并行的渲染工作。如果有多个CPU的话,那么系统将尝试把线程分别放在不同的CPU上运行,不过每一帧结束前都会强制同步所有的线程。
DrawThreadPerContext:这一线程模型同样会为每个GraphicsContext创建线程,并分配到不同的CPU上。十分值得注意的是,这种模式会在当前帧的所有线程完成工作之前,开始下一帧。
CullThreadPerCameraDrawThreadPerContext:这一线程模型将为每个GraphicsContext和每个摄像机创建线程,这种模式同样不会等待前一次的渲染结束,而是返回仿真循环并再次开始执行frame函数。如果您使用四核甚至更高的系统配置,那么使用这一线程模型将最大限度地发挥多CPU的处理能力。
与DrawThreadPerContext和CullThreadPerCameraDrawThreadPerContext这两种同样可以用于多CPU系统,且相对更有效率的线程模型相比,CullDrawThreadPerContext的应用范围比较有限;而SingleThreaded模式在单核以及配置较低的系统上运行稳定。
这些话长篇大论地说出来似乎令人满腹疑窦:OSG为什么要为每个GraphicsContext设备配置一个线程?为什么又要为每个摄像机配置一个线程?线程的同步是怎么实现的?线程与CPU的关系又是怎么处理的?OSG入门书籍中常说的更新(Update)/筛选(Cull)/绘制(Draw)三线程又是在那里体现的?为什么……
天哪,这么多问题我们都要解读吗?是的,绝对要解读,不管花费多少时间!OSG的学习是为了实际的应用,但是只有真正理解了它的运行机制,才能够最有效地把这个愈加著名的实时场景渲染软件用好。
但是有些事情是急不来的,从frame函数的源代码中可以大致推测出来,场景的筛选和绘制工作是由ViewerBase::renderingTraversals函数来完成的。相应的,很多线程的调度和同步工作也是在这个函数中完成的,那么就让我们把问题留到那个时候吧。不过不妨先透露一点信息:第四日中我们提到的渲染器(Renderer)类,事实上也是与OSG的渲染线程密切相关的,因为筛选和绘制的工作就是由它来具体负责!好的,遗留的问题可以说暂时得到了解答,不过新的问题又出现了,而且任务看起来更为艰巨,继续努力好了。
线程相关的问题留待后面解决,不过还是让我们先通读一下setUpThreading函数的代码也无妨。它的工作主要是处理单线程(SingleThreaded)模式下,多处理器系统的数据线程分配方式。
听起来很深奥,不过实际上这里没有多么复杂。在现阶段,如果采用单线程模式的话,OSG系统将使用CPU0来处理用户更新、筛选和渲染等一切事务,而使用CPU1来处理场景的两个分页数据库(DatabasePager)线程(它们分别用于处理本地和网络上的场景数据)。
这里还出现了一个Viewer::getScenes函数(osgViewer/Viewer.cpp,141行),它的作用是获取当前视景器对应的osgViewer::Scene对象,也就是场景。一个场景包括了唯一的场景图形根节点,分页数据库(DatabasePager),以及分页图像库(ImagePager)。Viewer视景器对象通常只包括一个Scene场景,而CompositeViewer复合视景器则可能包括多个场景对象.
如果系统采用了SingleThreaded之外的其它线程模型,那么setUpThreading函数将自动执行ViewerBase::startThreading——多线程渲染的最重要函数之一,这个函数将在我们追踪到renderingTraversals函数的时候重新进行解析。
当前位置 osgViewer/Viewer.cpp486osgViewer::Viewer::realize()
好了,如果您还没有忘记我们来自何方的话,请回到realize函数,现在这个函数的执行已经接近了尾声,不过我们又遇到了一个问题:编译上下文(也就是Compile Contexts,暂时就这样翻译吧)?如果要启用它的话并不困难,只需要在调用realize之前执行:
osg::DisplaySettings::instance()->setCompileContextsHint(true);
随后,正如您在realize函数的491-503行之间看到的,系统将设法遍历所有可能的GraphicsContext设备,针对它们分别再各自添加一个新的GraphicsContext设备(也就是说,如果系统中已经有了数个图形上下文,那么现在又将新增同样数量的图形上下文与之对应),所用的函数为GraphicsContext::getOrCreateCompileContext。这之后,分别执行了创建图形线程,设置CPU依赖性,以及启动图形线程的工作,具体的实现内容可以暂时忽略。
观察getOrCreateCompileContext函数的内容,很快我们就可以发现其中的重点:这些新增的GraphicsContext对象使用了pBuffer的特性,并与对应的已有对象共享同一个图形上下文(Traits::sharedContext特性)。事实上,这是OSG利用OpenGL的像素缓存(Pixel Buffer)技术,为图形上下文的后台编译提供的一种新的解决方案。这样不仅可以提高图形刷新的速度,还可以方便用户为某一特定的GraphicsContext设备添加特殊的处理动作,方法是使用osg::GraphicsContext::getCompileContext获取后台图形上下文,再使用GraphicsContext::add函数向其中追加osg::Operation对象,类似的例子可以参看osgterrain。
对了,在结束这一日的旅途之前,还要提示一句:“编译上下文”这一功能在Windows的实现尚有问题,目前可能会造成系统的崩溃(不要大失所望呀^_^)。
解读成果
线程模型,osgViewer::Viewer::realize。
悬疑列表
类变量_cameraWithFocus的意义是什么?如何调度和实现OSG的多线程机