宝爷Debug小记——Cocos2d-x(3.13之前的版本)底层BUG导致Spine渲染花屏

最近在工作中碰到不少棘手的BUG,其中的一个是Spine骨骼的渲染花屏,在战斗中派发出大量士兵之后有概率出现花屏闪烁(如下图所示),这种莫名奇妙且难以重现的BUG最为蛋疼。

前段时间为了提高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渲染代码的时候,发现一个可优化的点,就是每次添加一个渲染命令,都会重新分配一块内存用于存储顶点信息,为什么不直接使用传入的顶点信息指针呢?可能是因为后面对顶点进行了坐标转换,这样同一个顶点可能被转换多次,那么在这里使用一个简易的内存池也可以起到很好的优化作用。

时间: 2024-10-27 14:46:43

宝爷Debug小记——Cocos2d-x(3.13之前的版本)底层BUG导致Spine渲染花屏的相关文章

熬了多少个夜晚,大家期待的《网络工程师思科华为华三实战案例红宝书》即网工必备技术命令大全版本1完书

熬了多少个夜晚,最近也没空更新博客.军哥编写的大家期待的<网络工程师思科华为华三实战案例红宝书>即网工必备技术命令大全版本1完书,一本融合了思科华为华三的实战型辅导书(辅助乾颐堂QCNA课程的).不多说上图 目录关于作者 2本书读者和笔者心语 3本书内容和结构 4第1部分 网络实施基础 15案例0 模拟器的部署和连接管理 16学习利器模拟器简书 160.1 华为模拟器Ensp部署 160.2 思科模拟器EVE部署 310.3 部署SecureCrt管理网络设备 400.3.1 部署终端管理软件

Node程序debug小记

有时候,所见并不是所得,有些包,你需要去翻他的源码才知道为什么会这样.用Console来Debug 背景 今天调试一个程序,用到了一个很久之前的NPM包,名为formstream,用来将form表单数据转换为流的形式进行接口调用时的数据传递. 这是一个几年前的项目,所以使用的是Generator+co实现的异步流程. 其中有这样一个功能,从某处获取一些图片URL,并将URL以及一些其他的常规参数组装到一起,调用另外的一个服务,将数据发送过去. 大致是这样的代码: const co = requi

Pig安装及简单使用(pig版本0.13.0,Hadoop版本2.5.0)

原文地址:http://www.linuxidc.com/Linux/2014-03/99055.htm 我们用MapReduce进行数据分析.当业务比较复杂的时候,使用MapReduce将会是一个很复杂的事情,比如你需要对数据进行很多预处理或转换,以便能够适应MapReduce的处理模式,另一方面,编写MapReduce程序,发布及运行作业都将是一个比较耗时的事情. Pig的出现很好的弥补了这一不足.Pig能够让你专心于数据及业务本身,而不是纠结于数据的格式转换以及MapReduce程序的编写

IntelliJ IDEA 13.1.1版本偶然的错误

总之很悲催也很浪费时间,这款软件很喜欢,不想卸载 图片中的style.css使得style.css一直是文本形式 将style.css删除就恢复正常了,这个错误弄了半天才搞定,心累.

Intellij Idea 13 vmoptions (Mac版本)

-ea -server -Xms1g -Xmx1g -Xss16m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:+DoEscapeAnalysis -XX:+UseCompressedOops -XX:+UnlockExperimentalVMOptions -XX:+UseConcMarkSweepGC -XX:LargePageSizeInBytes=256m -XX:ReservedCodeCacheSize=96m -XX:+UseCodeCac

Android - 解决ViewPager嵌套时在API 13及其以下版本中不能滑动的问题

通过对ViewPager事件处理的分析发现解决此问题的关键点在于判断是否可以横向滑动的部分,也就是canScroll(View, boolean, int, int, int)方法 在此方法中先依次递归判断子View是否可以横向滑动,在最后一行则判断自己是否可以横向滑动.关键处在于调用了ViewCompat.canScrollHorizontally(View, int)方法来判断是否可以横向滑动.进一步查看ViewCompat.canScrollHorizontally(View, int)的

tcp中 fast_open 学习 nginx 13年的版本开始支持该功能

https://www.cnblogs.com/lanjianhappy/p/9868622.html 三次握手的过程中,当用户首次访问server时,发送syn包,server根据用户IP生成cookie,并与syn+ack一同发回client: client再次访问server时,在syn包携带TCP cookie: 如果server校验合法,则在用户在 server 回复ack前就可以直接发送数据 (即不需要第三步 和 服务器的第二部的 SYN+ACK):否则按照正常三次握手进行. TFO

V 9 saltstack (1)

salt是一个新的基础平台管理工具,2011-02-20诞生,创造者Thoms SHatch,起名salt原因生活中常见.易记,使用saltstack.com原因这个域名没有被注册,Because salt goes everywhere: 部署简单,只需花费很短时间即可运行起来,扩展性足以支持管理上万台server: 速度很快,毫秒级通信,server间完成数据传递足够快: 用python语言开发: 工作方式:Master/Minion(ZeroMQ),Masterless,Salt-SSH(

一个基于JRTPLIB的轻量级RTSP客户端(myRTSPClient)——实现篇:(五)用户接口层之提取媒体流数据

当RTSP客户端向RTSP服务端发送完PLAY命令后,RTSP服务端就会另外开启UDP端口(SDP协商定义的端口)发送RTP媒体流数据包.这些数据包之间会间隔一段时间(毫秒级)陆续被发送到RTSP客户端,此时RTSP客户端可以调用GetMediaData等接口获取媒体流数据. 一.uint8_t * RtspClient::GetMediaData(string media_type, uint8_t * buf, size_t * size, size_t max_size) 该函数的作用即获