寶爺Debug小記——Cocos2d-x(3.13以前的版本)底層BUG致使Spine渲染花屏

最近在工做中碰到很多棘手的BUG,其中的一個是Spine骨骼的渲染花屏,在戰鬥中派發出大量士兵以後有機率出現花屏閃爍(以下圖所示),這種莫名奇妙且難以重現的BUG最爲蛋疼。node


 
前段時間爲了提升Spine骨骼動畫的加載速度,將Spine庫進行了升級,新的Spine庫支持skel二進制格式,二進制格式的加載速度比json格式要快5倍以上。
 
這是一個大工程,遊戲中全部的骨骼動畫都須要使用更高版本的Spine編輯器從新導出,因爲部分美術沒有對源文件進行版本管理,丟失了源文件,致使部分骨骼動畫要從新制做,浪費了很多時間。咱們對代碼進行了嚴格的版本管理,而且大受裨益,但美術的源文件管理確實很容易被忽視,因此在這裏吃了一個大虧。升級版本以後,部分使用了翻轉的骨骼出現了一些問題,須要美術逐個檢查,從新設置翻轉以後再導出。
 
使用了新版本的Spine庫,除了二進制格式的支持外,渲染方面也進行了一個優化,使用TriangleCommand替換了原先的CustomCommand,這使得多個骨骼動畫的渲染能夠被合併,原來的版本每一個骨骼至少佔用一個drawcall。另外新Spine使用的頂點Shader也發生了變化,致使以前使用的舊Shader也須要跟着調整頂點Shader。
 
接下來,讓咱們開始Debug,首先排查一下骨骼動畫的問題,同一個關卡,我讓測試人員幫忙以很高的頻率出兵,可是隻出一種兵,看看花屏是否是某種兵的渲染致使的。結果是每種兵出到必定的數量以後都會出現這個問題,可是不一樣的兵種出問題的時間不一樣,其中的大樹人兵種在派出了6個以後就會出現花屏的問題,而其餘兵種則比較難出現。
 
那麼大樹的骨骼和其餘幾個骨骼有什麼不一樣呢?詢問美術人員以後,得知大樹這個骨骼動畫使用了較多的Mesh,也就是Spine中的網格功能,這個功能可讓2D的圖片實現柔順的扭曲效果,例如毛髮、衣物的飄揚效果。
 
既然是Spine的網格出問題,那麼是否由於Spine的版本問題致使?編輯器導出的版本與Spine運行庫的版本不匹配致使的,根據文檔讓美術使用了3.3.07,3.5.35和3.5.51版本的Spine編輯器導出骨骼,並使用了3.5.35和3.5.51的運行庫進行測試,都存在這個問題。
 
接下來我開始對比Spine的渲染代碼,對比上一版本(升級前的Spine,也就是Cocos2d-x3.13.1以前的Spine庫),上一版本使用的是本身的批渲染,而最新版本是TriangleCommand,嘗試改回去,但代碼和數據結構已經發生了較大的改動,強制改回去以後發現渲染效果更加糟糕了。
 
閱讀了Spine的渲染代碼以後,嘗試跳過spine的網格渲染,我添加了一個測試用的靜態變量,而後在運行中打斷點,以後動態修改這個變量的值,來控制程序的運行流程,逐個跳過Spine的渲染類型,最後定位到只要把網格渲染跳掉,出再多的大樹人也不會致使花屏。我想或許有些沒有程序員精神的程序員到這裏就會結案,而後通知美術人員去除全部網格,從新導出資源。但我決定認真分析下爲何這個網格渲染會致使花屏。
 1 static int skiptype = 0;  2  
 3 void SkeletonRenderer::draw (Renderer* renderer, const Mat4& transform, uint32_t transformFlags) {
 4     SkeletonBatch* batch = SkeletonBatch::getInstance();
 5  
 6     for (auto t : _curTriangles)
 7     {
 8         TrianglesMgr::getInstance()->freeTriangles(t);
 9     }
10     _curTriangles.clear();
11     _triCmds.clear();
12  
13     Color3B nodeColor = getColor();
14     _skeleton->r = nodeColor.r / (float)255;
15     _skeleton->g = nodeColor.g / (float)255;
16     _skeleton->b = nodeColor.b / (float)255;
17     _skeleton->a = getDisplayedOpacity() / (float)255;
18  
19     Color4F color;
20     AttachmentVertices* attachmentVertices = nullptr;
21     for (int i = 0, n = _skeleton->slotsCount; i < n; ++i) {
22         spSlot* slot = _skeleton->drawOrder[i];
23         if (!slot->attachment) continue;
24         if (slot->attachment->type == skiptype) continue; 25  
26         switch (slot->attachment->type) {
27         case SP_ATTACHMENT_REGION: {
28             spRegionAttachment* attachment = (spRegionAttachment*)slot->attachment;
29             spRegionAttachment_computeWorldVertices(attachment, slot->bone, _worldVertices);
30             attachmentVertices = getAttachmentVertices(attachment);
31             color.r = attachment->r;
32             color.g = attachment->g;
33             color.b = attachment->b;
34             color.a = attachment->a;
35             break;
36         }
37         case SP_ATTACHMENT_MESH: {
38             spMeshAttachment* attachment = (spMeshAttachment*)slot->attachment;
39             spMeshAttachment_computeWorldVertices(attachment, slot, _worldVertices);
40             attachmentVertices = getAttachmentVertices(attachment);
41             color.r = attachment->r;
42             color.g = attachment->g;
43             color.b = attachment->b;
44             color.a = attachment->a;
45             break;
46         }
47         default:
兩種渲染最後的處理都同樣,不一樣的地方就在於上面這個switch中的頂點計算部分,閱讀了一下舊版本Spine的Mesh頂點計算代碼,再看看新的Mesh頂點計算,直接吐血,本來的幾行代碼,新版本使用了幾百行代碼,都是各類複雜的計算,可讀性很糟糕...,嘗試把舊的Mesh頂點計算代碼應用到新的Spine,結果也是很是糟糕。
 
接下來我決定換一個簡單點的環境來定位問題,這 樣能夠排除其餘的干擾!我修改了一下Cocos2d-x3.13版本的TestCpp中的SpineTest進行簡單的測試,結果發現了一個有意思的現象,當我添加到第十二個樹人時渲染出現了一些奇怪的現象(美術給個人是小樹人,頂點較少,因此到第十二個纔出問題)
  
再次檢查了一下渲染的代碼後忽然注意到左下角的頂點數,當我添加第12個樹人的時候,頂點數突破了65535!記得在Cocos2d-x底層渲染中,65535是VBO頂點緩存區的最大值,接下來把目標鎖定在Cocos2d-x的渲染中。再次閱讀了一下Render的代碼,特別是TriangleCommand的渲染,調試了一下,發現渲染的頂點是2W多個,而Index索引是7W多個,難道是index的限制不能超過65535?因而把代碼中的INDEX_VBO_SIZE替換爲VBO_SIZE,這樣一次渲染中Index和Vertex都不能超過65535,改完以後,問題果真解決了。那這就結案了嗎?我以爲還得再深刻探討一下,把問題的根源完全肯定。
 1 void Renderer::processRenderCommand(RenderCommand* command)
 2 {
 3     auto commandType = command->getType();
 4     if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
 5     {
 6         // flush other queues
 7         flush3D();
 8  
 9         auto cmd = static_cast<TrianglesCommand*>(command);
10  
11         // flush own queue when buffer is full
12         if(_filledVertex + cmd->getVertexCount() > VBO_SIZE || _filledIndex + cmd->getIndexCount() > INDEX_VBO_SIZE)
13         {
14             CCASSERT(cmd->getVertexCount()>= 0 && cmd->getVertexCount() < VBO_SIZE, "VBO for vertex is not big enough, please break the data down or use customized render command");
15             CCASSERT(cmd->getIndexCount()>= 0 && cmd->getIndexCount() < INDEX_VBO_SIZE, "VBO for index is not big enough, please break the data down or use customized render command");
16             drawBatchedTriangles();
17         }
18  
19         // queue it
20         _queuedTriangleCommands.push_back(cmd);
21         _filledIndex += cmd->getIndexCount();
22         _filledVertex += cmd->getVertexCount();
23     }
24  
難道IndexCount真的不能超過65535嗎?google查閱了很多資料,glGet獲取GL_MAX_ELEMENTS_INDICES,發現其值是10W+,仔細閱讀了OpenGL超級寶典關於緩存區部分的介紹,也沒有說Index不能超過65535。Cocos2d-x底層的VBO也分配了足夠的空間。難道是頂點或者索引錯位了之類的問題致使的,因而我把動畫中止,把全部的樹人都限定在同一個位置,而後在Render的最底層,打印出每一個樹人渲染時的全部頂點和索引信息,而後對比一下只有一個樹人、11個樹人以及12個樹人渲染的頂點和索引信息有何不一樣。
 
 1 // 增長一些調試用的靜態變量  2 static bool __dbg = false;  3 static bool __deepDbg = false;  4 static int __cmdCount = 68;  5 static int __curCmdCount = 0;  6 static int __idxCount = 0;  7 static int __vexCount = 0;  8 static int __maxidx = 0;   9  
 10 void Renderer::fillVerticesAndIndices(const TrianglesCommand* cmd)
 11 {
 12     memcpy(&_verts[_filledVertex], cmd->getVertices(), sizeof(V3F_C4B_T2F) * cmd->getVertexCount());
 13  
 14     // fill vertex, and convert them to world coordinates
 15     const Mat4& modelView = cmd->getModelView();
 16     for(ssize_t i=0; i < cmd->getVertexCount(); ++i)
 17     {
 18         modelView.transformPoint(&(_verts[i + _filledVertex].vertices));
 19 // 打印全部頂點的xyz和紋理uv  20 if(__dbg && __deepDbg)  21  {  22 CCLOG("vertex %d is xyz %.2f,%.2f,%.2f uv %.2f,%.2f", i + _filledVertex - __vexCount,_verts[i + _filledVertex].vertices.x,  23 _verts[i + _filledVertex].vertices.y, _verts[i + _filledVertex].vertices.z,  24 _verts[i + _filledVertex].texCoords.u, _verts[i + _filledVertex].texCoords.v);  25  }  26     }
 27  
 28     // fill index
 29     const unsigned short* indices = cmd->getIndices();
 30     for(ssize_t i=0; i< cmd->getIndexCount(); ++i)
 31     {
 32         _indices[_filledIndex + i] = _filledVertex + indices[i];
 33 if (__dbg)  34  {  35 if (__maxidx < _indices[_filledIndex + i])  36  {  37 __maxidx = _indices[_filledIndex + i];  38  }  39 if (__deepDbg)  40  {  41 CCLOG("index %d is %d", _filledIndex + i - __idxCount, _indices[_filledIndex + i] - __vexCount);  42  }  43  }  44     }
 45  
 46     _filledVertex += cmd->getVertexCount();
 47     _filledIndex += cmd->getIndexCount();
 48 }
 49  
 50 void Renderer::drawBatchedTriangles()
 51 {
 52     if(_queuedTriangleCommands.empty())
 53         return;
 54  
 55     CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_BATCH_TRIANGLES");
 56  
 57 if (__dbg)  58  {  59 __vexCount = 0;  60 __idxCount = 0;  61 __curCmdCount = 0;  62  }  63  
 64     _filledVertex = 0;
 65     _filledIndex = 0;
 66  
 67     /************** 1: Setup up vertices/indices *************/
 68  
 69     _triBatchesToDraw[0].offset = 0;
 70     _triBatchesToDraw[0].indicesToDraw = 0;
 71     _triBatchesToDraw[0].cmd = nullptr;
 72  
 73     int batchesTotal = 0;
 74     int prevMaterialID = -1;
 75     bool firstCommand = true;
 76  
 77     for(auto it = std::begin(_queuedTriangleCommands); it != std::end(_queuedTriangleCommands); ++it)
 78     {
 79         const auto& cmd = *it;
 80         auto currentMaterialID = cmd->getMaterialID();
 81         const bool batchable = !cmd->isSkipBatching();
 82 if (__dbg)  83  {  84 if (__curCmdCount % __cmdCount == 0)  85  {  86 CCLOG("begin %d =====================================", __curCmdCount / __cmdCount);  87 __vexCount = _filledVertex;  88 __idxCount = _filledIndex;  89  }  90 ++__curCmdCount;  91  }  92  
 93         fillVerticesAndIndices(cmd);
 94  
 95         // in the same batch ?
 96         if (batchable && (prevMaterialID == currentMaterialID || firstCommand))
 97         {
 98             CC_ASSERT(firstCommand || _triBatchesToDraw[batchesTotal].cmd->getMaterialID() == cmd->getMaterialID() && "argh... error in logic");
 99             _triBatchesToDraw[batchesTotal].indicesToDraw += cmd->getIndexCount();
100             _triBatchesToDraw[batchesTotal].cmd = cmd;
101         }
102         else
103         {
104             // is this the first one?
105             if (!firstCommand) {
106                 batchesTotal++;
107                 _triBatchesToDraw[batchesTotal].offset = _triBatchesToDraw[batchesTotal-1].offset + _triBatchesToDraw[batchesTotal-1].indicesToDraw;
108             }
109  
110             _triBatchesToDraw[batchesTotal].cmd = cmd;
111             _triBatchesToDraw[batchesTotal].indicesToDraw = (int) cmd->getIndexCount();
112  
113             // is this a single batch ? Prevent creating a batch group then
114             if (!batchable)
115                 currentMaterialID = -1;
116         }
117  
118         // capacity full ?
119         if (batchesTotal + 1 >= _triBatchesToDrawCapacity) {
120             _triBatchesToDrawCapacity *= 1.4;
121             _triBatchesToDraw = (TriBatchToDraw*) realloc(_triBatchesToDraw, sizeof(_triBatchesToDraw[0]) * _triBatchesToDrawCapacity);
122         }
123  
124         prevMaterialID = currentMaterialID;
125         firstCommand = false;
126     }
127     batchesTotal++;
128 if (__dbg) 129  { 130 CCLOG("MAX IDX %d", __maxidx); 131  } 132 __dbg = false; 133  
在添加第一個樹人後,打斷點,並將__dbg和__deepDbg開啓,它會打印出本次渲染的樹人詳情,添加到第十一和第十二個的時候,再各打印一次,經過Beyond Compare對比結果,發現這些信息徹底正確,每一個樹人的全部頂點和索引都是徹底同樣的,渲染的內容並無被修改或發生錯位。那正確的內容爲何渲染不出正確的結果呢?因而繼續分析接下來的glDrawElements方法,在十二個樹人渲染的時候,斷點檢查了一下該函數的全部參數,發現了第二個參數的值出現了問題!這個值表示要渲染的頂點索引數量,在只渲染一次的狀況下, _triBatchesToDraw[i].indicesToDraw應該等同於_filledIndex纔對,而斷點看到的值卻遠小於_filledIndex,查找了一下indicesToDraw的全部引用,發現這個值在每合併一個Command的時候會加上該Command的IndexCount,而這個變量的類型是GLushort!結果終於真相大白,這個變量在不斷增長的過程當中溢出了,從而致使渲染的Index出現問題,最終致使的花屏。
1     for (int i=0; i<batchesTotal; ++i)
2     {
3         CC_ASSERT(_triBatchesToDraw[i].cmd && "Invalid batch");
4         _triBatchesToDraw[i].cmd->useMaterial();
5         glDrawElements(GL_TRIANGLES, (GLsizei) _triBatchesToDraw[i].indicesToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (_triBatchesToDraw[i].offset*sizeof(_indices[0])) );
6         _drawnBatches++;
7         _drawnVertices += _triBatchesToDraw[i].indicesToDraw;
8     }
最終的改法應該是將indicesToDraw的類型修改成GLsizei,測試經過後,開開心心地打算提交一個pull request,結果卻發現,在下一個版本3.14中,該BUG已被修復...,想一想仍是應該多升級一下引擎啊....
 
最後反思一下這個Bug,有些千奇百怪的Bug,處理到最後每每是那麼一兩行代碼的事情,整個解決Bug的流程看上去雖然很繞,但其實是先肯定並重現我呢體,再從出問題的地方——Spine一點點排查,一直到最底層的渲染邏輯。若是是用逆向思惟,可能一會兒就定位到問題了,但一開始根本沒懷疑Cocos2d-x的渲染有問題,由於Cocos2d-x的版本已經有段時間沒有升級過了,而Spine則是最近升級的。
 
因此呢,就算不升級引擎,也應該多關心一下引擎的更新日誌,瞭解修改了哪些BUG。除了程序的緣由,美術過量使用了網格,也是這個BUG的一大誘因,過量使用網格,會致使Spine骨骼動畫加載變慢,資源文件變大,並影響性能。
 
在分析Spine渲染代碼的時候,發現一個可優化的點,就是每次添加一個渲染命令,都會從新分配一塊內存用於存儲頂點信息,爲何不直接使用傳入的頂點信息指針呢?多是由於後面對頂點進行了座標轉換,這樣同一個頂點可能被轉換屢次,那麼在這裏使用一個簡易的內存池也能夠起到很好的優化做用。
相關文章
相關標籤/搜索