问题表现
我制作了一个最简单的龙骨动画,里面有骨骼嵌套,在父骨骼的时间轴上切换子骨骼的animation,正常的表现如下:
换肤后直接不动了
定位过程
因为代码是从js移植过来的,所有的经验都来自js引擎,其实native的c++引擎和js引擎比较像,但编码结构上也有差异,隐约感觉是这部分的问题。
问题比较难定位,所以我才做了这样的一个最小的demo,实际发现,排查起来也是无从下手,看来只能从最终提交顶点来反推问题了。
draw
下手的地方当然还是从OpenGL的draw
函数下手。
c++
void DeviceGraphics::draw(size_t base, GLsizei count)
{
commitVertexBuffer();// 顶点数据是在这里进行提交的
if (nextIndexBuffer)
{
// 这里调用draw,但是数据是绑定在buffer里面的,如果对OpenGL不熟悉,需要补补基础
GL_CHECK(glDrawElements(ENUM_CLASS_TO_GLENUM(_nextState->primitiveType),
count,
ENUM_CLASS_TO_GLENUM(nextIndexBuffer->getFormat()),
(GLvoid *)(base * nextIndexBuffer->getBytesPerIndex())));
}
}
分配顶点缓冲区的数据
c++
void DeviceGraphics::commitVertexBuffer()
{
if (attrsDirty)
{
for (int i = 0; i < _caps.maxVertexAttributes; ++i)
_newAttributes[i] = 0;
for (int i = 0; i < _nextState->maxStream + 1; ++i)
{
auto vb = _nextState->getVertexBuffer(i);
if (!vb)
continue;
// 仅仅做了 glBindBuffer的操作
// vb->getHandle()仅仅是buffer的id
GL_CHECK(ccBindBuffer(GL_ARRAY_BUFFER, vb->getHandle()));
auto vboffset = _nextState->getVertexBufferOffset(i);
const auto& attributes = _nextState->getProgram()->getAttributes();
auto usedAttriLen = attributes.size();
for (int j = 0; j < usedAttriLen; ++j)
{
const auto& attr = attributes[j];
const auto* el = vb->getFormat().getElement(attr.hashName);
if (!el || !el->isValid())
{
RENDERER_LOGW("Can not find vertex attribute: %s", attr.name.c_str());
continue;
}
if (0 == _enabledAtrributes[attr.location])
{
// 启用shader里面的attribute
GL_CHECK(ccEnableVertexAttribArray(attr.location));
_enabledAtrributes[attr.location] = 1;
}
_newAttributes[attr.location] = 1;
// glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
GL_CHECK(ccVertexAttribPointer(attr.location,
el->num,
ENUM_CLASS_TO_GLENUM(el->type),
el->normalize,
el->stride,
(GLvoid*)(el->offset + vboffset * el->stride)));
}
}
}
}
查到这里,其实就要追查glGenBuffers
的逻辑,但是这里很明显是没有的,那glGenBuffers
的操作就一定是在其他地方,这里仅仅是直接拿来使用。
这里需要补充一个基础知识,在creator的架构里面,是存在一个非常大的buffer,所有的顶点数据都是存放在这个大Buffer里面,这样有一个好处,就是不用频繁的开辟内存。
glBufferData
这个函数是我们下一步追踪的重点函数,因为顶点数据就是通过这个函数和buffer建立联系的(OpenGL的基础),这部分的逻辑也是放在jsb触发的
我们全局搜glBufferDta
,使用的地方很少,很容易我们就能看到是VertexBuffer
有在使用,其实真正的顶点数据
也正是在这个函数里面绑定的。
c++
// 在MiddlewareMarco.h中定义的有最大容量相关的宏
// index buffer init capacity
#define INIT_INDEX_BUFFER_SIZE 1024000
// max vertex buffer size
#define MAX_VERTEX_BUFFER_SIZE 65535
// fill debug data max capacity
#define MAX_DEBUG_BUFFER_SIZE 409600
// type array pool min size
#define MIN_TYPE_ARRAY_SIZE 1024
bool VertexBuffer::init(DeviceGraphics* device, VertexFormat* format, Usage usage, const void* data, size_t dataByteLength, uint32_t numVertices)
{
_bytes = _format->_bytes * numVertices;// 20*65535=1310700,会预申请一块非常大的内存
// 我们看到了申请创建buffer的逻辑
glGenBuffers(1, &_glID);
}
void VertexBuffer::update(uint32_t offset, const void* data, size_t dataByteLength)
{
// 给vbo绑定数据,在init中申请了buffer
glBufferData(GL_ARRAY_BUFFER, _bytes, (const GLvoid*)data, glUsage);
// 这样绑定后,只需要持有这个buffer的handler,就能操作对应的内存数据
// 这个data没有缓存,因为他是来自meshBuffer里面,所以排查方向转到了MeshBuffer
}
- MeshBufer.h
c++
IOBuffer _vb;
void MeshBuffer::uploadVB()
{
auto length = _vb.length();
if (length == 0) return;
auto glVB = _glVBArr[_bufferPos];
// 数据来自_vb,buffer的数据类型是uint8_t,可能是要和js的ArrayBuffer同步
glVB->update(0, _vb.getBuffer(), length);
}
- IOBuffer.h
C++
class IOBuffer
{
public:
uint8_t* _buffer = nullptr;
IOBuffer (std::size_t defaultSize)
{
_bufferSize = defaultSize;
_buffer = new uint8_t[_bufferSize]; // 在构造函数中有赋值
}
inline uint8_t* getBuffer () const
{
return _buffer; // 绑定数据的真正源头
}
}
确定顶点数据的存放位置
一路反推glBufferData
的data参数,发现就是来自上边的_buffer
,再去看_buffer
的赋值地方,就发现
顶点数据的来源如下:
c++
auto mgr = middleware::MiddlewareManager::getInstance();
middleware::MeshBuffer* mb = mgr->getMeshBuffer(VF_XYUVC);
auto glvb = mb->getGLVB();
auto vb = mb->getVB().getBuffer();
auto ib = mb->getIB().getBuffer();
再看ccVertexAttribPointer确定顶点数据布局
- a_position 8字节 x,y, offset为0
- a_uv0 8字节,uv, offset为8
- a_color 4字节,rgba, offset为16
所以数据布局格式为:
a_position | a_uv0 | a_color |
---|---|---|
8 | 8 | 4 |
一组数据一共20个字节
再次观察提交的顶点数量,发现内存顶点数据一个细节
正常的时候一直是6个,也就是2个三角形,发现换肤后,顶点变成了12个,
在上一步我们知道了顶点数据的内存地址,我观察了下该内存地址的数据
发现多了4个顶点,虽然我不知道这组数据是否和这个bug有关系,因为处理换肤顶点我有这样的逻辑:
c++
memcpy(worldVerts, triangles.verts, triangles.vertCount * sizeof(middleware::V2F_T2F_C4B));
感觉可能是worldVerts
可能发生变化,可能需要从大的bufferdata里面找到对应的数据,然后直接修改。
后续排查了下,发现其实和这个是没有关系的,worldVerts
并不是那个大的renderBuffer,这样copy并不会带来额外的数据错乱,worldVerts
仅仅也是一个中转的数据地址,最终会合并到MeshBuffer
里面,所以问题不在这里。
这里虽然会多copy一些数据过来,但是用不用还是受indices的控制。
为什么会多提交一个Sprite
问题还是出在提交的顶点数量,为什么多了?
顶点的数量来自,这个是和InputAssembler有关联的
c++
uint32_t InputAssembler::getPrimitiveCount() const
{
if (-1 != _count)
return _count;
if (_indexBuffer)
return _indexBuffer->getCount();
assert(_vertexBuffer);
return _vertexBuffer->getCount();
}
后来发现是inputAssembler的indices的长度发生了异常
c++
void CCArmatureDisplay::dbRender()
{
_curISegLen = 0;// 这个数值发生了变化
traverseArmature(_armature);
if (_preISegWritePos != -1)
{
// 这里面会更新inputAssembler的indices长度
_assembler->updateIARange(_materialLen - 1, _preISegWritePos, _curISegLen);
}
}
而在traverseArmature
里面,的确是有处理_curISegLen
c++
void CCArmatureDisplay::traverseArmature(Armature* armature, float parentOpacity)
{
for (std::size_t i = 0, len = slots.size(); i < len; i++)
{
texture = slot->getTexture();// 问题就出在我hack了getTexture的实现
if (!texture) continue;// 如果没有纹理就直接返回了,而我会一直返回我hack的纹理,导致indices被累加了
// Record this turn index segmentation count,it will store in material buffer in the end.
_curISegLen += triangles.indexCount;
}
}
知道了原因,修复方法也变的很简单了,是否有纹理,应该以DragonBones的数据为基准,当有纹理了,再返回hack的纹理,修复后换肤就正常了
结尾
对Engine又更加熟悉了