那些年,我在游戏开发中改过的bug:靠不住的OS和SDK
記憶中有很多次了,幾個程序員朋友聊天,聊著聊著,就聊到自己遇到過的bug。然后大家開始口沫橫飛交流那些或詭異或神奇的bug,談?wù)撟约寒?dāng)年是如何搞定bug或是被bug搞定。
正好看見Gamesutra上也登了篇Dirty Coding Tricks ,發(fā)現(xiàn)老外也有這個癖好,原來天下程序員本一家。一路走來,程序員的成長,便是一路刀光劍影,與bug斗個你死我活。了解別人的Debug過程,或是回憶一下自己Debug的時候思路,也是很有意思的事情,值得定期總結(jié)。
下面分享一下自己遇到過印象深刻的bug:
靠不住的c:Memcpy的傳說
做一個PC項目的時候,兇猛的測試兄弟把Winxp 64單獨列出來,作為一個測試平臺,然后我們的噩夢就開始了。游戲在Winxp 64上面頻繁Crash,經(jīng)常在更新Octree的時候訪問到空指針,但邏輯上來看那個指針不可能是空的。Crash位置很隨機,到處都有,通常都是玩了一個小時在一個莫名其妙的地方Crash。
第一反應(yīng)就是那些地址被非法訪問,可能是某個錯誤的指針指向那里,往里面寫了不該寫的值。于是我根據(jù)最常Crash的地址設(shè)下數(shù)據(jù)斷點,試了好幾天,從來沒有斷下過,Crash還是依舊。然后同事試圖加上大量的保護代碼,判斷一個指針是不是空指針后才使用,很好的降低了Crash機率,但偶爾還是會有。想想問題根源沒有找到,降低Crash概率只是讓自己更難修bug,而且訪問Octree也比較多,亂加保護會影響性能,我一狠心又把保護代碼全去掉了。
?
來回幾輪搞下來,根據(jù)某次比較靠譜的Crash Callstack,懷疑到了memcpy。memcpy是個老同志了,兢兢業(yè)業(yè)地忙碌在各個程序里,負責(zé)搬運數(shù)據(jù)很多年,工作績效有口皆碑。它有什么問題呢?它還能有什么問題呢?
為了保證多線程能同步并行執(zhí)行,我們程序中有個很大的memcpy,把一塊Octree從后臺用memcpy復(fù)制到前臺的工作buffer。當(dāng)然這個做法的設(shè)計優(yōu)劣不在此討論,存在即合理,2007年,多線程引擎我們還不是那么擅長搞。
既然有懷疑,就要捉奸。我做了試驗,在memcpy后面直接加一個循環(huán),逐字節(jié)比較源數(shù)據(jù)和目標(biāo)數(shù)據(jù),有時候居然會不相等... 這個可顛覆了我的世界觀。我試圖寫了一個函數(shù),里面就一個循環(huán),逐字節(jié)復(fù)制數(shù)據(jù),然后把所有的memcpy全替換成這個函數(shù),果然不Crash了。但顯然這是不行的,速度太慢了。
既然有了點線索,就可以試圖簡化bug重現(xiàn)條件了。我不能每次都花一個小時去運行游戲,尋找那一次crash。我在游戲load起來,開始走主循環(huán)后,加了一個死循環(huán),不停用memcpy復(fù)制一塊內(nèi)存,然后比較源數(shù)據(jù)和目標(biāo)數(shù)據(jù)。源數(shù)據(jù)里面沒有0,都是1-255的值,可是運行幾十秒以后目標(biāo)數(shù)據(jù)居然有0。這樣,我們成功地把重現(xiàn)一次bug的平均時間從一個小時降低到一分鐘。
我們的懷疑從3d代碼轉(zhuǎn)移到多線程,在進入那個死循環(huán)之前,我們設(shè)下斷點,把其他無關(guān)的線程全部都Freeze,只有那個線程會運行。這樣,任何多線程的干擾全部排除,memcpy在一個理想的環(huán)境中歡快的運行,但memcpy還是會出錯。
繼續(xù)簡化,我單獨寫一個小程序,里面只做死循環(huán)和memcpy,游戲賬號交易平臺來判斷是不是OS的問題(實在是走投無路了)。試驗結(jié)果是,Winxp 64沒有問題,memcpy始終如一地正常工作著(本該如此^_^)。 可是某一次,當(dāng)我們的游戲在后臺運行的時候,再啟動這個小程序,居然memcpy又出問題了...無語了,原來我們的游戲還能萬里追殺,跨進程搞垮OS下面的其他進程。
山窮水盡疑無路,我無奈下單步跟蹤了一下memcpy的匯編代碼,上來有兩句
也就是說,memcpy上來看有沒有設(shè)置__sse2_available,這個值估計是CRT庫里面設(shè)的,如果有SSE2就執(zhí)行sse優(yōu)化的memcpy,沒有就跳到Dword_align那里執(zhí)行普通版本的流程。我開始懷疑是不是我們的游戲里面對系統(tǒng)做了點什么手腳,導(dǎo)致在__sse2_available允許的情況下,優(yōu)化的代碼會執(zhí)行出錯。游戲的代碼規(guī)模實在太大,又用了n個中間件,我無力一一查看,且我也看不懂SSE代碼(哎呀好羞射),就隨手做了個試驗,在那句判斷的地方,通過Debugger把__sse2_available的值改成了0。從此memcpy再也不出錯了。
所以最終的解決方案是,Win64下,我在游戲一開始初始化的地方,加上謎一樣的初始化代碼:
?
這樣memcpy就永遠使用不做sse2優(yōu)化的代碼了。
memcpy不使用sse2后會不會有性能問題?經(jīng)過測試,發(fā)現(xiàn)問題不大,對于頻繁調(diào)用的少量數(shù)據(jù)復(fù)制,memcpy不太能從sse2里面得到多少好處。對于大量數(shù)據(jù)的復(fù)制,我們用得也不多,profile了一下,沒有發(fā)現(xiàn)明顯的瓶頸,無視了。這事情也可以從反向理解,由于游戲規(guī)模太大,各種多線程和GPU/CPU同步,導(dǎo)致任何一點的效率損失,可能不會擴散到整個游戲,被其他同步和等待吸收掉了…我真是一個好程序員,能想到這么好的理由說服自己。
對游戲跨進程影響其他程序的memcpy,實在沒能力解決了,Winxp 64是一個太小眾的環(huán)境,用戶要么用Winxp 32, 要么用Vista 32/64,市場占有率很低,我們也算仁至義盡了。
可得結(jié)論:Winxp 64靠不住(其實問題還是應(yīng)該出在我們內(nèi)部,不過其他OS都沒問題,就賴在它身上了)
?
為了達成目的,我們要不擇手段。
靠不住的SDK:OutputDebugString
話說當(dāng)年開發(fā)Splinter Cell 4,使用的還是XBOX360的Alpha kit。微軟早期的360 Kit,全不像后期的Kit,后期kit長得和主機差不多。而當(dāng)時的KIt,是用一個很大的Power Mac G5,換上一塊ATI顯卡,再刷上MS的固件,連馬甲都不穿一件就出來見人了。
Xbox 360開發(fā)SDK,早期bug一大堆,比如預(yù)編譯頭文件太大了,編譯器抱怨說預(yù)編譯頭文件預(yù)留內(nèi)存不夠,這個好辦,加上/Zm512編譯選項即可。加上,編譯,沒用?!只好寫信去MS問,他們說,哦,原來如此啊,今天天氣真好,哈哈哈哈,請等待下一次更新,謝謝您匯報云云...雖是MS的bug,可是我也不能等著他修復(fù)才工作,只好手動拆分預(yù)編譯頭文件,把很多內(nèi)容放在預(yù)編譯頭文件外面,預(yù)編譯頭文件就會變小,編譯就可以順利通過了。
扯遠了,回頭來說這個OutputDebugString的問題。
360有3個cores,每個core有2個Hyperthreads,總計6個線程,我們的游戲在邏輯線程、聲音線程和渲染線程外,還開了3個輔助線程,用自己寫的Thread Pool管理系統(tǒng)來管理這些線程,初始化的時候就是一個循環(huán)把每個線程創(chuàng)建出來。
這個bug的表現(xiàn)現(xiàn)象就是加載失敗,程序僵死。團隊當(dāng)時有幾十個測試人員,每天打游戲八小時,從沒碰到過游戲加載失敗情況。但是開發(fā)人員這里就有很低概率會加載失敗,表現(xiàn)情況為用VC啟動游戲,然后過一會加載屏幕就僵死不動了。開發(fā)人員往往過了5分鐘還沒在電視上看見游戲畫面才知道游戲又掛了,重現(xiàn)概率是每個程序員每一到兩天碰到一次。我們擔(dān)心這是線程管理系統(tǒng)內(nèi)部的問題,就讓每個開發(fā)人員碰到這種情況不要急著重啟動,把現(xiàn)場給我看一下。每次僵死的時候都是在系統(tǒng)內(nèi)核死鎖,Callstack也沒有有價值的信息,基本都是在創(chuàng)建每個線程的時候打印一句語句的時候就內(nèi)核就死了。在接下來一周里,我試圖在線程管理系統(tǒng)內(nèi)部加一些日志輸出,每次重現(xiàn)bug的時候查看日志,也沒有找到更好的線索。
重現(xiàn)概率實在太低,不好調(diào)試,于是我試圖用簡單的程序片段來重現(xiàn)這個bug。因為都是創(chuàng)建新線程時候死鎖,第一個想到的就是寫一個小程序,直接一個死循環(huán),創(chuàng)建線程,打印日志,然后殺掉線程,重復(fù)再做。果然能夠重現(xiàn)bug,程序運行1分鐘以后,就死鎖了。同樣的現(xiàn)場,還是在OutputDebugString內(nèi)部死鎖了。難道bug不是在線程庫,而是在OutputDebugString內(nèi)?
?
正好那些天有個微軟360開發(fā)組的人員在我們組Onsite支持,于是他帶著大量的360 Sdk的符號庫(Symbol)來幫助調(diào)試,因為他不是做這一個模塊的,最后也沒有什么結(jié)果。最后他把我的小程序發(fā)回微軟,找到內(nèi)部開發(fā)人員處理,這比我們直接走正式support流程快很多。
?
一天后,微軟發(fā)回Email,說這是內(nèi)部的Bug,請無視,不會影響Release版本,是Debug協(xié)議上的問題。
可得結(jié)論:微軟靠不住。
總結(jié)
以上是生活随笔為你收集整理的那些年,我在游戏开发中改过的bug:靠不住的OS和SDK的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈RTS游戏网络同步:3种同步机制模式
- 下一篇: 游戏编程设计模式——Game Loop