• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 14,  comments - 57,  trackbacks - 0
              2013年12月2日

            問題  

               上周開始,我們一個(gè)已經(jīng)在線運(yùn)行了快2年的游戲突然頻繁宕機(jī),宕機(jī)前剛好上了一個(gè)資料片,提交了大批量的代碼。
            比較麻煩的是宕機(jī)的core文件里沒有任何有效CallStack信息。在隨后的多次宕機(jī)core文件里也都找不到有效的CallStack信息,定位問題變得無從入手。

            原因
             

               根據(jù)經(jīng)驗(yàn),這是一個(gè)典型的棧破壞問題。一旦棧破壞了函數(shù)返回值后,堆棧完全是錯(cuò)亂的,得不到任何有效信息。
            最開始我建議項(xiàng)目組的同事查看最近提交的代碼,看看能否找到線索。不過由于近一個(gè)月提交的代碼實(shí)在太多,大海撈針了一段時(shí)間后,
            毫無頭緒。
               棧覆蓋一般是因?yàn)閙emcpy或者是循環(huán)賦值語句導(dǎo)致的,一般棧覆蓋的層次不會(huì)太多,所以從底部往上找,應(yīng)該能找到些有效的線索。
            不過,由于服務(wù)器函數(shù)經(jīng)常會(huì)有Package的臨時(shí)變量,導(dǎo)致函數(shù)棧很大,從下往上找線索也很困難,很多似是而非的合法地址很容易分散精力。

            解決

              按照上面的分析,從底部往上找是大海撈針,那么從頂部往下找如何呢?
            這里先說明下一般函數(shù)堆棧幀的建立(未優(yōu)化情況下的用戶函數(shù)):
            push rbp
            mov rbp, rsp
            從這里可以看出,本層函數(shù)的返回值是存儲(chǔ)在 [rbp + 8],而上層函數(shù)的rbp地址則存儲(chǔ)在 [rbp]。
            所以,從下網(wǎng)上找的時(shí)候,可以根據(jù)rbp逐步找到上層函數(shù)和上層函數(shù)的堆棧幀。

            那么如何往下找呢,假如知道了一個(gè)上層函數(shù)的rbp,如何獲取下層函數(shù)呢,
            這里有個(gè)小竅門,gdb7.X的版本有一個(gè)find功能,可以在內(nèi)存區(qū)域搜索數(shù)值,
            從上往下找的時(shí)候,可以在堆棧查找本層 rbp的存放地址,從而確定下層函數(shù)rbp的存放地址。
            舉個(gè)例子:

            #0  0x00007ffff77d7830 in nanosleep () from /lib/x86_64-linux-gnu/libc.so.6
            #1  0x00007ffff77d76ec in sleep () from /lib/x86_64-linux-gnu/libc.so.6
            #2  0x000000000040070a in test1 () at main.cpp:9
            #3  0x0000000000400715 in test () at main.cpp:14
            #4  0x000000000040072b in main (argc=1, argv=0x7fffffffe648) at main.cpp:19

            這是一個(gè)典型的CallStack,讓我們先找到0x000000000040072b的堆棧信息吧。
            先 info r 查看當(dāng)前的寄存器信息:
            得到 rsp為0x7fffffffe358

            find $rsp, +0x300, 0x000000000040072b
            0x7fffffffe548
            1 pattern found.

            只有一個(gè)地址,那么存放rbp的地址就是
            0x7fffffffe540了,
            繼續(xù) find $rsp, +0x300, 0x7fffffffe540
            0x7fffffffe530
            1 pattern found.
            驗(yàn)證下是否正確:
            x/10xg 
            0x7fffffffe530

            0x7fffffffe530: 0x00007fffffffe540      0x0000000000400715
            0x7fffffffe540: 0x00007fffffffe560      0x000000000040072b
            0x7fffffffe550: 0x00007fffffffe648      0x0000000100000000

            看到了吧,就是這樣找到了下一級(jí)的函數(shù)。
            真實(shí)環(huán)境中往往沒這么簡(jiǎn)單,有時(shí)候會(huì)找到好幾個(gè)地址,這個(gè)時(shí)候需要自己逐個(gè)去偽存真了。

             

            posted @ 2013-12-02 20:51 feixuwu 閱讀(781) | 評(píng)論 (0)編輯 收藏
              2012年7月15日
              寫這篇文章是對(duì)自己2011bug戰(zhàn)斗時(shí)光一個(gè)交代,隨著時(shí)間的推移,當(dāng)初印象深刻的痛苦和壓力慢慢消逝,到現(xiàn)在甚至是需要很長(zhǎng)時(shí)間來弄清楚這中間的關(guān)系,趁著現(xiàn)在頭腦還算清楚,記錄下吧。

            場(chǎng)景管理

               為了說明Bug產(chǎn)生的原因,先描述下場(chǎng)景管理的實(shí)現(xiàn)方式吧。

              1、游戲場(chǎng)景是游戲地圖的一個(gè)實(shí)例(假設(shè)地圖是class),一個(gè)地圖可以創(chuàng)建多個(gè)場(chǎng)景,場(chǎng)景主要負(fù)責(zé)管理玩家的移動(dòng)、廣播等處理。
              2、場(chǎng)景的廣播是采取經(jīng)典的九宮格方式來實(shí)現(xiàn)的,每一個(gè)格子的我們定義為Area對(duì)象,一個(gè)場(chǎng)景的格子組成其實(shí)是一個(gè)二維數(shù)組。
              3、玩家進(jìn)入場(chǎng)景的時(shí)候,根據(jù)坐標(biāo)可以知道要進(jìn)入哪個(gè)格子,每個(gè)格子內(nèi)會(huì)保留一個(gè)Head指針,標(biāo)記最新進(jìn)入的玩家對(duì)象。玩家對(duì)象上有2個(gè)指針,標(biāo)記玩家所在格子的前一個(gè)和后一個(gè)對(duì)象。可以通過格子內(nèi)的Head指針便利Area內(nèi)的所有玩家對(duì)象。
              4、玩家移動(dòng)切換格子的時(shí)候,先從原來的格子內(nèi)Leave,這會(huì)調(diào)用原來Area對(duì)象的Leave函數(shù)。再進(jìn)入新的格子,調(diào)用Area的Enter函數(shù)。很明顯,Leave函數(shù)就是一個(gè)鏈表刪除操作,如果玩家是Head,則設(shè)置新的Head。
               Enter操作就是將新進(jìn)入的玩家鏈接到原隊(duì)列里,新進(jìn)入的玩家會(huì)被設(shè)置為Head。

            問題表現(xiàn)  

               根據(jù)上面的描述,如果一切按照正常程序,這個(gè)方案運(yùn)轉(zhuǎn)是沒問題的。最初上線的時(shí)候,也沒有出現(xiàn)問題,但是在出了一個(gè)資料片之后,服務(wù)器基本上每隔半小時(shí)左右就會(huì)發(fā)現(xiàn)有死循環(huán)或者宕機(jī)問題。

            死循環(huán)的表現(xiàn)很明顯,就是在遍歷場(chǎng)景玩家的時(shí)候,出現(xiàn)死循環(huán)。宕機(jī)則更加復(fù)雜些,每次宕機(jī)位置不同,總的來說大概有3-4個(gè)地方,每個(gè)地方單看都不合常理。
            直接分析上面的表現(xiàn),都找不到真正的的原因,只好擴(kuò)大搜索范圍了。
            比較倒霉的是那個(gè)資料片的主要系統(tǒng)都是我開發(fā)的,所以自然嫌疑最大,然后大家集中精力來分析我的代碼,由于每個(gè)人風(fēng)格都不同,所以大家看的不太明白的地方都會(huì)來問我,所以那個(gè)晚上基本就在解答設(shè)計(jì)疑問了。
            被輪了大半個(gè)晚上,知道凌晨2點(diǎn),大家也沒分析出問題,只好先回去睡覺了。結(jié)果早上7點(diǎn),測(cè)試給我打電話了,沒辦法只好跑過去了,一到公司,發(fā)現(xiàn)圍了一堆老大,老大們很嚴(yán)肅:這個(gè)問題很嚴(yán)重,必須盡快解決。
            沒辦法,只好繼續(xù)上陣了,戰(zhàn)斗到下午2點(diǎn),突然靈光一閃,想到了原因,當(dāng)時(shí)感覺真的心力交瘁了,更加感慨的是其實(shí)這個(gè)問題真和我沒啥關(guān)系。。。


            原因

               真正導(dǎo)致這次事故的其實(shí)是一個(gè)小操作:玩家重登錄(手機(jī)玩家斷網(wǎng)的時(shí)候,服務(wù)器會(huì)保存一段時(shí)間在線狀態(tài))的時(shí)候,有的時(shí)候由于其他原因,會(huì)卡在不能地圖的物理層(不能行動(dòng)的點(diǎn)),玩家完全不能移動(dòng)。為了解決這個(gè)問題,有個(gè)同事在玩家重登錄的時(shí)候,直接設(shè)置了玩家的坐標(biāo)到一個(gè)可移動(dòng)的點(diǎn)。
            這個(gè)看似無關(guān)緊要的操作,真正導(dǎo)致了服務(wù)器1天多時(shí)間內(nèi)不停的宕機(jī)。下面來記錄下分析過程吧。
            1、玩家在重登錄前,其實(shí)是在場(chǎng)景中的,也就是在一個(gè)具體的Area里。
            2、由于玩家上線后,直接設(shè)置了坐標(biāo),而我們后續(xù)的計(jì)算是通過坐標(biāo)來獲取Area對(duì)象的,其實(shí)這里就出現(xiàn)問題了,玩家其實(shí)是在A格子的鏈表上,但是根據(jù)坐標(biāo)計(jì)算獲得的格子是B。
            3、玩家移動(dòng)后,切換格子,需要從原格子Leave,然后進(jìn)入新的格子,但是基于上面的原因,所以其實(shí)涉及到的有3個(gè)格子,(1)、玩家真實(shí)所在的格子鏈表(A)。(2)、通過坐標(biāo)計(jì)算所得的格子(B),這個(gè)格子對(duì)象上會(huì)調(diào)用Leave操作。
              (3)要進(jìn)入的新格子(C)。
            4、由于玩家其實(shí)在格子A,但是我們調(diào)用的是B.Leave(player);C.Enter(player),從這里看,肯定是有問題的,但是細(xì)看則不然,由于玩家對(duì)象是記錄了前一個(gè)和后一個(gè)對(duì)象,所以B.Leave本身并不會(huì)破壞B的鏈表結(jié)構(gòu),C.Enter看上去也沒問題,那么,問題在哪里?
            5、真正的原因其實(shí)是格子A對(duì)象被破壞了,B.Leave(player)上是將玩家從它自己的鏈表上刪除了,鏈表本身是沒有被破壞了,關(guān)鍵的原因是如果玩家在格子A是Head,那么實(shí)際上在玩家被刪除后,Head應(yīng)該被改變,但是由于操作的是格子B,所以,A其實(shí)被破壞了,很奇妙,這個(gè)對(duì)象沒有操作,卻被破壞了。后面的問題就簡(jiǎn)單了,如果玩家進(jìn)入的C就是A,則會(huì)是一個(gè)很明顯的死循環(huán),如果玩家進(jìn)入的C是一個(gè)新的格子,則格子A的對(duì)象都不能被感知了。

             

             

             

            posted @ 2012-07-15 22:13 feixuwu 閱讀(410) | 評(píng)論 (0)編輯 收藏
              2012年2月16日

                    2011已經(jīng)謝幕了,現(xiàn)在都流行總結(jié),要是讓我總結(jié)2011,可以用2個(gè)詞來概括,辛苦、刺激。
            辛苦是因?yàn)?011基本上是加了一年班,從過完年開始,到2012年過年前最后一周,這一年來,是我感覺最辛苦的一年,好在最終
            項(xiàng)目算是打了個(gè)翻身仗,心里總算有了些慰藉。

                   2011年游戲經(jīng)歷從技術(shù)封測(cè)、內(nèi)測(cè)、公測(cè)到整改、重新內(nèi)測(cè)公測(cè),一路走來,遇到無數(shù)稀奇古怪的Bug,
            有時(shí)候壓力大的時(shí)候,晚上都睡不著,腦子里回想著現(xiàn)場(chǎng)的一絲絲蛛絲馬跡,希望能找到bug的原因,經(jīng)歷過無數(shù)次絕望到重生的喜悅,也有被猜忌不信任的痛苦,活脫脫就是一部部偵探劇情。
              沒有從事過游戲開發(fā)或者游戲沒上線的同學(xué)很難理解:bug有這么難找嗎?的確,如果是簡(jiǎn)單的空指針宕機(jī),當(dāng)然是好找的,用我們的話,這類問題是個(gè)傻子都能解決(其實(shí)不然,很多時(shí)候直接原因是空指針,
            真正的原因隱藏很深),但是更多的是隱藏很深的問題,需要反復(fù)的分析現(xiàn)場(chǎng),假設(shè)劇情才能得到靈感,然后推演,才可能得到結(jié)果,當(dāng)然,這個(gè)和游戲邏輯的復(fù)雜度是分不開的。
              具體的bug細(xì)節(jié)不便在此分析,但是大部分的問題,其實(shí)都是因?yàn)椴徽5脑O(shè)計(jì)引起的,所以其實(shí)我一直在思考,在軟件開發(fā)領(lǐng)域,其實(shí)也存在著"道",說通俗點(diǎn)叫客觀規(guī)律,不按照道行事,遲早是要受到懲罰的。
            但是在游戲后臺(tái)開發(fā)中,很多時(shí)候存在不同技術(shù)方案的矛盾,難以讓人取舍,這些矛盾都是真實(shí)在很多項(xiàng)目存在的。

            動(dòng)態(tài)內(nèi)存還是靜態(tài)內(nèi)存

                  很多開發(fā)者由于擔(dān)心內(nèi)存泄露,在項(xiàng)目中禁止使用動(dòng)態(tài)內(nèi)存(當(dāng)然這實(shí)際上幾乎是做不到的),使用對(duì)象池來避免動(dòng)態(tài)內(nèi)存,就是預(yù)先創(chuàng)建預(yù)計(jì)最大數(shù)量的對(duì)象,后續(xù)申請(qǐng)和歸還的時(shí)候,都是操作對(duì)象池,
            避免動(dòng)態(tài)new和delete。這樣的項(xiàng)目還不少,我見過的就好幾個(gè)。對(duì)象池的好處是顯而易見的,基本上可以避免內(nèi)存泄露。但是實(shí)際上,這種方式是把雙刃劍,個(gè)人覺得在游戲項(xiàng)目中,這種方式弊大于利。
            主要弊端有下面幾點(diǎn):
            1、開發(fā)不方便,導(dǎo)致需要添加很多的對(duì)象池管理類,即使有模板幫忙,也是非常繁瑣的。實(shí)際開發(fā)中,幾乎不可能對(duì)這些小對(duì)象類都搞一個(gè)對(duì)象池管理類。

            2、由于采用預(yù)先生成對(duì)象,一般會(huì)預(yù)估一個(gè)對(duì)象可能存在的最大數(shù)量,然后按照最大數(shù)量來創(chuàng)建,浪費(fèi)內(nèi)存。
              的確,你沒有內(nèi)存泄露,但是你啟動(dòng)的時(shí)候就需要好幾個(gè)G的內(nèi)存,這個(gè)是內(nèi)存浪費(fèi),好在現(xiàn)在server開發(fā)基本都是64位,沒有地址空間的困擾了,但是,在大部分情況下浪費(fèi)好幾個(gè)G的內(nèi)存,
            光想想都有點(diǎn)心疼。

            3、引入了新的風(fēng)險(xiǎn),由于采用對(duì)象池,申請(qǐng)新對(duì)象的時(shí)候,只是簡(jiǎn)單的pop一個(gè)空閑對(duì)象就可以了,很容易漏掉對(duì)象初始化的工作,在回收對(duì)象的時(shí)候,大部分開發(fā)者也很容易漏掉清理工作,或者初始化和
            清理工作過于簡(jiǎn)單,這樣容易導(dǎo)致新對(duì)象被歷史操作影響。曾經(jīng)遇到過一個(gè)新FB所有傳送點(diǎn)都打不開的問題,就是因?yàn)闅v史對(duì)象回收時(shí)數(shù)據(jù)沒清理導(dǎo)致的。

                回頭來看對(duì)象池的優(yōu)點(diǎn),很多開發(fā)者堅(jiān)持是為了解決內(nèi)存碎片和內(nèi)存泄露。先說內(nèi)存碎片,暫且不說內(nèi)存碎片真的是否有這么嚴(yán)重,退一步,其實(shí)內(nèi)存碎片已經(jīng)有很多的成熟解決方案了,自己重載smallObject還是
            采用標(biāo)準(zhǔn)的tcmalloc解決,都是非常輕松的。至于內(nèi)存泄露,個(gè)人覺得這個(gè)問題其實(shí)是很好查的,也是c++程序員的基本要求。

            分模塊針對(duì)接口編程還是一鍋粥

                 這個(gè)問題單獨(dú)提出來,幾乎所有人都會(huì)說,當(dāng)然是分模塊針對(duì)接口開發(fā)了。和天下所有的事情一樣,知易行難。由于游戲邏輯項(xiàng)目影響的地方非常多,比如死亡的時(shí)候,既需要判斷死亡掉落,又需要處理任務(wù)狀態(tài),
            如果在戰(zhàn)場(chǎng)和競(jìng)技場(chǎng)中,還要判斷基數(shù)和得分等等,這就導(dǎo)致很多開發(fā)者不假思索的把所有的東西都揉在一起,你中有我,我中有你,我改你的代碼你改我的。
            一個(gè)最簡(jiǎn)單的例子,我在項(xiàng)目中開發(fā)掉落功能,當(dāng)把物品添加到玩家背包后,發(fā)現(xiàn)客戶端沒有更新背包,一查,居然還需要掉落的開發(fā)者自己構(gòu)造數(shù)據(jù)包同步客戶端,其實(shí)作為其他模塊,根本不關(guān)心背包數(shù)據(jù)同步的細(xì)節(jié)。
            這個(gè)其實(shí)在現(xiàn)實(shí)生活中很常見,我委托背包模塊添加一個(gè)物品,具體的細(xì)節(jié)是被由被委托人來負(fù)責(zé)的。將過多的細(xì)節(jié)交給其他模塊處理,會(huì)導(dǎo)致復(fù)雜度增加,容易出現(xiàn)問題,對(duì)其他人來說,也是一個(gè)精力浪費(fèi),如果是一個(gè)復(fù)雜
            模塊,你會(huì)發(fā)現(xiàn)需要了解太多的細(xì)節(jié),修改太多自己不熟悉的代碼,進(jìn)而導(dǎo)致風(fēng)險(xiǎn)。還有一種觀點(diǎn),認(rèn)為一鍋粥的開發(fā)方式有助于了解游戲的各個(gè)業(yè)務(wù)模塊,對(duì)這種觀點(diǎn),我是不以為然的,每天陷入到繁瑣的細(xì)節(jié),真的對(duì)熟悉業(yè)務(wù)有好處嗎?或許閑下來玩玩游戲更有幫助,而且,這么亂的代碼,看起來也是非常累的。分模塊開發(fā),具體的辦法,游戲編程精粹5上有篇文章寫得很好,這里不擴(kuò)展了。

            真的需要禁用STL嗎

              不止一次在和其他項(xiàng)目交流的資料里看到對(duì)方很威嚴(yán)的宣稱在項(xiàng)目里禁止使用STL。說實(shí)話,我還真沒覺得STL有什么不好。見過太多這類項(xiàng)目自己重復(fù)實(shí)現(xiàn)一個(gè)個(gè)蹩腳的排序算法、容器等等。
            大部分人一般都會(huì)根據(jù)經(jīng)驗(yàn)選擇使用自己熟悉的技術(shù),這個(gè)無可厚非,但是像這樣明著禁止使用STL,真不知道如何能理直氣壯。其實(shí)大部分不用STL的理由,基本上都是不熟悉,完全沒有足夠的理由禁止使用。

            游戲開發(fā)無技術(shù)含量?
             
                曾經(jīng)多次聽到行業(yè)內(nèi)的兄弟有此感慨,確實(shí),游戲邏輯復(fù)雜度非常高,架構(gòu)上大部分都是類似的。但是這并不說明游戲后臺(tái)開發(fā)復(fù)雜度不高,如何將游戲開發(fā)邏輯復(fù)雜剝離開來,做到穩(wěn)定高效開發(fā),其實(shí)還是有很多
            東西可以探討的,看看那些項(xiàng)目,大部分都是一鍋粥,需要什么功能就蠻干,加上去,這樣確實(shí)毫無技術(shù)含量,都是蠻干。所以,一件事情是否有技術(shù)含量,不光是看事情本身,還要看怎么干,蠻干和苦干,那是最沒有技術(shù)
            含量的方式了,程序員還是要有強(qiáng)烈的“偷懶”意識(shí)。

             

            posted @ 2012-02-16 21:00 feixuwu 閱讀(748) | 評(píng)論 (4)編輯 收藏
              2011年5月14日

             問題

               最近游戲開始技術(shù)封測(cè)了,不過剛剛上線3個(gè)小時(shí),Server就掛了,掛在框架代碼里,一個(gè)不可能掛的地方。
            從CallStack看,是在獲取數(shù)據(jù)時(shí)發(fā)送請(qǐng)求包的時(shí)候掛的,由于框架部分是其他部門的同事開發(fā)的,所以查問題的時(shí)候就拉上他們了,
            大家折騰了2天,沒有實(shí)質(zhì)性的進(jìn)展,服務(wù)器還是基本上每3個(gè)小時(shí)宕機(jī)一次。由于上層邏輯大部分都在我那,所以壓力比較大,宕機(jī)的直接原因是hashtable的一個(gè)桶的指針異常,
            這個(gè)hashtable是框架代碼的一個(gè)內(nèi)部成員,按道理我們是無從破壞的,只有可能是多線程環(huán)境下迭代器損壞導(dǎo)致的。
            但是框架代碼在這個(gè)地方確實(shí)無懈可擊,所以真正的原因應(yīng)該還是上層代碼破壞了堆內(nèi)存,很可能是一個(gè)memcpy越界導(dǎo)致的。這畢竟是個(gè)猜想,如何找到證據(jù)呢,這是個(gè)問題。
            把所有代碼里的memcpy瀏覽了一遍,沒有發(fā)現(xiàn)明顯問題。

            猜測(cè)

              一般游戲中比較容易出現(xiàn)但是不好查的問題很多時(shí)候都是腳本(lua)導(dǎo)致的,我們的腳本部分是一個(gè)同事幾年前寫的,在幾個(gè)產(chǎn)品中都使用過,按道理沒這么脆弱,不過老大還是和最初開發(fā)這個(gè)模塊的部門溝通了下,
            還真發(fā)現(xiàn)問題了,趕緊拿了新的版本更新上去。經(jīng)過一天的觀察,服務(wù)器沒有宕機(jī)了,OK,問題碰巧解決了,背了這么久的黑鍋,終于放下來了。

            PageHeap

               假如沒有碰巧解決了這個(gè)問題,正常的思路該如何解決這個(gè)問題呢,這個(gè)時(shí)候我懷念windows了,在windows下有PageHeap來解決這類寫越界的問題。基本思路就是每次分配內(nèi)存的時(shí)候,都將內(nèi)存的結(jié)尾放在頁的邊緣,緊接著這塊內(nèi)存分配一塊不能寫的內(nèi)存,這樣,一旦寫越界,就會(huì)寫異常,導(dǎo)致宕機(jī)。linux下沒有現(xiàn)成的工具,但是linux提供了mmap功能,我們可以自己實(shí)現(xiàn)這樣一個(gè)功能,當(dāng)然,這一切都不用自己動(dòng)手了,tcmalloc已經(jīng)包含了
            這個(gè)功能了,不過在文檔里基本沒有介紹,我也是在閱讀tcmalloc代碼時(shí)看到的,這個(gè)功能默認(rèn)是關(guān)閉的,打開這個(gè)開關(guān)需要改寫代碼:

            這個(gè)代碼在debugallocation.cc里:

            DEFINE_bool(malloc_page_fence,
                        EnvToBool("TCMALLOC_PAGE_FENCE", false),
                        "Enables putting of memory allocations at page boundaries "
                        "with a guard page following the allocation (to catch buffer "
                        "overruns right when they happen).");
            把false改成true就可以了。
            想要在項(xiàng)目里加入PageHeap功能,只需要鏈接的時(shí)候加上 -ltcmalloc_debug即可。把它加入項(xiàng)目中,試著運(yùn)行下,直接掛了,
            仔細(xì)一看,原來是項(xiàng)目中很多成員變量沒有初始化導(dǎo)致的,tcmalloc_debug會(huì)自動(dòng)將new 和malloc出來的內(nèi)存初始化為指定值,這樣,一旦變量沒有初始化,很容易就暴露了。
            修改完這個(gè)問題后,編譯,再運(yùn)行,還是掛,這個(gè)是mprotect的時(shí)候掛的,錯(cuò)誤是內(nèi)存不夠,這怎么可能呢,其實(shí)是達(dá)到了資源限制了。
            echo 128000 > /proc/sys/vm/max_map_count
            把map數(shù)量限制加大,再運(yùn)行,OK了!
             
              但是游戲Server啟動(dòng)后,發(fā)現(xiàn)一個(gè)問題,CPU長(zhǎng)期處于100%,導(dǎo)致登陸一個(gè)玩家都很困難,gdb中斷后,info thread,發(fā)現(xiàn)大部分的操作都在mmap和mprotect,最開始
            懷疑我的linux版本有問題,導(dǎo)致這2個(gè)AP慢,寫了測(cè)試程序試了下,發(fā)現(xiàn)其實(shí)API不慢,估計(jì)是頻繁調(diào)用導(dǎo)致的。
            所以得換種思路優(yōu)化下才可以,其實(shí)大部分情況下,我們free的時(shí)候,無需將頁面munmap掉,可以先cache進(jìn)來,下次分配的時(shí)候,如果有,直接拿來用就可以了。
            最簡(jiǎn)單的cache算法就是定義一個(gè)void* s_pageCache[50000]數(shù)組,頁面數(shù)相同的內(nèi)存組成一個(gè)鏈表,掛在一個(gè)數(shù)組項(xiàng)下,這個(gè)很像STL的小內(nèi)存處理,我們可以將mmap出來的內(nèi)存的
            前面幾個(gè)字節(jié)(一個(gè)指針大小)用于索引下一個(gè)freePage。當(dāng)然這個(gè)過程需要加鎖,不能用pthread的鎖(因?yàn)樗麄儠?huì)調(diào)用malloc等內(nèi)存分配函數(shù)),必須用spinlock,從linux源碼里直接抄一個(gè)過來即可。
            static void*   s_pagePool[MAX_PAGE_ALLOC]={0};

            malloc的時(shí)候,先從pagePool里面獲取:
            // 先從pagePool找
             void* pFreePage = NULL;
             spin_lock(&s_pageHeapLock);
             assert(nPageNum < MAX_PAGE_ALLOC);
             if(s_pagePool[nPageNum])
             {
               pFreePage = s_pagePool[nPageNum];
               void* pNextFreePage = *((void**)pFreePage);
               s_pagePool[nPageNum] = pNextFreePage;
             }
             spin_unlock(&s_pageHeapLock);

            free內(nèi)存的時(shí)候,直接放到pagePoll里:
            spin_lock(&s_pageHeapLock);
             assert(nPageNum < MAX_PAGE_ALLOC);
             void* pNextFree = s_pagePool[nPageNum];
             *(void**)pAddress = pNextFree;
             s_pagePool[nPageNum] = pAddress;
             
             spin_unlock(&s_pageHeapLock);

            編譯、運(yùn)行,OK了,CPU迅速降下來了,空載的時(shí)候不到1%,而且也能達(dá)到檢測(cè)寫溢出的問題。

            posted @ 2011-05-14 21:16 feixuwu 閱讀(2039) | 評(píng)論 (1)編輯 收藏
              2011年4月10日

              最近項(xiàng)目開始集中測(cè)試了,服務(wù)器程序經(jīng)常crash,由于服務(wù)器一般情況下都是關(guān)閉了core的,所以好幾次都只能通過雜亂的日志來定位問題。
            當(dāng)然,我們可以通過ulimit來打開core開關(guān),不過這可能帶來新的問題:我們的服務(wù)器程序每個(gè)core文件大概有1G多,測(cè)試期間如果頻繁crash,沒有注意及時(shí)清理,一不小心就會(huì)把磁盤寫滿,
            而且core文件畢竟是和進(jìn)程程序相關(guān)的,有時(shí)候找相應(yīng)版本也是個(gè)麻煩事。

            能否在程序crash的時(shí)候,將callStack以及參數(shù)和局部變量都記錄到日志里?
            這個(gè)技術(shù)其實(shí)在游戲客戶端已經(jīng)用了很多年了,一般游戲客戶端crash后,都會(huì)彈出一個(gè)是否發(fā)送錯(cuò)誤的選擇框,其實(shí)就是發(fā)送的CallStack的日志和MiniDUmp文件。
            要想記錄CallStack就必然涉及到Stack的遍歷,linux下的Stack遍歷使用很簡(jiǎn)單,簡(jiǎn)單的backtrace就可以搞定,man backtrace就有現(xiàn)成的例子,
            這比windows下復(fù)雜的頭疼的StackWalk好用的多。

            解決了Stack遍歷問題后,還剩下一個(gè)問題:如何在程序crash的時(shí)候得到通知執(zhí)行我們自己的dump代碼?
            在Windwos下有SEH異常來實(shí)現(xiàn)這個(gè)功能,而linux下可以通過使用信號(hào)在進(jìn)程crash的時(shí)候執(zhí)行自己的處理代碼。

            好了,開始寫個(gè)簡(jiǎn)單代碼測(cè)試下:
            首先設(shè)置幾個(gè)主要crash信號(hào)的處理函數(shù)
            signal(SIGSEGV, &DumpHelper::OnCrash);
            signal(SIGABRT, &DumpHelper::OnCrash);
            signal(SIGFPE, &DumpHelper::OnCrash);

            在OnCrash里我們用前面提到的backtrace系列函數(shù),來記錄堆棧:
            void* szStackFrame[100];
            int nFrameCount = backtrace(szStackFrame, 100);
            char** strFrameInfo = backtrace_symbols(szStackFrame, nFrameCount); 
            char szDumpFileName[1024] = {0};
            snprintf(szDumpFileName, sizeof(szDumpFileName), "dump_%u.log", (unsigned int)time(NULL) );
            FILE* pFile = fopen(szDumpFileName, "wb");
            if(!pFile) return;
            for(int i = 0; i < nFrameCount; i++)
            {
                fprintf(pFile, "%s\n", strFrameInfo[i]);
            }
            fclose(pFile);
            free(strFrameInfo);

            接著,設(shè)置幾個(gè)嵌套調(diào)用的函數(shù):
            void fun()
            {
             //assert(0);
             int* p = NULL;
             *p =3;
            }

            void fun1()
            {
             fun();
            }

            void fun2()
            {
             fun1();
            }

            void fun3()
            {
             fun2();
            }

            最后,我們?cè)趍ain函數(shù)里執(zhí)行fun3,注意編譯的時(shí)候帶上-rdynamic 選項(xiàng)。

            運(yùn)行下,果然可以打印基本的堆棧,不過馬上,發(fā)現(xiàn)了新的問題:這個(gè)堆棧信息也太簡(jiǎn)陋了,只有調(diào)用函數(shù)的名字,其余的參數(shù)、局部變量完全沒有,
            這個(gè)和gdb能看到的callStack差距也太大了。
            解決這個(gè)問題最簡(jiǎn)單的辦法就是用gdb來打印堆棧,在這里,gdb和其他程序有區(qū)別,如果你試圖通過 echo "bt"|gdb -p XXX>a.txt來獲得堆棧,那將會(huì)非常失望,
            根本不起作用,google了下,基本沒什么解決辦法。
            不過gdb 可以從文件讀入指令,例如 gdb XXX<cmddata,這給了我們機(jī)會(huì),
            system("echo  \"bt full|gcore\">testcmd");
              char dbx[160]={0};
                 sprintf(dbx, "gdb -p %d ./main<testcmd >gdbdump_%d.log", getpid(), getpid() );
              system(dbx);

            測(cè)試運(yùn)行,發(fā)現(xiàn)可以打印詳細(xì)的堆棧,不過,要求機(jī)器上有g(shù)db.
            上面的命令還dump了一個(gè)core文件,不過這個(gè)core文件的堆棧信息是錯(cuò)誤的,我不知道為什么。。。。

            多線程環(huán)境下使用上述辦法,只能輸出一個(gè)線程的堆棧,需要先獲取線程數(shù)目,然后逐個(gè)線程打印堆棧。

            最后,為了避免影響正常的coredump,要在OnCrash的處理函數(shù)里將信號(hào)的處理函數(shù)設(shè)置為默認(rèn)。
            如果我一定要有core呢,setrlimit吧,去掉core限制即可。
            posted @ 2011-04-10 14:47 feixuwu 閱讀(1036) | 評(píng)論 (0)編輯 收藏
              2011年3月19日
               我們的新項(xiàng)目是在linux平臺(tái)下運(yùn)行的,本人是Linux和windows下都開發(fā)過,我呆的2個(gè)linux后臺(tái)項(xiàng)目都是所有代碼放在一塊,編譯成一個(gè)可
            執(zhí)行文件,基本不考慮編譯成動(dòng)態(tài)庫,所有代碼的頭文件依賴也是一團(tuán)糟,隨著項(xiàng)目的增大,編譯速度越來越慢,到后來編譯一個(gè)項(xiàng)目4進(jìn)程同時(shí)編譯都需要10來分鐘。
             
              其實(shí)分析下可以發(fā)現(xiàn),主要的編譯速度損耗在頭文件上,尤其是模板相關(guān)的頭文件。VC有一個(gè)預(yù)編譯頭文件技術(shù),將常用的公共頭文件放在一起,預(yù)先編譯成pch文件,這樣
            可以加快編譯速度。gcc到底有沒有類似技術(shù)呢,打開gcc的手冊(cè)搜索了precompiled,發(fā)現(xiàn)還真有相關(guān)介紹,使用方法也很簡(jiǎn)單。
             
            主要是以下步驟:
              1、在項(xiàng)目下建立一個(gè) stdafx.h的文件,包含了大部分公共頭文件。在每個(gè)cpp最開始都#include "stdafx.h"。cpp文件包含了這個(gè)預(yù)編譯頭文件后,就可以將原來和
            stdafx .h 里頭文件重復(fù)的內(nèi)容刪除了,尤其是模板相關(guān)的頭文件,另外,非PCH的頭文件里盡量少包含其他頭文件。     
              2、修改makefile文件, 加入OBJ對(duì) gch的依賴,用一個(gè)簡(jiǎn)單的項(xiàng)目做示例,一看就明白
               
            TARGET=TimerTest
            PCH=stdafx.h.gch
            PCH_H=stdafx.h
            OBJ=stdafx.o TimerManager.o TimerTest.o

            %.o:%.cpp
                g++ -Wall -c -g $^ -o $@

            $(TARGET):$(OBJ)
                g++ -g  $^ -o $@


            pch.d:stdafx.cpp
                g++ -g -MM stdafx.cpp |sed 's/stdafx.o/stdafx.h.gch/'>$@

            -include pch.d

            $(OBJ):$(PCH)
            $(PCH):
                g++ $(PCH_H)

            clean:
                rm -f $(OBJ) $(PCH)

                完成以上內(nèi)容后,make clean,再重新編譯,初步估計(jì)只需要2分鐘!!  整整優(yōu)化了4-5倍。
                  

            posted @ 2011-03-19 16:39 feixuwu 閱讀(4383) | 評(píng)論 (5)編輯 收藏
              2011年3月13日
              最新?lián)Q了個(gè)項(xiàng)目組,閱讀代碼后,發(fā)現(xiàn)Server端代碼居然沒有事件和定時(shí)器。由于沒有事件,所以各個(gè)模塊代碼互相調(diào)用的地方特別多,導(dǎo)致代碼結(jié)構(gòu)混亂,所有代碼都放在一塊,亂成一鍋粥了。
            沒有定時(shí)器,所有需要定時(shí)的任務(wù),都只能添加類似OnUpdate的函數(shù),在主循環(huán)的時(shí)候執(zhí)行。定時(shí)需求少的時(shí)候,看不出明顯的問題,但是一旦這種需求多了,尤其是很多內(nèi)部對(duì)象有定時(shí)需求的時(shí)候,
            這個(gè)問題就比較明顯了,寫好了OnUpdate后,還要建立一條從主循環(huán)MainLoop到自身OnUpdate的調(diào)用鏈。
             
              事件其實(shí)就是一個(gè)廣播和訂閱的關(guān)系,Delegate就是實(shí)現(xiàn)這樣一套機(jī)制的利器,目前Delegate的實(shí)現(xiàn)主要有2種,一種是CodeProject上的一個(gè)FastDelegate實(shí)現(xiàn),另外一個(gè)比較典型的實(shí)現(xiàn)就是boost的
            實(shí)現(xiàn)了,無論采取哪種實(shí)現(xiàn)方案,實(shí)現(xiàn)難度都不算太大。
              Server當(dāng)前框架對(duì)定時(shí)器無任何支持,只有一個(gè)DoMainLoop的函數(shù)可以派生來運(yùn)行自己的定時(shí)邏輯。
              我原來都是用的ACE封裝的組件,用了一段時(shí)間也沒發(fā)現(xiàn)明顯問題,不過ACE的定時(shí)器不太適合在這個(gè)新項(xiàng)目用,主要原因有如下幾點(diǎn):
              1、ACE庫太大了,不想僅僅為了定時(shí)器引入一個(gè)這么龐大的庫。
              2、ACE的定時(shí)器需要額外啟動(dòng)一個(gè)定時(shí)器線程,定時(shí)任務(wù)是在定時(shí)器線程跑的,而我們的項(xiàng)目邏輯其實(shí)是在單個(gè)線程運(yùn)行的,如果直接采用ACE定時(shí)器,會(huì)給邏輯帶來額外的復(fù)雜度。由于整個(gè)邏輯線程的框架是公共模塊,手頭也沒有代碼,所以將定時(shí)器線程的任務(wù)發(fā)送到主邏輯線程運(yùn)行也是不可行的。
              3、ACE的定時(shí)器有很多種,TIMER_QUEUE、TIMER_WHELL、TIMER_HEAP等,個(gè)人感覺這些定時(shí)器的插入、取消操作都比較耗時(shí),加以改裝放到主線程run的帶價(jià)將會(huì)很大。

            其實(shí)linux內(nèi)核就有一個(gè)比較高性能的定時(shí)器,代碼在kernel/Timer.c里, 2.6內(nèi)核的定時(shí)器代碼更是簡(jiǎn)潔。
            linux的定時(shí)任務(wù)都是以jiffie 為單位的,linux將所有定時(shí)任務(wù)分為5個(gè)階梯,
            struct tvec {
                struct list_head vec[TVN_SIZE];
            };

            struct tvec_root {
                struct list_head vec[TVR_SIZE];
            };

            struct tvec_base {
                spinlock_t lock;
                struct timer_list *running_timer;
                unsigned long timer_jiffies;
                struct tvec_root tv1;
                struct tvec tv2;
                struct tvec tv3;
                struct tvec tv4;
                struct tvec tv5;
            } ____cacheline_aligned;

            對(duì)一個(gè)新的定時(shí)任務(wù),處理方法如下:
            static void internal_add_timer(struct tvec_base *base, struct timer_list *timer)
            {
                unsigned long expires = timer->expires;
                unsigned long idx = expires - base->timer_jiffies;
                struct list_head *vec;

                if (idx < TVR_SIZE) {
                    int i = expires & TVR_MASK;
                    vec = base->tv1.vec + i;
                } else if (idx < 1 << (TVR_BITS + TVN_BITS)) {
                    int i = (expires >> TVR_BITS) & TVN_MASK;
                    vec = base->tv2.vec + i;
                } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {
                    int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;
                    vec = base->tv3.vec + i;
                } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {
                    int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
                    vec = base->tv4.vec + i;
                } else if ((signed long) idx < 0) {
                    /*
                     * Can happen if you add a timer with expires == jiffies,
                     * or you set a timer to go off in the past
                     */
                    vec = base->tv1.vec + (base->timer_jiffies & TVR_MASK);
                } else {
                    int i;
                    /* If the timeout is larger than 0xffffffff on 64-bit
                     * architectures then we use the maximum timeout:
                     */
                    if (idx > 0xffffffffUL) {
                        idx = 0xffffffffUL;
                        expires = idx + base->timer_jiffies;
                    }
                    i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
                    vec = base->tv5.vec + i;
                }
                /*
                 * Timers are FIFO:
                 */
                list_add_tail(&timer->entry, vec);
            }
            從上可以看到Linux對(duì)定時(shí)器的處理:對(duì)即將在TVR_SIZE 個(gè)jiffies內(nèi)到達(dá)的定時(shí)任務(wù),將它掛到第一組tv1 下,具體就是掛到expires & TVR_MASK 對(duì)應(yīng)的列表上去。
            同一個(gè)jiffies到達(dá)的定時(shí)器是掛在同一個(gè)鏈表的。
            同理,掛到第二個(gè)組的是 到期時(shí)間小于 1 << (TVR_BITS + TVN_BITS) jiffies的。
            掛到第三個(gè)組的是 到期時(shí)間小于1 << (TVR_BITS + 2 * TVN_BITS) jiffies的。
            掛到第四個(gè)組的是 到期時(shí)間小于 1 << (TVR_BITS + 3 * TVN_BITS) jiffies的。
            超過1 << (TVR_BITS + 3 * TVN_BITS) 的掛到第五組。
            這樣,所有到期的任務(wù)都會(huì)在第一組。任何時(shí)刻都可以直接通過當(dāng)前jiffies&TVR_SIZE 來找到需要運(yùn)行的定時(shí)器任務(wù)列表,定時(shí)器的插入效率就是O(1)。

            下面是定時(shí)器的運(yùn)行代碼:
            static int cascade(struct tvec_base *base, struct tvec *tv, int index)
            {
                /* cascade all the timers from tv up one level */
                struct timer_list *timer, *tmp;
                struct list_head tv_list;

                list_replace_init(tv->vec + index, &tv_list);

                /*
                 * We are removing _all_ timers from the list, so we
                 * don't have to detach them individually.
                 */
                list_for_each_entry_safe(timer, tmp, &tv_list, entry) {
                    BUG_ON(tbase_get_base(timer->base) != base);
                    internal_add_timer(base, timer);
                }

                return index;
            }

            #define INDEX(N) ((base->timer_jiffies >> (TVR_BITS + (N) * TVN_BITS)) & TVN_MASK)

            /**
             * __run_timers - run all expired timers (if any) on this CPU.
             * @base: the timer vector to be processed.
             *
             * This function cascades all vectors and executes all expired timer
             * vectors.
             */
            static inline void __run_timers(struct tvec_base *base)
            {
                struct timer_list *timer;

                spin_lock_irq(&base->lock);
                while (time_after_eq(jiffies, base->timer_jiffies)) {
                    struct list_head work_list;
                    struct list_head *head = &work_list;
                    int index = base->timer_jiffies & TVR_MASK;

                    /*
                     * Cascade timers:
                     */
                    if (!index &&
                        (!cascade(base, &base->tv2, INDEX(0))) &&
                            (!cascade(base, &base->tv3, INDEX(1))) &&
                                !cascade(base, &base->tv4, INDEX(2)))
                        cascade(base, &base->tv5, INDEX(3));
                    ++base->timer_jiffies;
                    list_replace_init(base->tv1.vec + index, &work_list);
                    while (!list_empty(head)) {
                        void (*fn)(unsigned long);
                        unsigned long data;

                        timer = list_first_entry(head, struct timer_list,entry);
                        fn = timer->function;
                        data = timer->data;

                        timer_stats_account_timer(timer);

                        set_running_timer(base, timer);
                        detach_timer(timer, 1);
                        spin_unlock_irq(&base->lock);
                        {
                            int preempt_count = preempt_count();
                            fn(data);
                            if (preempt_count != preempt_count()) {
                                printk(KERN_ERR "huh, entered %p "
                                       "with preempt_count %08x, exited"
                                       " with %08x?\n",
                                       fn, preempt_count,
                                       preempt_count());
                                BUG();
                            }
                        }
                        spin_lock_irq(&base->lock);
                    }
                }
                set_running_timer(base, NULL);
                spin_unlock_irq(&base->lock);
            }
            當(dāng)?shù)谝唤M運(yùn)行完一輪后,需要將tv2的一組新的定時(shí)任務(wù)加到第一組。這就好比時(shí)鐘的指針,秒針運(yùn)行一圈后,分針步進(jìn)一格,后續(xù)的調(diào)整都是類似。
            cascade 就是負(fù)責(zé)將下一組的定時(shí)任務(wù)添加到前面的任務(wù)階梯。只有當(dāng)?shù)谝惠喌亩〞r(shí)任務(wù)全部運(yùn)行完畢后,才會(huì)需要從第二輪調(diào)入新的任務(wù),只有第二級(jí)別的任務(wù)都調(diào)入完畢后,才需要從第三輪的定時(shí)任務(wù)調(diào)入新的任務(wù):
             if (!index &&
                        (!cascade(base, &base->tv2, INDEX(0))) &&
                            (!cascade(base, &base->tv3, INDEX(1))) &&
                                !cascade(base, &base->tv4, INDEX(2)))
                        cascade(base, &base->tv5, INDEX(3));

            這就是負(fù)責(zé)調(diào)整的代碼,相當(dāng)?shù)暮?jiǎn)潔。
            參照上述代碼實(shí)現(xiàn)一個(gè)定時(shí)器后,加入4000個(gè)定時(shí)任務(wù):
                for(int i = 1; i < 4000; i++)
                {
                    g_TimerHandle[i] = g_timerManager.setTimer(&tmpSink1, i, i*10, "ss");
                }
            從10毫秒到4000*10毫秒,運(yùn)行后,測(cè)試下性能,
            函數(shù)名                                    執(zhí)行次數(shù)    最小時(shí)間     平均時(shí)間       最大時(shí)間
            TimerManager::runTimer    2170566        10              10               3046   
            可以看到,除了個(gè)別時(shí)間是因?yàn)榫€程切換導(dǎo)致數(shù)據(jù)比較大外,平均每次運(yùn)行runTimer的時(shí)間是10微秒。
            這個(gè)時(shí)間還包括每個(gè)定時(shí)器的執(zhí)行消耗,效率還是不錯(cuò)的。
            posted @ 2011-03-13 22:06 feixuwu 閱讀(2103) | 評(píng)論 (0)編輯 收藏
              2010年9月25日
              最近游戲又要封測(cè)了,工作比較緊張,晚上下班了比較累,回家懶得寫代碼了,不過順便倒是繼續(xù)完成了對(duì) 新劍俠情緣(和月影傳說的資源格式相同)的資源逆向。完成了資源逆向后,突然興致來了,寫了個(gè)簡(jiǎn)單的地圖查看器,到目前為止,一切運(yùn)行正常。后來做了個(gè)簡(jiǎn)單的Demo,實(shí)現(xiàn)了基本的尋路和技能動(dòng)畫播放,其實(shí)新劍俠情緣原本的技能效果以今天的眼光看起來也還可以,即便如此,我還是集成了hge的粒子系統(tǒng)進(jìn)去,試了下效果,還是挺奇怪的。
            做完了這些之后,本想為我的PSP山寨一個(gè)新劍俠情緣。不料后來連續(xù)加了好幾天班,加了幾天班之后,人也懶了,山寨游戲的事情也就無疾而終了。
            前面寫過幾篇逆向工程的文章,前幾天翻出來看了下,感覺像是另一個(gè)人寫的天書,我自己看自己的文章尚且如此,別人就更不用說了,其實(shí)對(duì)大部分人而言,關(guān)心的只是逆向的成果。對(duì)新劍俠情緣的資源和相關(guān)渲染感興趣的朋友可以單獨(dú)Email我。
              開始閱讀Ogre代碼正是在這百無聊賴的狀態(tài)下開始的,Ogre推出來很多年了,貌似05年就聽說朋友說起過這個(gè)項(xiàng)目,不過我一向是專注服務(wù)端開發(fā),對(duì)客戶端開發(fā)經(jīng)驗(yàn)不是很多,在3D領(lǐng)域就完全是的新手了,所以一直也沒仔細(xì)研究。這幾天拿起原來下載的一個(gè)版本,簡(jiǎn)單讀了下代碼。
            Ogre的結(jié)構(gòu)還是很清晰的,和手冊(cè)上說的一樣,主要就是那幾個(gè)對(duì)象,Demo大部分也很簡(jiǎn)單,代碼量不多,看起來很振奮人心。
            但是對(duì)我這樣的新手來說,首先想了解的當(dāng)然是渲染流程。 Ogre的渲染流程確實(shí)會(huì)讓3D新手不適應(yīng),它是從RenderTarget開始的,一個(gè)RenderTarget可以有幾個(gè)ViewPort,每個(gè)ViewPort都有一個(gè)獨(dú)立的攝像機(jī),這可以實(shí)現(xiàn)同屏幕多個(gè)渲染。
            通過ViewPort對(duì)象的update調(diào)用
             mCamera->_renderScene(this, mShowOverlays);
            來執(zhí)行場(chǎng)景渲染,而場(chǎng)景渲染里,最重要的要算_findVisibleObjects了,
            這個(gè)函數(shù)將可見的物體添加到渲染隊(duì)列里,這個(gè)函數(shù)非常的繞,里面還用到了Vistor,精神不好容易被繞暈,好在我挺住了,熬過來了。
            熟悉了大致的渲染流程后,我覺得該寫點(diǎn)東西來實(shí)戰(zhàn)了。
            3D教程的開始一般會(huì)教大家畫三角形,所以我也想用Ogre畫個(gè)三角形玩玩,
            一開始,我也想從像那些Demo一樣從ExampleApplication繼承,不過我發(fā)現(xiàn)這樣啟動(dòng)太慢了,而且我不需要加載那么多的材質(zhì),
            所以自己手動(dòng)Configure了,代碼如下:
            Ogre::LogManager* pLogManager = new Ogre::LogManager;
                Ogre::Log* pLog = pLogManager->createLog("ogreLearn1.log");
                pLog->setDebugOutputEnabled(true);

                Ogre::Root* pRootObject = new Ogre::Root;
                pRootObject->loadPlugin("RenderSystem_Direct3D9_d.dll");
                pRootObject->loadPlugin("Plugin_OctreeSceneManager_d.dll");
                
                Ogre::RenderSystem* pRenderSystem = pRootObject->getRenderSystemByName("Direct3D9 Rendering Subsystem");
                pRenderSystem->setConfigOption("Full Screen", "False");
                pRootObject->setRenderSystem(pRenderSystem);   
                Ogre::RenderWindow* pRenderWindow = pRootObject->initialise(true);

            編譯測(cè)試了下,可以正常運(yùn)行,不過發(fā)現(xiàn)屏幕是花的,我還沒有創(chuàng)建場(chǎng)景呢,繼續(xù)添加攝像機(jī)和ViewPort以及場(chǎng)景
            // 創(chuàng)建場(chǎng)景和攝像機(jī)以及ViewPort
                Ogre::SceneManager* pSceneManager = pRootObject->createSceneManager(Ogre::ST_GENERIC, "OgreLearn1");
                Ogre::Camera* pCamera = pSceneManager->createCamera("MainCamara");
                pCamera->setPosition(0.0, 0.0, -20.0);
                pCamera->lookAt(0, 0, 0);
                pCamera->setNearClipDistance(2);

                Ogre::Viewport* pViewPort = pRenderWindow->addViewport(pCamera);
                pViewPort->setBackgroundColour(Ogre::ColourValue(0, 0, 0, 1.0f) );
                pCamera->setAspectRatio(pViewPort->getActualWidth()/pViewPort->getActualHeight() );

            最后加上pRootObject->startRendering();
            編譯運(yùn)行,一切正常,屏幕顏色也變成了想要的黑色,恩,下一步該添加三角形了,我不太喜歡用OgreManualObject,一堆的繁瑣操作。這里用自定義的Mesh來繪制3角形。
            pSceneManager->setAmbientLight(Ogre::ColourValue(0.2, 0.2, 0.2) );
                    Ogre::MeshPtr pMeshData = Ogre::MeshManager::getSingleton().createManual("Learn", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
                    Ogre::SubMesh* pSubMesh = pMeshData->createSubMesh();
                    pSubMesh->useSharedVertices = false;
                    pSubMesh->vertexData = new Ogre::VertexData;
                    pSubMesh->vertexData->vertexStart = 0;
                    pSubMesh->vertexData->vertexCount = 3;

            先設(shè)置了環(huán)境光(其實(shí)沒啥用,我后面會(huì)禁止),然后創(chuàng)建了一個(gè)自定義的Mesh,
            緊接著的是創(chuàng)建一個(gè)SubMesh,要知道Ogre中最小的網(wǎng)格就是SubMesh,創(chuàng)建好SubMesh后,要填充網(wǎng)格結(jié)構(gòu)了,
            創(chuàng)建了一個(gè)VertexData,設(shè)置頂點(diǎn)數(shù)目為3(也就是一個(gè)三角形),下面該定義頂點(diǎn)格式了,
            Ogre::VertexDeclaration* pDecle = pSubMesh->vertexData->vertexDeclaration;
                    size_t sOffset = 0;
                    pDecle->addElement(0, sOffset, Ogre::VET_FLOAT3, Ogre::VES_POSITION);
                    sOffset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3);
                    pDecle->addElement(0, sOffset, Ogre::VET_COLOUR, Ogre::VES_DIFFUSE);
                    sOffset += Ogre::VertexElement::getTypeSize(Ogre::VET_COLOUR);

            上述代碼定義了頂點(diǎn)格式,只有基本的坐標(biāo)和顏色。
            下一步將是申請(qǐng)顯存,填充頂點(diǎn)結(jié)構(gòu)。
            Ogre::HardwareVertexBufferSharedPtr vBuf = Ogre::HardwareBufferManager::getSingleton().createVertexBuffer(sOffset, 3, Ogre::HardwareBuffer::HBU_STATIC_WRITE_ONLY);
                    float* pReal = static_cast<float*>(vBuf->lock(Ogre::HardwareBuffer::HBL_DISCARD));
                    Ogre::RGBA* pColor = NULL;

                    *pReal++ = -2.0f;
                    *pReal++ = 0.0f;
                    *pReal++ = 0.0f;
                    pColor = (Ogre::RGBA*)pReal;
                    pRenderSystem->convertColourValue(Ogre::ColourValue(1.0f, 0.0, 0, 0.0f), pColor);
                    pReal = (float*)(pColor+1);
                    
                    *pReal++ = 0.0f;
                    *pReal++ = 2.0f;
                    *pReal++ = 0.0f;
                    pColor = (Ogre::RGBA*)pReal;
                    pRenderSystem->convertColourValue(Ogre::ColourValue(0.0f, 0, 1.0, 1.0f), pColor);
                    pReal = (float*)(pColor+1);


                    *pReal++ = 2.0f;
                    *pReal++ = 0.0f;
                    *pReal++ = 0.0f;
                    pColor = (Ogre::RGBA*)pReal;
                    pRenderSystem->convertColourValue(Ogre::ColourValue(1.0f, 0, 0, 1.0f), pColor);
                    pReal = (float*)(pColor+1);
                    vBuf->unlock();
                    pSubMesh->vertexData->vertexBufferBinding->setBinding(0, vBuf);
                   
                    pMeshData->load();
                    pMeshData->_setBounds(Ogre::AxisAlignedBox(-2, 0, -1, 2, 2, 1) );
            填充頂點(diǎn)后,設(shè)置網(wǎng)格包圍盒,這樣一個(gè)自定義的網(wǎng)格就創(chuàng)建好了,接下來要?jiǎng)?chuàng)建一個(gè)使用該網(wǎng)格的實(shí)體了
                Ogre::Entity* pEntity = pSceneManager->createEntity("TestEntity", "Learn");
                    pEntity->setMaterialName("BaseWhiteNoLighting");

                    pSceneManager->getRootSceneNode()->createChildSceneNode()->attachObject(pEntity);
                    pEntity->getParentNode()->setPosition(3, 0, 0);
                    pEntity->getParentNode()->rotate(Ogre::Quaternion(1.0f, 1.0f, 0, 1.0f) );

            好了,這樣實(shí)體也創(chuàng)建好了,接下來執(zhí)行渲染吧:
            pRootObject->startRendering();

            遇到的問題

              上述代碼是運(yùn)行正常的,但是一開始,我執(zhí)行的結(jié)果是看不到任何東西,跟蹤了下,發(fā)現(xiàn)實(shí)體每次都被攝像機(jī)裁剪了,才發(fā)覺自定義Mesh要自己設(shè)置包圍盒子,
            設(shè)置可包圍盒子。
             設(shè)置了包圍盒后,數(shù)據(jù)已經(jīng)進(jìn)入了D3D的渲染管道,但是還是沒看到三角形,仔細(xì)觀察,原來攝像機(jī)對(duì)著的是三角形的背面。。。
            調(diào)整攝像機(jī)后,終于能看到一個(gè)三角形了,不過是白色的。。。
            從這個(gè)癥狀看,應(yīng)該是沒有關(guān)閉光照導(dǎo)致的,但是我明明主動(dòng)調(diào)用RenderSystem關(guān)閉光照了啊,仔細(xì)跟蹤了下原來是材質(zhì)在搗亂,
            默認(rèn)的材質(zhì)是開啟了光照的,所以在渲染前的SceneManager::_setPass 的時(shí)候,開啟了光照。
            這好辦,主動(dòng)設(shè)置了關(guān)閉光照的材質(zhì)"BaseWhiteNoLighting" 后,終于看到了彩色三角形了。


            posted @ 2010-09-25 21:44 feixuwu 閱讀(1896) | 評(píng)論 (2)編輯 收藏
              2010年8月4日
              在前面的文章里,我發(fā)布了修改版的boor到http://download.csdn.net/source/2578241 ,
            支持中文pdf和中文txt,能正確顯示中文目錄。


            問題

            不過,有的朋友反映打開大的pdf文件時(shí),容易死機(jī)。
            這幾天看了下,果然發(fā)現(xiàn)問題了。 為了加快讀取pdf頁面速度,bookr一開始就加載了所有的pageTree到內(nèi)存中,
            這樣顯示特定頁面的時(shí)候,就無需查找該頁面的PageObject了,這在PC機(jī)上一般沒什么問題,PC機(jī)有虛擬內(nèi)存,即使pdf文件很大,無非是加載pageTree慢點(diǎn),
            不過在PSP上就不行了,PSP的內(nèi)存是有限的,而且沒有虛擬內(nèi)存,所以如果PageTree很大,那么很可能會(huì)導(dǎo)致內(nèi)存不夠,直接死機(jī)了。。。。


            解決辦法

            解決辦法其實(shí)也很簡(jiǎn)單,bookr啟動(dòng)的時(shí)候不加載PageTree,而是在每次顯示的時(shí)候,從Root開始便利PageTree查詢PageObject對(duì)象,任意一時(shí)刻,內(nèi)存中只有一個(gè)PageObject對(duì)象。
            這樣就就基本解決了內(nèi)存問題。下一個(gè)問題是查詢效率的問題,這個(gè)問題其實(shí)不那么嚴(yán)重,PageTree本來就是一個(gè)樹形結(jié)構(gòu),pdf的PageObject查詢可以優(yōu)化成一個(gè)樹查詢,這樣應(yīng)該會(huì)很快,實(shí)際編碼測(cè)試,
            根本感覺不到修改前后翻頁速度有明顯變化。
            修改后用 金庸全集三聯(lián)版.pdf(48.6M)測(cè)試OK。

            下載

              1、下載http://download.csdn.net/source/2578241  
                  解壓到psp/game目錄下。
              2、從http://m.shnenglu.com/Files/feixuwu/EBOOT.rar 下載,解壓后,覆蓋原來bookr目錄下的EBOOT.PBP文件即可。


            posted @ 2010-08-04 17:41 feixuwu 閱讀(2000) | 評(píng)論 (2)編輯 收藏
              2010年7月26日
              PSP上的閱讀軟件我所知道的有bookr和XReader(沒用過)等,我比較喜歡bookr,不過bookr在閱讀很多pdf時(shí),中文會(huì)顯示成亂碼,閱讀txt時(shí),根本就無法顯示中文,
            這確實(shí)很讓人不爽。
            過年前閑著沒事,順便解決了bookr中文問題,本文記錄了那段時(shí)間的工作:如何從官方版bookr修改,解決pdf中文亂碼問題,支持txt中文、中文目錄顯示的問題,拋磚引玉和大家分享下整個(gè)的思路
            和問題的解決方式,解決過程比較丑陋,希望高手多多指點(diǎn)。
            貌似這里不能貼超過2M的附件,這里就不貼出修改后的bookr的發(fā)布文件了,在PSP2000測(cè)試通過,最近也一直在用,需要的同學(xué)可以email問我要。
            注:已經(jīng)上傳到csdn:http://download.csdn.net/source/2578241
            內(nèi)置了少量字體,大家可以自行擴(kuò)展字體。


            準(zhǔn)備工作

              1、搭建PSP開發(fā)環(huán)境。sourceforge上集成的安裝包,下載下來直接安裝即可,這里不多說了。
              2、下載bookr源碼,我下載的是0.7.1版本。可以用SVN下載最新的,也可以在sourceForge下載打包的源碼包。

            解決PDF中文問題

              bookr的代碼結(jié)果很清晰,由于是用c++開發(fā)的,所以代碼很好理解,BKLayer 是基礎(chǔ)的顯示類,BKDocument是基礎(chǔ)的文檔處理類。
            bookr支持pdf、txt、html和 PalmDoc(我沒用過這種),分別由從BKDocument的派生類來處理。
            pdf是由BKPDF來處理的。解決pdf中文問題的關(guān)鍵就在BKPDF類了。

               1、如何調(diào)試

                   PSP開發(fā)首先遇到的問題是調(diào)試,PSP并不能實(shí)時(shí)調(diào)試,這確實(shí)是個(gè)問題。好在Bookr源碼級(jí)支持了跨平臺(tái),在windows下,使用Makefile.cygwin 就可以編譯一個(gè)windows版本
            的Bookr了。一般來說,在windows下bookr運(yùn)行正常,大致在PSP上的版本也是正常的,當(dāng)然,在細(xì)節(jié)上其實(shí)是有差別的。另外,還可以通過日志來實(shí)現(xiàn)跟蹤。

               2、解決字體問題

                  實(shí)際上,有些中文pdf用bookr打開時(shí)是正常的,比如Programming_In_Lua.pdf,但是在打開經(jīng)常溫習(xí)的金庸全集的時(shí)候,就出現(xiàn)亂碼了。
            注意到Bookr的pdf顯示其實(shí)是主要是mudpf來實(shí)現(xiàn)的。那么bookr中文顯示問題到底是bookr自身的還是mupdf的呢?
            為了確認(rèn)這個(gè)問題,先從sourceforge下載mupdf,建立一個(gè)vs2005的項(xiàng)目,編譯,OK,可以運(yùn)行了,打開金庸全集一看,還是亂碼,這下基本可以確認(rèn),這個(gè)
            問題是從mupdf就存在了。要定位中文顯示亂碼的問題,自然需要了解pdf的內(nèi)部格式,從adobe的官網(wǎng)下載了最新的pdf手冊(cè),打開一看,一共700多頁。。。
            好在我們不需要從頭開始閱讀,只要挑關(guān)鍵的地方看就可以了,中文問題一般是因?yàn)樽煮w引起的。

            所以我們可以直接挑Text的字體相關(guān)部分看。
            mupdf的字體加載在pdf_loadfont里,從pdf手冊(cè)可知,pdf支持了若干種字體,反正我是沒什么耐心看下去,直接打開金庸全集單步調(diào)試了下,
            發(fā)現(xiàn)問題在TrueType字體的加載里,TrueType字體的加載是loadsimplefont來處理的,通過跟蹤可知,在獲取字體的FontDescriptor的時(shí)候失敗了,然后就是用內(nèi)置的默認(rèn)字體來處理了。
            默認(rèn)字體都不支持中文,所以自然就顯示成亂碼了。
            最簡(jiǎn)單的辦法就是只要是TrueType字體,不管具體是什么字體,都強(qiáng)制從硬盤加載一種指定字體(例如黑體),當(dāng)然,這樣會(huì)導(dǎo)致我們看到的pdf和實(shí)際應(yīng)該顯示的樣子有差別,只有一種字體了。
            讓我們先這樣試試吧:在發(fā)現(xiàn)pdf_loadfontdescriptor 加載失敗后,強(qiáng)制用loadCustomFont 加載硬盤指定字體"font/simhei.ttf",這樣字體加載的問題貌似解決了。
            編譯,運(yùn)行,發(fā)現(xiàn)還是有問題,這次的問題在文字編碼了。

               3、編碼問題

                一般顯示不正常的中文pdf都是GBK編碼的,mupdf的顯示是通過如下兩個(gè)步驟來做的:
               首先解碼,將文字內(nèi)容全部轉(zhuǎn)化成cid,然后將要顯示的cid全部push到一個(gè)隊(duì)列,然后遍歷cid,將cid轉(zhuǎn)化成gid(對(duì)trueType就是轉(zhuǎn)成unicode),接著顯示。
            mupdf本身有一個(gè)比較優(yōu)雅的辦法來解碼,通過pdf_lookupcmap來得到unicode,我用了比較笨的辦法:自己暴力做GBK到unicode的轉(zhuǎn)換。
            一般在windows和linux下都有庫或者API來完成編碼轉(zhuǎn)換問題。不過在PSP下卻沒有這樣的API,只好自己做一個(gè)編碼轉(zhuǎn)換了,
            在http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT 找到了轉(zhuǎn)換表,復(fù)制粘貼到txt文本,用lua腳本處理下,生成一個(gè).c文件,分別將GBK和unicode值存儲(chǔ)到2個(gè)數(shù)組里,
            現(xiàn)在你一定知道怎么轉(zhuǎn)換了:二分查找到指定GBK值在GBK數(shù)組的下標(biāo),然后直接在unicode數(shù)組用這個(gè)下標(biāo),可以得到對(duì)應(yīng)的unicode值。
            編譯測(cè)試,OK了,終于能正常顯示中文了。
            不過到現(xiàn)在為止,整個(gè)頁面只有一種字體,要解決這個(gè)問題,我們可以根據(jù)名字匹配來找到指定的字體,名字匹配不到的,使用默認(rèn)的字體(我是用的simsun.ttf)。

             txt中文問題

               相對(duì)來說txt的中文問題比較好解決了,基本都是些常規(guī)開發(fā),從FzFont.cpp代碼可知,txt顯示不了中文主要是字體加載的時(shí)候,只加載了前面的256個(gè)字形。那么我們只要做2件事情就可以顯示中文了:
              1、文字解碼,現(xiàn)在大部分的txt電子書都是gbk編碼的,這樣比較省空間,解碼算法前面已經(jīng)提過了。
              2、中文文字紋理管理和效率問題。一般在PC游戲中,中文字體一般都是將多個(gè)連續(xù)的漢字按照存儲(chǔ)到一張64X64的紋理中,這樣可以節(jié)省顯存,降低渲染批次(3D菜鳥的簡(jiǎn)單推測(cè))。
            不過如果在psp也這樣做,會(huì)發(fā)現(xiàn)顯示頁面是在太慢,最后發(fā)現(xiàn),最簡(jiǎn)單的辦法居然是每個(gè)漢字一個(gè)紋理,當(dāng)然要實(shí)際用到的時(shí)候才生成該漢字紋理。
              3、顯示頁分割。bookr閱讀txt的時(shí)候,會(huì)自己將電子書分割成若干頁面,并且支持書簽功能,因此,不可避免的涉及到頁面分割問題,引入中文顯示后,這里稍微有點(diǎn)不同,要注意不能拆分一個(gè)漢字,
            當(dāng)然,還有其他細(xì)節(jié)需要處理,這里不多說了。

            目錄中文問題

              必須承認(rèn),這是我遇到的最痛苦的問題了,原因是從API上就有問題了,讀取出來的目錄名居然是Shift-jis編碼的,誰讓PSP是索尼產(chǎn)的呢,我嘗試過將PSP的語言設(shè)置、時(shí)間等本地化設(shè)置改成中文、中國(guó)等。
            發(fā)現(xiàn)讀取到的目錄名還是shift-jis編碼。我先將目錄名從shift-jis轉(zhuǎn)回gb2312,然后顯示,結(jié)果發(fā)現(xiàn)很多漢字丟失,因?yàn)閺膅b2312轉(zhuǎn)到shift-jis的時(shí)候已經(jīng)失真了,轉(zhuǎn)回來顯示很多字體就顯示不了了。
            看上去這個(gè)問題無法解決了,其實(shí)不然,PSP的API提供了打開記憶卡設(shè)備的功能,這樣,我們自己做一個(gè)FAT32驅(qū)動(dòng)(叫驅(qū)動(dòng)不合適,其實(shí)就是自己讀取FAT32文件系統(tǒng)管理文件)就可以了,F(xiàn)AT32的文檔
            到處都是,linux下也有vfat文件系統(tǒng)的實(shí)現(xiàn),不過我偷懶了,我直接從PMP Player的代碼里拷貝了FAT32相關(guān)文件,直接移植過來,修改了FzScreenPsp.cpp文件里目錄相關(guān)的目錄讀取函數(shù),
            然后修改目錄相關(guān)顯示代碼后,編碼顯示,一切終于解決了。。。

             


            posted @ 2010-07-26 21:41 feixuwu 閱讀(7023) | 評(píng)論 (5)編輯 收藏
            僅列出標(biāo)題  下一頁
            <2025年6月>
            25262728293031
            1234567
            891011121314
            15161718192021
            22232425262728
            293012345

            文章轉(zhuǎn)載請(qǐng)注明出處

            常用鏈接

            留言簿(11)

            隨筆分類

            隨筆檔案

            搜索

            •  

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            久久精品无码免费不卡| 99久久精品国产一区二区| 亚洲狠狠综合久久| 久久国产乱子伦精品免费午夜| 亚洲国产天堂久久久久久| 亚洲中文字幕久久精品无码APP | 色诱久久久久综合网ywww | 久久99精品国产麻豆宅宅| 国内精品伊人久久久久777| 国内精品久久久久久野外| 日本精品一区二区久久久| 久久久久久久人妻无码中文字幕爆 | 久久久久久亚洲精品无码| 亚洲AV无码久久精品色欲| 91久久成人免费| 精品人妻伦九区久久AAA片69| 国产ww久久久久久久久久| 亚洲国产欧洲综合997久久| 久久精品视频91| 国产高潮久久免费观看| 99999久久久久久亚洲| 久久久久亚洲AV无码观看| 香港aa三级久久三级老师2021国产三级精品三级在 | 久久国产精品99精品国产| 99精品国产在热久久| 国产精品久久久久久五月尺| 亚洲国产成人久久精品影视| 欧美va久久久噜噜噜久久| 亚洲天堂久久久| 一本大道久久香蕉成人网| 久久精品人妻一区二区三区| 99久久国产免费福利| 久久99精品国产麻豆宅宅| 色综合久久久久综合体桃花网| 一本色道久久99一综合| 亚洲欧美一区二区三区久久| 国产精品成人久久久| 久久精品国产乱子伦| 香蕉久久夜色精品升级完成| 亚洲国产精品无码久久一线| 九九久久自然熟的香蕉图片|