cocos-engine
cocos-engine copied to clipboard
2D渲染时, 某些情况下会莫名其妙的断批
Cocos Creator version
3.8.3
System information
all
Issue description
现象: 两个本来是可以合批, 在一个dc里被绘制的sprite节点 莫名其妙的断批了.
首先 我在2D渲染DC优化方面有一定经验, 所以可以排除低级错误 (比如 不能合批的 被我误以为可以合批).
查看了一下dc信息, 发现断批时会出现下图中的情况
这两个dc指令的参数一模一样, 且都使用的同一个 texture, 但是中间插入了一次 bindVertexArrayOES , 导致断批 (或者说是因为断批 才多执行了一次 bindVertexArrayOES , 具体cocos 3内部如何处理的我没太明白)
Relevant error log output
No response
Steps to reproduce
如上
Minimal reproduction project
No response
合批有顶点数量限制吧 https://docs.cocos.com/creator/3.8/manual/zh/ui-system/components/engine/ui-batch.html#meshbuffer-%E5%90%88%E6%89%B9%E8%AF%B4%E6%98%8E
合批有顶点数量限制吧 https://docs.cocos.com/creator/3.8/manual/zh/ui-system/components/engine/ui-batch.html#meshbuffer-%E5%90%88%E6%89%B9%E8%AF%B4%E6%98%8E
我查了下 还真不是这个原因.
图中 橙色 和 蓝色 两个区域的 drawElement 就是理论上可以合批的.
我查看了两次drawElement 的参数. 除了顶点数据外, 唯一的差异就是 第一个 drawElement 的 BlendState里多了一个 validCommandIds 参数, 不知道这个是什么原因引起的.
另外有一个 uniforms 参数 的 Location 不一样. 但是值一样 (都是 value 9), 所用的贴图也是同一张.
name: cc_spriteTexture
size: 1
type: SAMPLER_2D
location: WebGLUniformLocation - ID: 130
value: 9
除此之外, 其他参数都一致.
可能和 MeshBuffer 的機制有關, 2D 渲染對象會優先使用同一個 MeshBuffer 的空間 但在釋放 2D 渲染對象後, 會對 MeshBuffer 造成一定的碎片空間 當下一個 2D 渲染對象在 MeshBuffer 找不到足夠的空間使用時, 系統會再建立另一個 MeshBuffer 導致合批被斷開
這邊的管理比較複雜, 可能需要官方想辦法處理 例如發現空間不足時, 先將當前 MeshBuffer 進行整理以騰出更大的空間 若還是不足才建立新的 MeshBuffer
@minggo 这个问题你们官方有复现吗? 有解决方案或者优化建议吗?
我最近也遇到這個問題, 研究了下引擎的寫法 導致 Batch 被打斷的主要原因是 UIRenderer.renderData 的 chunk 被分配到不同的 buffer 上 會發生這現象的原因有很多種, 我的情況是因為 2D 物件經常被隨機生成和複用 當 2D 物件被渲染時會申請一塊 chunk, 由於是在遊戲過程中生成的, 有可能當前 buffer 不足而使用新的 buffer 這樣就會導致連續的 2D 可合批的物件, 因 chunk 被分配到不同的 buffer 上 Batch 被打斷了
此外複用時, 放入節點池 renderData 的 chunk 並不會被釋放 (生命週期和渲染組件綁定) 當從節點池拿出 2D 物件時, 無法確保這些 2D 物件的 chunk 都在同個 buffer 上, draw calls 也會變高
目前我的解法是:
- 稍微加大 buffer 大小
- 在 2D 物件從節點池拿出時, 強制重新申請一塊 chunk 並複製舊內容到新 chunk 中 這樣有較大的機會會分配在同一個 buffer 上
a. 需要自己寫個組件掛在節點上
b. 重新申請 chunk 並複製舊內容到新 chunk 中的方式
目前實測起來, draw calls 有不錯的下降程度 但目前 renderData 被標記為內部使用, 未來有可能被棄用, 風險再自行評估 @finscn
@yoki0805 万分感谢. 希望引擎组能看到.
这个问题我们已经注意到了,后续会优化。
有没有复现demo
給予 2D 物件隨機的存在時長, 用於模擬遊玩過程的生成與刪減 在大量物件的情況下, 複用時從物件池取出會無法保證這些物件的頂點資料都在相同 Buffer 中 Demo: batch-break.zip
@GengineJS
給予 2D 物件隨機的存在時長, 用於模擬遊玩過程的生成與刪減 在大量物件的情況下, 複用時從物件池取出會無法保證這些物件的頂點資料都在相同 Buffer 中 Demo: batch-break.zip
@GengineJS
好的
这个问题我们看了,因为我们的ui合批方案采用的是网格合批,顶点数组的默认长度是4096,当使用相同资源的ui节点超出这个范围就会新建chunk,这个时候就会新增drawcall,可以适当增加该范围,但是并不一定越大越好。demo里会突然暴增是因为使用的是NodePool,NodePool是会回收使用,而不会销毁,这会导致MeshBuffer已经分配的数据依旧存在chunk中,系统只能继续新增chunk,应该使用node的destroy方法,每帧才会重新计算chunk。 另外网格合批的方案并不适合频繁增删物体的情况,是否有必要对UI新增instancing支持,我们内部需要详细评估下。如果频繁增删的物体是简单的四边形,我建议用Quad,放在UI中,并对Quad使用gpu instancing,这时候可以绕过UI对GPU instancing的支持
Demo 只是對實際情況做了簡化的模擬 以 '吸血鬼倖存者' 類型的遊戲舉例, 怪物和子彈都是帶有動畫的 2D 渲染物件, 並且會有渲染合批和物件複用的需求, GPU instancing 在處理 2D 動畫的 UV 更新上不是那麼方便
也許我們只需要一個能手動清除 chunk 數據的 API, 在放入節點池前由開發者主動呼叫, 當下次被取出並放入場景時, 再透過引擎自動分配即可?
如果一定得用NodePool,可以通过我截图得方式,在put的时候释放renderdata,这时候系统会自动利用空闲的chunk,在get的时候需要调用下_flushAssembler,以便重算renderdata。
这是这边我运行10分钟后drawcall的情况,还是保持2个drawcall,中间会少量增加,最后会回到2
如果把回收调用put的时长固定为1,会一直保持为2个drawcall,这是因为回收与再利用的频率大体一致
.delay(1).call(() => {
// 放入 Pool 不會釋放 RenderData, 銷毀才釋放 RenderData
// 若改使用 lucy.destroy() 則 DrawCalls 不會暴漲
// 或者在 Lucy 節點上掛載 RenderDataChunkMover 組件也可以達到相同效果
this.lucyPool.put(lucy);
// 回收后的node,需要销毁renderdata,以便系统更好的利用chunk
lucy.getComponent(Sprite).destroyRenderData();
// lucy.destroy();
})
.start();```
嗯, 這樣就已經滿好用的 接著看未來版本如何更好地公開 _flushAssembler 方法給開發者了, 謝謝!
@GengineJS 但是我的情况和 @yoki0805 的略有不同, 我没有用到 对象池. 只是单纯的一堆东西画在屏幕上. 我这种情况无解了吗 ?
@GengineJS 但是我的情况和 @yoki0805 的略有不同, 我没有用到 对象池. 只是单纯的一堆东西画在屏幕上. 我这种情况无解了吗 ?
一堆东西是什么?有多少数量?是只新增不删除么?评论沟通方式成本很高,尽量一次性把问题描述清楚下
@GengineJS 但是我的情况和 @yoki0805 的略有不同, 我没有用到 对象池. 只是单纯的一堆东西画在屏幕上. 我这种情况无解了吗 ?
一堆东西是什么?有多少数量?是只新增不删除么?评论沟通方式成本很高,尽量一次性把问题描述清楚下.
- 一堆2d sprite , 大概 3000多个, 都在同屏里.
- 只新增 不删除, 但是有隐藏 (active = false )
- 用了动态合图 , 所以这些精灵用到的图片 大概4张动态图集.
- 粗略估算了一下, 理想状态 dc 应该在 50以下, 但是因为会出现莫名其妙的断批, 实际在 130以上
@finscn 先用我这个pr试下,https://github.com/cocos/cocos-engine/pull/17900 还是不行的话,得排查下,先确保共用一张动态图集,看drawcall降下来得程度,如果依旧很高,适当扩容下chunk,还是不行,给个demo
?改了吗?3.8.4 问题还存在。怎么关了bug