那些年,我在游戏开发中改过的bug:坑爹的Vista与中间件
繼續說那些奇葩的Bug。
靠不住的系統組件:Vista和Speech Recogition
我們游戲使用了語音識別,使用了DirectX里面的XAudio來采集聲音。Windows Vista里面有一個語音識別的組件,啟動那個程序,然后玩我們的游戲,玩了一段時間,因為沒有使用麥克風,那個程序會自動關閉使用麥克風,然后我們的游戲就Crash了。又是一個跨進程的奇葩Bug。
從現場來看,Crash在使用XAudio的庫代碼里面。看不出什么線索,最后發現在日志里面,Crash前一會兒,XAudio的dll被Unload掉了。Vista那個語音組件很強悍,也會跨進程追殺大法,在自己進程把我的進程dll給Unload掉了。
雖然遇事先要懷疑自己的代碼有問題,但這個情況太匪夷所思,所以我試圖推卸責任,我跑了幾個其他DirectX程序和Demo,但凡用到XAudio組件的,在Vista Speech Recognition這一大招前無一幸免,全部Crash。
問題不在我們這里,但是作為一個職業殺蟲人,我有著“只解沙場為國死,何須bug裹尸還”的覺悟,決定還是要想辦法搞定它。
我猜這個組件退出的時候,廣播了點什么消息,讓系統所有進程去卸載這個Dll。我找了一個微軟提供的庫Detours Express,專做Hook API的勾當。它會反匯編API的入口代碼,然后動態替換入口,插入jmp指令去執行我們自己的函數。先看了文檔,搞懂這個東西怎么用,然后猜測是一個OS內部的消息導致Unload DLL,我們就攔截了RegisterWindowMessage。一通翻箱倒柜,啥有意義的東西都沒發現。
再用spy++來攔截消息,發現有一個MSUIM.msg.rpcsendreceive的消息面目猙獰,很可疑。就在收到這個消息以后,Dll就被Unload了。于是我們用Detours截住這個消息,直接返回。
以為搞定了,但結果還是不行,有幾條別的消息也會觸發Unload Dll,我一一用Detours攔截返回。攔截的消息越來越多,看來不是個解決辦法。于是我懷疑我們沒有攔截到正確的消息,懷疑OS啟動的時候便注冊了一堆內部用的消息,而不是在運行進程的時候才注冊。那些消息里肯定有我想要的,我便異想天開地Hook了User32.dll,攔截下所有消息,在安全模式下把修改過的user32.dll覆蓋原來的文件,然后滿懷希望的重啟動Vista的時候。結果不出所料的藍屏了...User32.dll是凡人能隨便碰的嗎?這不是一個User friendly的庫,應該改名叫God32.dll。
這個bug搞了1周,最后才想到最基本的道理,說不定只是Dll內部引用計數被清掉了,所以系統就卸掉Dll了。當Vista語音組件Unload XAudio的時候,系統去看看XAudio庫有沒有什么別人引用,結果發現居然沒有,那就順手清理了。就像地上有100塊錢,你東張西望,發現沒有人宣布擁有那張錢,沒有一點點猶豫,你隨手就把它放進了褲兜,進行了回收。嗯,就是這樣的,我喜歡比喻這種修辭手法。
理論上創建XAudio設備的時候沒有做什么額外的加Dll引用計數的事情,懷疑是MS的bug,創建D3D設備、DInput設備的時候都不需要做什么特別的事情的,但他們的引用計數都沒有問題。
于是我惡搞了一下,在初始化XAudio完成以后,手動做了一次LoadLibrary,把XAudio2_1.dll強行Load一遍,相當于自行增加了引用計數。于是再無Crash。
從此我們的游戲,在Vista 32上過上了幸福的生活。
可得結論:Vista靠不住
?
性能的迷思
臨近最終版本發布的時候,測試人員發現一個問題,當在Vista上連續打游戲過7關以后,游戲幀數一下子變成原來一半了。大家覺得很詭異,都不相信,之前整天玩游戲都沒問題的,于是責令測試人員再重現幾次,否則拖出去刨坑埋了。我也沒當回事,繼續調試別的bug。結果他們測了幾遍都是這種情況,看來我必須花點力氣看看了。
先編譯了一個Profile版本,有大量的Profile代碼,可以用我們自己做的工具看Profile結果。跑游戲,一個小時后順利重現之,按下快捷鍵,截下一段性能分析數據,打開工具就開始分析。顯示結果是在某個線程的聲音處理函數里面特別慢,占了20多ms,找來做聲音的程序員,讓他也去幫忙看。自己繼續研究,發現那個聲音函數簡單,估計最多1-2ms了,絕對不會有問題的。
于是開始懷疑多線程的問題,之前有過類似bug,在加載關卡的最后一小段時間,音樂和語音會很卡,音頻程序員查不出原因。后來我發現聲音線程根本分配不出內存,聲音線程的內存分配器和主線程共享,通過Critical Section保護著。聲音線程進不了Critical Section,因為主線程那時忙于加載,正在瘋狂地分配內存,導致聲音線程被無法分配所需資源。我又不能調低主線程的優先級,否則加載速度會受到很大影響。最后解決方案是為聲音線程單獨開了一個內存分配器,不和主線程搶了。會不會是這個分配器有問題呢?我驗證了很久,把那個聲音線程的內存分配器換成絕無性能問題的版本,還是有問題,估計不是內存分配的問題。
也許是那個很慢的聲音處理函數里面有些資源被別的線程占了,游戲賬號轉讓導致在那里傻等,最后耽誤了系統所有線程的同步,使幀數下降。有了想法我就開始驗證,我在那里的每個多線程同步操作里都加上了Profile代碼,繼續花了一個小時重現bug,并檢查Profile結果,發現那個線程就是慢,沒什么特別原因,每個Critical Section都很快就過去了,也就是說,沒有哪一個部分特別慢,是整個函數被均勻地拖慢了。
忙了幾天,死去活來,找不到線索。領導決定游戲還是照樣發布,我繼續看這個問題,準備在Patch里面修正這個bug。
在一次次重現中,又簡化了重現方法,我發現只需要順序進入那幾關,不需要把每一關都打過去的,所以我用作弊碼直接完成每一關,重現一次 bug的時間縮短到10分鐘以內了。
看來自己的工具是搞不定這個問題了,請出Intel Vtune來收拾它。公司比較摳門,不肯買Vtune,只好申請了評估版,一個月試用期,應該夠搞定這個問題了。用Vtune Profile了幾次,每次拖慢的函數都不一定,而且里面真沒什么特別的地方。
又郁悶了兩天,某天看著Vtune里面紅紅綠綠的代表線程忙碌狀態的條條,突然發現有一個線程條全是密密麻麻的紅條,其他線程的紅藍條相對稀疏一點。開始猜想,是不是有某個線程太忙了,導致搶了所有的時間片,讓其他線程都沒機會拿到時間片。有了理論依據,大膽求證一番,重現一次bug,幀數很低了以后,斷下游戲,把系統里面的n個線程用二分法,Freeze一半,再運行,看幀數,然后恢復一半線程再跑,來回幾次,終于發現當某一個線程恢復以后幀數就很低了,其他線程開關都沒關系。關掉那個線程游戲馬上全速歡快跑開了。再恢復運行那個線程,又是老樣子。
找到了嫌疑目標后,我全力追查這個線程是如何創建的。先在所有創建線程的地方加上日志,輸出線程 id,重現bug后找出那個問題線程,然后對照線程 id的日志,試圖找出這個線程創建的地方。結果很杯具,那個線程根本就不是我們自己程序創建的。也就是說,OS偷偷幫我們創了一個線程。這個就比較難查了,線程創建的時候我又沒法設斷點,也不知道系統內部用什么函數去創建線程,無法用Detour去Hook API。
轉機出現在Intel的Thread Profiler,照例又是一個月評估版。Thread Profiler可以顯示出創建這個Thread的Callstack,雖然不是特別準確,不過已經是很有用的信息了。我發現那個線程創建的時候Callstack里面有Winhttp之類的函數。
繼續轉移戰場,看Msdn上介紹的Winhttp系列函數,然后搜索整個項目里面所有用到Winhttp系列函數的地方。應該是我們調用Winhttp的時候方法不正確吧,我猜。好在項目里面用的Winhttp系列函數也不多,每個地方讀一遍代碼,似乎都沒問題。繼續想,會不會是中間件的問題,我們用了一個其他分公司開發的網絡組件,那個組件沒有包含在項目里面,只是弄了個lib過來。我連忙找到那個組件的源碼,一搜Winhttp又一大堆,一個個看,也都貌似正確。
既然是連續玩n關才出問題,可能和什么資源泄露有關吧?我恍然大悟,注意看Winhttp 句柄的生命周期,發現那個中間件,在Xbox360版本上正常釋放了句柄,可是Win32上就沒有...沒什么好說的,WinHttpCloseHandle伺候,問題迎刃而解。修正這個bug耗時2周,一路殺到聲音、內存管理、網絡模塊,中間還順手修復了無數其他bug,最后終于將其正法,改動只是一行而已。
可得結論:中間件靠不住。
總結
以上是生活随笔為你收集整理的那些年,我在游戏开发中改过的bug:坑爹的Vista与中间件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Debug经验总结:优化、程序员和概率
- 下一篇: 漫谈C#编程语言在游戏领域的应用