现在的位置: 首页 > 综合 > 正文

OGRE3D 渲染系统线程化

2013年06月05日 ⁄ 综合 ⁄ 共 5762字 ⁄ 字号 评论关闭

OGRE3D 渲染系统线程化

译:BoYueJiang

http://blog.csdn.net/BoYueJiang

由于BLOG注册不到一周,无法上传图片。所以文中图片下周补上。

本文以及原文已上传到CSDN资源 http://download.csdn.net/source/2537929

译者序:偶然在网上看到这篇文章,自己很想仔细研究一下。但搜寻半天不见中文版。于是自己斗胆翻译了一下。文中不免有漏洞百出,甚至可以说有些地方不及Google翻译得好。但这样总的来说是出了一个中文版,而我自己在翻译过程中也会停下来仔细思考。

OGRE这个线程化的文章很老了。因为OGRE目前已经支持多线程渲染。 这篇文章貌似是某些人研究出来的三个线程化方案,并给出了测试结果。以向OGRE社区证明线程化方案的可行性。 对于许多想研究渲染线程化的人来说,是一篇值得参考的文章。文中提出了许多在不同情况下线程化时遇到的问题,以及需要注意的问题。值得一读/

 

介绍

OGRE3Da.k.a. Ogre)是目前最常用的开放源代码 3D 引擎之一。它是一款功能完善的通用 3D 引擎,可应用于从游戏到科学模拟等多种商用产品。该引擎由来自开放源代码社区的数百名技术人员历经五年时间而开发成功。如欲了解 Ogre 的详细信息,请访问网站 www.ogre3d.org  

然而,尽管 Ogre 功能强大,但是它却在技术上存在一个重要缺憾,那就是它无法在系统中充分利用多个处理器的优势。目前,英特尔已经有多款双核产品上市,而超线程(HT)技术更是在多年前就已应用在英特尔® 奔腾® 4 处理器中。将 Ogre 线程化所实现的性能增益将丝毫不逊色于添加第二枚处理器所实现的性能增益。 

本文将向您介绍将 Ogre 渲染系统线程化的三种不同处理方法,并且我们将根据下文所描述的线程化目的,选择一种方法进行完整实施。

 

线程化目标

当我们在对OGRE线程化时,需要实现以下几个目标:

·通过改动最少的OGRE源码以使OGRE社区接受。

·在双核处理器上,相对于非线程化渲染的OGRE系统来说,提升25%FPS,以平衡应用程序的CPUGPU使用率

·在用户不知道的情况下对SDK包中的DEMO进行必要的变动,而不让用户在画面上有任何察觉。

 

假设

本文提及了OGRE中的许多类,以及使用了OGRE中的许多程序片段。所以,这里假设读者都是对OGRE的源码非常了解或者已经对源码进行过快速的查阅,否则将很难理解这些OGRE的特性。同样,读者也应该对线程的概念有所了解。这里就不再介绍更多关于线程的内容。

 

限制

这里所讨论的所有实现,都没有在超过两个处理器的机器上实验过。因此,这些技术对于双核处理器或双核心系统最适合不过。以后的文章中将会讨论如何在OGRE中创建一个线程队列,使OGRE以及使用OGRE的应用程序都能够在双核处理器以及多核上得到性能的提升。

 

线程化OGRE渲染系统使用到的技术

OGRE中有很多地方都可以被线程化,但线程化最能在多核上提升性能的是渲染系统。渲染系统在OGRE占据了巨大的一块,并且从某种意义上讲,它能单独地被外部程序访问。下面介绍三种将OGRE渲染系统线程化的例子。

1、  OGRE中对于渲染的调用可以被放在他自己的线程中。

2、  一个线程化层可以被放在OgreMain和渲染系统插件之间。

3、  渲染系统插件可以单开一个线程来调用图形API

上面这三种方案都有各自的优缺点,本文将会一一讨论。

 

线程化并不会对每个应用程序有利

注意,线程化只对那种花费在图形调用API和逻辑处理上的时间很接近的应用程序有好处。若其中一个较另一个差别很大,则看不到明显的效果。

 

技术方案1:线程化OgreMain(高级线程化手段)

OgreMain中进行线程化是一种最高级的线程化手段,也表示能获得最高级的潜在性能。这是因为Ogre在做一次渲染的时候,需要做很多事情,并且不仅仅是提交某些命令或数据到显卡。比如,决定哪个摄相机是活动摄相机,遍历场景中所有可见物体,标志所有可见物体去渲染,等等。 下面的插图展示了一个Ogre渲染过程(为了简化起见,一些东西被忽略)。高级线程化将导致这里所有的过程被放置在他自己的线程里。

 

线程化问题

采用这种方案有一个主要的问题就是,两个线程中将会发生代码重叠。打个比方,主线程和渲染线程都需要访问场景中某个场景对象的相同数据。主线程要更新它的位置和方向,然而此时渲染线程要读这些信息或者在渲染线程渲染前主线程修改了这些内容。从而导致渲染帧的内容与实际不符,会相差一帧。特别地,当渲染在渲染一个对象时主线程却要把它删除,就会出问题。如下图所示:

除了刚刚提到的线程自身的问题外,同样也存在处理器共享失败的问题。当一个变量被一个线程更新的时候,这个变量是处于这个线程所在的CPU缓存行中的,而另外一个线程也会访问这个缓存行中的内容的相同数据。由于它们共享缓存行,一个处理器需要清空整个缓存行,不管其它处理器是否做了修改。这就是这个高级线程方方案的问题所在。因为主线程和渲染线程使用同样的类的实例。 由于类体变量被相继地放置在内存中,因此他们要共享同样的缓存行。关于更多缓存行共享失败的问题。可以查看相关文章。

 

避免问题

为了避免上面提到的线程问题,这里提供了两个可以安全访问和更新对象的解决方案。

1、  使用一个更新队列。

2、  复制对象

 

更新队列的方案通过维持一个对象的更新队列来防止访问重叠。见下图。 更新将只发生一次,即当主线程准备让渲染线程开始渲染的时候。当然,你需要等待渲染线程完成后才能进行第二次启用。这个方案有一个缺点,就是单处理系统上的CPU反而会承受这个更新队列的额外负担,而享受不到这个更新队列的好处。另一个缺点就是,当对硬件资源(如索引,顶点缓冲等)改变时。排队改变这些资源将会很困难,因为这些数据都非常大。

 

复制对象的方案从本质上讲,就是为一个经常变动的对象复制副本。在这种方案下,使用OGRE的应用程序将被要求复制一个经常需要更新的对象,因为只有它知道哪些对象是需要经常更新的。应用程序也不得不按照一定的方式来写:对象的处理是在对象被显示后的下一帧。(这点没有太明白,貌似意思是说,对象的处理和渲染为两个帧,一个帧拿来渲染,一个帧拿来处理,看到下面那图应该是这个意思)。当然,如果你的应用程序并不每帧更新对象,这也是一个问题,在这种情况下,你的对象有可能是在几帧后才被访问,这样就会导致冲突。也可以对其做一些优化,如只有对象中主线程和渲染线程要共享的数据才被复制,以此来减少负担。在这样的情况下对象将不再是一个复制品,但是将会有一个双缓冲用于存放你复制的这些东西。

在下图中,注意那个object X 将保持一致性(假设更新速度大于30FPS)。但是object Y将不会,因为在前一帧进行了缩放,但是这个数据并没有被体现在复制对象中。

图里有许多关于同步的东西被我删掉了,但是上面着实能够反应这个技术的实现形式。

 

上面说到的两个技术中,并没有任何一个技术被OGRE社区接受,它们都需要大量修改OGRE代码,因此并未继续。一个需要复制对象数据来控制数据修改的例子便是Frustum::updataView函数和_update函数,对于实现这个函数的所有类,都需要在渲染中被调用,以及OGRE的其它地方(渲染以外的地方)。

 

在哪里进行线程化

OgreMain中进行线程化的一个理想的地方便是在Root::renderOneFrame中。这个函数调用了主渲染_Root::_updateAllRenderTargets,这个函数可以轻易地被封装一次。

下面是一些实现上面想法的示例代码。

_beginthreadex(0,0,Root::renderThreadFuc,0,0,0);

/*static*/ unsigned int Root::renderThreadFunc( )

{

   while(1)

   {

waitForStartRendering( );

_setStartRendering(flase);

_updateAllRenderTargets( );

_setRenderingComplete(true);

   }

}

 

bool Root::renderOneFrame( )

{

 if(!_fireFrameStarted( ) )

return false;

if(mThreadedRendering)

{

     _waitForRenderingComplete( );

  _setRenderingComplete(false);

     _setStartRendering(true);

}

else

{

 _updateAllRenderTargets( );

}

}

 

_wait _set函数演示了操作系统依赖的同步函数调用,例如,WINDOWS版本的_waitForRenderingComplete将会包含一个WaitForSingleObject调用。注意当多线程开启的时候,应该在真正渲染完成之前调用_fireFrameEnded函数。

 

技术方案2:创建一个线程渲染系统层(中级线程化手段)

创建一个线程化的渲染系统层对OGRE渲染系统来说是一个很不显眼的方案。但是由于OGRE渲染系统的复杂性,它也是最困难的方案。渲染层从本质上讲是对渲染系统插件(如D3DOPENGL)的一个封装。想要在OGRE中创建和集成一个这样的附加渲染系统,只需要对OGRE进行较少的改动。但是创建创建一个线程化的渲染系统层,又是另一回事了。

为了创建一个线程化的渲染系统层,你至少需要在OgreMain中实现Ogre::RenderSystem 类和Ogre:RenderWindow类。这两个类仅仅是界于Ogre和实际的渲染器插件之间的一个层。这个层的工作并不是简单的将对插件的函数调用进行封装。需要决定要做哪些什么,以便调用渲染器插件,因为这个方案的目标是将渲染的工作分离到另一个线程中。在实面这些类的函数时,有几事情需要思考。

·习惯性地(如,所有函数调用仅仅是在开始渲染之前调用。)可以仅是对渲染器插件的直接调用,(相当于函数转发)。也可以在包装的同时进行一些必要的初始化。

·在调用渲染器插件进行创建操作时,将需要等待前一帧渲染完成。并且需要一个包装类来包装那个渲染器返回(提供一个已经存在的实例)的与创建相当的类。一个需要包装类的好例子便是渲染器插件返回的RenderWindow类。这个类的实例通过RenderWindow:createRenderWindow创建并返回。

·某些函数需要访问基类。RenderSystemRenderWindow类需要调用一些基类方法来完成一些内部事情。在OGRE中不这样做,会导致不正确的行为。

·渲染用的函数需要被排队,以便享受到多线程的好处。渲染线程将会遍历那个队列,并按顺序调用那些函数。 RenderWindow中的swapBuffers函数是一个例外。它的包装函数既要加入渲染队列管理,但它又是向渲染线程发出信号,执行渲染队列中的函数的地方。

上面提到的几点描述了实现这个方案需要做的事情,也还有其它一些小问题需要考虑,并且一些问题需要在实现的时候处理。

 

线程化的问题

这个实现和“方案1:高级线程化方案”一样,存在同样的问题。除开这种情况,最好的解决方案就是复制需要共享的数据。因为这个中间层不拥有Ogre中的类。也有其它一些从“低级线程化方案”中借鉴而来的解决办法来完成这个实现,从而对显卡资源提供一个线程安全访问手段。

 

在哪里线程化

正如先前提到的,这将是线程化Ogre渲染系统的一个最不显眼的方案。所需要对Ogre做的轻微改变仅是对Ogre现有的渲染系统增加一个线程化的渲染系统层。像这样

 

void Root::AddRenderSystem(RenderSystem *newRender)

{

   mRenderers.push_back(newRender);

#ifdef  __THEARDRENDERSYSTEM__

   if(mThreadRenderSystem)

   {

      RenderSystem* newTRender =

                  new ThreadedRenderSystem(newRender);

      if(newTRender != NULL)

            mRenderers.push_back( newTRender);

   }

#endif

}

 

技术方案3:线程化渲染插件(低级线程化手段)

线程化一个特定的渲染插件带来了最低级的适应性,因为它和特定的技术(如D3DOPENGL)绑定起来。但是它也使你能够最大限度地操纵硬件资源。

实现这个方案最干净利落的办法就是创建一个介于API和渲染插件之间的层,用来处理线程化。(如下图)

 

在这种方法下,它仅仅是用你的层来替换API接口。并且每个一调用从渲染插件的调用都是线程安全的,因为你的这个层处理了所有的调用。为D3D做这个,仅仅是用你自己的包装类和方法替换了IDirect3D的接口。对于OPENGL,你将移除所有的OPENGL头文件,并用你自己包装好后的头文件替换掉它们。有可能你需要用一个命名空间来包装OPENGL函数,以免出现名词冲突。

这个方案依然和“中级线程化手段”一样需要考虑些线程化相关的东西。

·初始化不需要多线程

·创建函数以及一些get函数需要在渲染完一帧前等待。

·渲染命令,和伴随参数将需要加入队列中。

 

关于索引和顶点缓冲的加锁

对于所有的方案,都存在一个使用硬件资源时的潜在线程问题。对索引顶点缓冲区的加锁就是一个很大的挑战,因为某些应用程序在执行的时候总是会反复对这些缓冲区进行加锁和解锁。比如对于动态缓冲区,总是会每帧都改变它的内容来实现画面的变化。一些应用程序也会重用缓冲区以使多个对象每帧都能够共享同样的顶点和索引缓冲区。为了解决这个问题,我们需要缓冲这些缓冲区。然而,可以通过很多方法来实现,下面有两种缓冲办法:

1、  部分缓冲区加锁,这是一个类似于双缓冲的缓冲技术。我们将在显卡中创建两个缓冲区,而不是一个

抱歉!评论已关闭.