~~
先前研究渲染系統(tǒng)線程化的時候翻到了這篇文章,于是一邊看一邊寫出了漢語。 文中寫多地方翻譯得很不通順,見諒。
譯者序:偶然在網(wǎng)上看到這篇文章,自己很想仔細研究一下。但搜尋半天不見中文版。于是自己斗膽翻譯了一下。文中不免有漏洞百出,甚至可以說有些地方不及Google翻譯得好。但這樣總的來說是出了一個中文版,而我自己在翻譯過程中也會停下來仔細思考。
OGRE這個線程化的文章很老了。因為OGRE目前已經(jīng)支持多線程渲染。 這篇文章貌似是某些人研究出來的三個線程化方案,并給出了測試結(jié)果。以向OGRE社區(qū)證明線程化方案的可行性。 對于許多想研究渲染線程化的人來說,是一篇值得參考的文章。文中提出了許多在不同情況下線程化時遇到的問題,以及需要注意的問題。值得一讀/
介紹
OGRE3D(a.k.a. Ogre)是目前最常用的開放源代碼 3D 引擎之一。它是一款功能完善的通用 3D 引擎,可應(yīng)用于從游戲到科學模擬等多種商用產(chǎn)品。該引擎由來自開放源代碼社區(qū)的數(shù)百名技術(shù)人員歷經(jīng)五年時間而開發(fā)成功。如欲了解 Ogre 的詳細信息,請訪問網(wǎng)站 www.ogre3d.org 。
然而,盡管 Ogre 功能強大,但是它卻在技術(shù)上存在一個重要缺憾,那就是它無法在系統(tǒng)中充分利用多個處理器的優(yōu)勢。目前,英特爾已經(jīng)有多款雙核產(chǎn)品上市,而超線程(HT)技術(shù)更是在多年前就已應(yīng)用在英特爾® 奔騰® 4 處理器中。將 Ogre 線程化所實現(xiàn)的性能增益將絲毫不遜色于添加第二枚處理器所實現(xiàn)的性能增益。
本文將向您介紹將 Ogre 渲染系統(tǒng)線程化的三種不同處理方法,并且我們將根據(jù)下文所描述的線程化目的,選擇一種方法進行完整實施。
線程化目標
當我們在對OGRE線程化時,需要實現(xiàn)以下幾個目標:
·通過改動最少的OGRE源碼以使OGRE社區(qū)接受。
·在雙核處理器上,相對于非線程化渲染的OGRE系統(tǒng)來說,提升25%的FPS,以平衡應(yīng)用程序的CPU和GPU使用率
·在用戶不知道的情況下對SDK包中的DEMO進行必要的變動,而不讓用戶在畫面上有任何察覺。
假設(shè)
本文提及了OGRE中的許多類,以及使用了OGRE中的許多程序片段。所以,這里假設(shè)讀者都是對OGRE的源碼非常了解或者已經(jīng)對源碼進行過快速的查閱,否則將很難理解這些OGRE的特性。同樣,讀者也應(yīng)該對線程的概念有所了解。這里就不再介紹更多關(guān)于線程的內(nèi)容。
限制
這里所討論的所有實現(xiàn),都沒有在超過兩個處理器的機器上實驗過。因此,這些技術(shù)對于雙核處理器或雙核心系統(tǒng)最適合不過。以后的文章中將會討論如何在OGRE中創(chuàng)建一個線程隊列,使OGRE以及使用OGRE的應(yīng)用程序都能夠在雙核處理器以及多核上得到性能的提升。
線程化OGRE渲染系統(tǒng)使用到的技術(shù)
OGRE中有很多地方都可以被線程化,但線程化最能在多核上提升性能的是渲染系統(tǒng)。渲染系統(tǒng)在OGRE占據(jù)了巨大的一塊,并且從某種意義上講,它能單獨地被外部程序訪問。下面介紹三種將OGRE渲染系統(tǒng)線程化的例子。
1、 在OGRE中對于渲染的調(diào)用可以被放在他自己的線程中。
2、 一個線程化層可以被放在OgreMain和渲染系統(tǒng)插件之間。
3、 渲染系統(tǒng)插件可以單開一個線程來調(diào)用圖形API。
上面這三種方案都有各自的優(yōu)缺點,本文將會一一討論。
線程化并不會對每個應(yīng)用程序有利
注意,線程化只對那種花費在圖形調(diào)用API和邏輯處理上的時間很接近的應(yīng)用程序有好處。若其中一個較另一個差別很大,則看不到明顯的效果。
技術(shù)方案1:線程化OgreMain(高級線程化手段)
在OgreMain中進行線程化是一種最高級的線程化手段,也表示能獲得最高級的潛在性能。這是因為Ogre在做一次渲染的時候,需要做很多事情,并且不僅僅是提交某些命令或數(shù)據(jù)到顯卡。比如,決定哪個攝相機是活動攝相機,遍歷場景中所有可見物體,標志所有可見物體去渲染,等等。 下面的插圖展示了一個Ogre渲染過程(為了簡化起見,一些東西被忽略)。高級線程化將導致這里所有的過程被放置在他自己的線程里。
線程化問題
采用這種方案有一個主要的問題就是,兩個線程中將會發(fā)生代碼重疊。打個比方,主線程和渲染線程都需要訪問場景中某個場景對象的相同數(shù)據(jù)。主線程要更新它的位置和方向,然而此時渲染線程要讀這些信息或者在渲染線程渲染前主線程修改了這些內(nèi)容。從而導致渲染幀的內(nèi)容與實際不符,會相差一幀。特別地,當渲染在渲染一個對象時主線程卻要把它刪除,就會出問題。如下圖所示:
除了剛剛提到的線程自身的問題外,同樣也存在處理器共享失敗的問題。當一個變量被一個線程更新的時候,這個變量是處于這個線程所在的CPU緩存行中的,而另外一個線程也會訪問這個緩存行中的內(nèi)容的相同數(shù)據(jù)。由于它們共享緩存行,一個處理器需要清空整個緩存行,不管其它處理器是否做了修改。這就是這個高級線程方方案的問題所在。因為主線程和渲染線程使用同樣的類的實例。 由于類體變量被相繼地放置在內(nèi)存中,因此他們要共享同樣的緩存行。關(guān)于更多緩存行共享失敗的問題。可以查看相關(guān)文章。
避免問題
為了避免上面提到的線程問題,這里提供了兩個可以安全訪問和更新對象的解決方案。
1、 使用一個更新隊列。
2、 復制對象
更新隊列的方案通過維持一個對象的更新隊列來防止訪問重疊。見下圖。 更新將只發(fā)生一次,即當主線程準備讓渲染線程開始渲染的時候。當然,你需要等待渲染線程完成后才能進行第二次啟用。這個方案有一個缺點,就是單處理系統(tǒng)上的CPU反而會承受這個更新隊列的額外負擔,而享受不到這個更新隊列的好處。另一個缺點就是,當對硬件資源(如索引,頂點緩沖等)改變時。排隊改變這些資源將會很困難,因為這些數(shù)據(jù)都非常大。
復制對象的方案從本質(zhì)上講,就是為一個經(jīng)常變動的對象復制副本。在這種方案下,使用OGRE的應(yīng)用程序?qū)⒈灰髲椭埔粋€經(jīng)常需要更新的對象,因為只有它知道哪些對象是需要經(jīng)常更新的。應(yīng)用程序也不得不按照一定的方式來寫:對象的處理是在對象被顯示后的下一幀。(這點沒有太明白,貌似意思是說,對象的處理和渲染為兩個幀,一個幀拿來渲染,一個幀拿來處理,看到下面那圖應(yīng)該是這個意思)。當然,如果你的應(yīng)用程序并不每幀更新對象,這也是一個問題,在這種情況下,你的對象有可能是在幾幀后才被訪問,這樣就會導致沖突。也可以對其做一些優(yōu)化,如只有對象中主線程和渲染線程要共享的數(shù)據(jù)才被復制,以此來減少負擔。在這樣的情況下對象將不再是一個復制品,但是將會有一個雙緩沖用于存放你復制的這些東西。
在下圖中,注意那個object X 將保持一致性(假設(shè)更新速度大于30FPS)。但是object Y將不會,因為在前一幀進行了縮放,但是這個數(shù)據(jù)并沒有被體現(xiàn)在復制對象中。
圖里有許多關(guān)于同步的東西被我刪掉了,但是上面著實能夠反應(yīng)這個技術(shù)的實現(xiàn)形式。
上面說到的兩個技術(shù)中,并沒有任何一個技術(shù)被OGRE社區(qū)接受,它們都需要大量修改OGRE代碼,因此并未繼續(xù)。一個需要復制對象數(shù)據(jù)來控制數(shù)據(jù)修改的例子便是Frustum::updataView函數(shù)和_update函數(shù),對于實現(xiàn)這個函數(shù)的所有類,都需要在渲染中被調(diào)用,以及OGRE的其它地方(渲染以外的地方)。
在哪里進行線程化
在OgreMain中進行線程化的一個理想的地方便是在Root::renderOneFrame中。這個函數(shù)調(diào)用了主渲染_Root::_updateAllRenderTargets,這個函數(shù)可以輕易地被封裝一次。
下面是一些實現(xiàn)上面想法的示例代碼。
_wait 和_set函數(shù)演示了操作系統(tǒng)依賴的同步函數(shù)調(diào)用,例如,WINDOWS版本的_waitForRenderingComplete將會包含一個WaitForSingleObject調(diào)用。注意當多線程開啟的時候,應(yīng)該在真正渲染完成之前調(diào)用_fireFrameEnded函數(shù)。
技術(shù)方案2:創(chuàng)建一個線程渲染系統(tǒng)層(中級線程化手段)
創(chuàng)建一個線程化的渲染系統(tǒng)層對OGRE渲染系統(tǒng)來說是一個很不顯眼的方案。但是由于OGRE渲染系統(tǒng)的復雜性,它也是最困難的方案。渲染層從本質(zhì)上講是對渲染系統(tǒng)插件(如D3D,OPENGL)的一個封裝。想要在OGRE中創(chuàng)建和集成一個這樣的附加渲染系統(tǒng),只需要對OGRE進行較少的改動。但是創(chuàng)建創(chuàng)建一個線程化的渲染系統(tǒng)層,又是另一回事了。
為了創(chuàng)建一個線程化的渲染系統(tǒng)層,你至少需要在OgreMain中實現(xiàn)Ogre::RenderSystem 類和Ogre:RenderWindow類。這兩個類僅僅是界于Ogre和實際的渲染器插件之間的一個層。這個層的工作并不是簡單的將對插件的函數(shù)調(diào)用進行封裝。需要決定要做哪些什么,以便調(diào)用渲染器插件,因為這個方案的目標是將渲染的工作分離到另一個線程中。在實面這些類的函數(shù)時,有幾事情需要思考。
·習慣性地(如,所有函數(shù)調(diào)用僅僅是在開始渲染之前調(diào)用。)可以僅是對渲染器插件的直接調(diào)用,(相當于函數(shù)轉(zhuǎn)發(fā))。也可以在包裝的同時進行一些必要的初始化。
·在調(diào)用渲染器插件進行創(chuàng)建操作時,將需要等待前一幀渲染完成。并且需要一個包裝類來包裝那個渲染器返回(提供一個已經(jīng)存在的實例)的與創(chuàng)建相當?shù)念?。一個需要包裝類的好例子便是渲染器插件返回的RenderWindow類。這個類的實例通過RenderWindow:createRenderWindow創(chuàng)建并返回。
·某些函數(shù)需要訪問基類。RenderSystem和RenderWindow類需要調(diào)用一些基類方法來完成一些內(nèi)部事情。在OGRE中不這樣做,會導致不正確的行為。
·渲染用的函數(shù)需要被排隊,以便享受到多線程的好處。渲染線程將會遍歷那個隊列,并按順序調(diào)用那些函數(shù)。 RenderWindow中的swapBuffers函數(shù)是一個例外。它的包裝函數(shù)既要加入渲染隊列管理,但它又是向渲染線程發(fā)出信號,執(zhí)行渲染隊列中的函數(shù)的地方。
上面提到的幾點描述了實現(xiàn)這個方案需要做的事情,也還有其它一些小問題需要考慮,并且一些問題需要在實現(xiàn)的時候處理。
線程化的問題
這個實現(xiàn)和“方案1:高級線程化方案”一樣,存在同樣的問題。除開這種情況,最好的解決方案就是復制需要共享的數(shù)據(jù)。因為這個中間層不擁有Ogre中的類。也有其它一些從“低級線程化方案”中借鑒而來的解決辦法來完成這個實現(xiàn),從而對顯卡資源提供一個線程安全訪問手段。
在哪里線程化
正如先前提到的,這將是線程化Ogre渲染系統(tǒng)的一個最不顯眼的方案。所需要對Ogre做的輕微改變僅是對Ogre現(xiàn)有的渲染系統(tǒng)增加一個線程化的渲染系統(tǒng)層。像這樣
技術(shù)方案3:線程化渲染插件(低級線程化手段)
線程化一個特定的渲染插件帶來了最低級的適應(yīng)性,因為它和特定的技術(shù)(如D3D,OPENGL)綁定起來。但是它也使你能夠最大限度地操縱硬件資源。
實現(xiàn)這個方案最干凈利落的辦法就是創(chuàng)建一個介于API和渲染插件之間的層,用來處理線程化。(如下圖)
在這種方法下,它僅僅是用你的層來替換API接口。并且每個一調(diào)用從渲染插件的調(diào)用都是線程安全的,因為你的這個層處理了所有的調(diào)用。為D3D做這個,僅僅是用你自己的包裝類和方法替換了IDirect3D的接口。對于OPENGL,你將移除所有的OPENGL頭文件,并用你自己包裝好后的頭文件替換掉它們。有可能你需要用一個命名空間來包裝OPENGL函數(shù),以免出現(xiàn)名詞沖突。
這個方案依然和“中級線程化手段”一樣需要考慮些線程化相關(guān)的東西。
·初始化不需要多線程
·創(chuàng)建函數(shù)以及一些get函數(shù)需要在渲染完一幀前等待。
·渲染命令,和伴隨參數(shù)將需要加入隊列中。
關(guān)于索引和頂點緩沖的加鎖
對于所有的方案,都存在一個使用硬件資源時的潛在線程問題。對索引頂點緩沖區(qū)的加鎖就是一個很大的挑戰(zhàn),因為某些應(yīng)用程序在執(zhí)行的時候總是會反復對這些緩沖區(qū)進行加鎖和解鎖。比如對于動態(tài)緩沖區(qū),總是會每幀都改變它的內(nèi)容來實現(xiàn)畫面的變化。一些應(yīng)用程序也會重用緩沖區(qū)以使多個對象每幀都能夠共享同樣的頂點和索引緩沖區(qū)。為了解決這個問題,我們需要緩沖這些緩沖區(qū)。然而,可以通過很多方法來實現(xiàn),下面有兩種緩沖辦法:
1、 部分緩沖區(qū)加鎖,這是一個類似于雙緩沖的緩沖技術(shù)。我們將在顯卡中創(chuàng)建兩個緩沖區(qū),而不是一個。在這種情況下,當應(yīng)用程序在寫一個緩沖區(qū)的時候,渲染線程將使用另一個緩沖區(qū)中的數(shù)據(jù)進行渲染。這個方案可以提升渲染速度但是有一個缺點,就是會消耗更多的顯存。并且不能處理應(yīng)用程序在每一幀對同一緩沖區(qū)連續(xù)寫兩次的情況。
2、 完全緩沖區(qū)加鎖,這個技術(shù)保持一個對于緩沖區(qū)的所有鎖的副本。保留一個本地緩沖區(qū)并在上面進行所有的修改。當解鎖它的時候,那些數(shù)據(jù)就會被放入一個參數(shù)隊列,這樣就可以對每次加鎖/解鎖維護一個唯一的副本。雖然這樣可以對緩沖區(qū)數(shù)據(jù)進行精確的維護,但當某些應(yīng)用程序在鎖較大的數(shù)據(jù)的時候效果會大打折扣。
為什么鎖表面的時候不用緩沖
表面(如前臺緩沖,后臺緩沖,紋理等)通常消耗大量的存儲空間。 所以,在對表面加鎖時緩沖是無用的,也是不必要的。為什么沒有用呢,因為在上面的加鎖緩沖技術(shù)中,一個顯卡會瞬間消耗掉大量的存儲空間,因為這個時候我們需要創(chuàng)建兩個表面。 例如:一個1024X1024 32bpp的紋理通常會消耗掉4MB的顯存,但是如果采用了上面的緩沖鎖技術(shù),那么將會占用8MB的顯存。這個技術(shù)將會導致紋理可用的顯存資源直接減半。而對于完全緩沖區(qū)鎖技術(shù)。則會消耗了大量的系統(tǒng)內(nèi)存并且花費較多時間來拷貝數(shù)據(jù)。因為這個技術(shù)在系統(tǒng)內(nèi)存中維護了一個數(shù)據(jù)的副本。所以它將會一次性地為這個表面分配大量的空間。當解鎖的時候,又會將數(shù)據(jù)拷貝到參數(shù)隊列里,并且參數(shù)隊列在必要的時候,也會分配較大的空間。在渲染時,渲染線程同樣需要將數(shù)據(jù)從參數(shù)隊列中取出,并拷貝到真正的表面上。所有的這些拷貝操作,都會導致加鎖和解鎖的速度大大降低。
表面加鎖時緩沖不必要是因為應(yīng)用程序不需要直接對表面(紋理除外)進行讀寫,為了取得更好的性能,可以使用顯卡直接對表面進行操作。紋理,從另一方面講,僅會被加鎖一次,然后從文件中加載信息到它的表面。所以,對于紋理的加鎖,可以讓主線程等待渲染線程返回,然后再進行加鎖,而不用對其進行緩沖。注意,等待渲染線程返回并加鎖有可能導致錯誤。因為有可能顯卡正在填充這個表面資源。記住,頻繁的對表面進行加鎖將會導致因為主線程的強制等待而使效率降低。
這個方案的實現(xiàn)只能是在包裝圖形API的那個包裝類中。每一個函數(shù)都會有不同的實現(xiàn),因為它們有可能是直接調(diào)用圖形API,而有可能是要在調(diào)用圖形API前等待前一幀繪制完成?;蛘呤菍?shù)據(jù)放入渲染線程的隊列,讓渲染線程去調(diào)用API。也可以對其進行一些優(yōu)化,采用在包裝類中做一些緩存來緩存那些需要等待的調(diào)用(譯注:個人理解是因為每一幀需要等待的調(diào)用可能不止一個,將他們緩存起來。這樣當渲染線程返回時,一并調(diào)用這些函數(shù),就能少了許多等待)。你還需要的就是修改渲染系統(tǒng)插件的代碼,用你的包裝類函數(shù)替換掉所有的圖形API調(diào)用。
下面的代碼演示了如何進行IUnknown接口的包裝。
這個技術(shù)達到了本文介紹中的所有目標,并且也是最終的選擇方案。D3D9渲染系統(tǒng)被選擇來實現(xiàn)這個方案,伴隨本文的代碼可以從Intel Developer website上下載。
OGRE分析以及結(jié)果
這個部分我們將討論采用“低級線程化手段”時,運行OGRE的例子程序的情況。下面的數(shù)據(jù)是在2.4GHz Core 2 雙核處理器,NVIDIA 7800顯卡上測試的結(jié)果。這些數(shù)據(jù)顯示了兩個不同的緩沖加鎖方案,以及在沒有開啟多線程時DirectX包裝類的負載。綠色背景指明了使用這個方案時提升到了1.1倍,甚至更多。 黃色部分指明了運用該方案時,降低到了1.0倍以下,但是并無太大損耗。而紅色部分指明了動用此方案時,出現(xiàn)了沖突情況。
注意,這里有太多的1。0左右的元素,當然也有一些稍微低于1。0的。但是不會太多。可以看到,這里也有許多綠色的元素,表明運行良好。但要注意,這些測試例子都是OGRE的DEMO,DEMO并不使用CPU處理太多的東西。只有兩個元素是紅的。都是處于“完全緩沖加鎖”方案中。因為這些DEMO經(jīng)常更新很大的頂點緩沖。導致很多時間花費在等待復制完成,從而出現(xiàn)問題。
需要特別說明的是“Shadow Demo”。有兩種陰影方案,模版和紋理。只有模版陰影在“部分緩沖加鎖”方案時會出現(xiàn)問題,而紋理陰影則工作良好。
同時也要注意的時,一些例子在“完全緩沖加鎖”方案中運行的效果比“部分緩沖加鎖”方案要好。在這種情況下,應(yīng)用程序取得一個可以讀寫的緩沖區(qū)。在這種情況下,主要是負載在于應(yīng)用程序會將顯存中的數(shù)據(jù)復制到系統(tǒng)內(nèi)存中。而“完全緩沖加鎖”方案通過將加鎖和解鎖放到渲染線程中,而將這個負擔消除了。
結(jié)論:
將一個3D渲染引擎線程化是一項具有挑戰(zhàn)性的工作,但事實證明,它是可行的。正如先前所展示的一樣,將OGRE渲染引擎線程化是很成功的,并且在某些DEMO上運行出了良好的效果。有三種線程化方案,但最終只選擇了“低級優(yōu)化方案“來實現(xiàn)。因為其滿足了本文最初提出的目標。在OGRE社區(qū)的共同努力下,OGRE可以在CPU和GPU資源都密集型的應(yīng)用程序中,運行出更多幀數(shù)和更平滑的顯示效果。
posted on 2010-12-20 23:14 麒麟子 閱讀(2357) 評論(0) 編輯 收藏 引用 所屬分類: Game and Engine
Powered by: C++博客 Copyright © 麒麟子