Mixing sprites and bitmap fonts on a single texture to limit draw calls

Hi all!

I’m trying to use one spritesheet with sprites AND bitmap fonts to limit GL draw calls - the trick is to put the spritesheet filename into the .fnt file definition.

Adding only bitmap labels works as expected (multiple labels are drawn in single call), but after mixing sprites and BM labels alternately, the renderer invokes multiple draw calls - despite getting images from the same texture (as shown in cached texture info).

SpriteFrameCache::getInstance()->addSpriteFramesWithFile("res/spritesheet.plist");
auto offset = SpriteFrameCache::getInstance()->getSpriteFrameByName("font.png")->getRectInPixels().origin;

// GL calls: 20 - could be 1?
for (int i = 0; i < 10; ++i)
{
    auto label = Label::createWithBMFont("res/spritesheet.fnt", "Cocos rules!", TextHAlignment::LEFT, 0, offset);
    label->setPosition(origin + Vec2(i*10, i*20));
    addChild(label);

    auto logo = Sprite::createWithSpriteFrameName("logo.png");
    logo->setPosition(origin + visibleSize/2 + Vec2(i*10, i*20));
    addChild(logo);
}

/* dumps:
 "/cc_2x2_white_image" rc=12 id=2 2 x 2 @ 32 bpp => 0 KB
 "/[...]/Resources/res/spritesheet.png" rc=14 id=1 252 x 452 @ 32 bpp => 444 KB
 TextureCache dumpDebugInfo: 2 textures, for 444 KB (0.43 MB)
*/
log("%s", Director::getInstance()->getTextureCache()->getCachedTextureInfo().c_str());

It used to work as expected in Cocos2d-iphone v3.x, but Cocos2d-x v3.9 somehow doesn’t recognize the same texture being used. Did anybody manage to achieve single draw call with sprites/BM labels from the same atlas?

Any help would be much appreciated :slight_smile:

The working example based on Cocos2d-x v3.9:

Thanks in advance and keep up the great work!

Not sure, but may be different shaders are used for sprites and labels.

Yep, likely due to diff shader for sprite and label.

To get a single draw call you could set the shader for the label to use that of the sprite, or vice versa, or create a custom shader that combines both somehow. I’d advise against this.

Unless I’m mistaken, if depth testing is enabled (default or explicitly) then the material sorter should be able to sort all nodes of one material (texture+shader+other) correctly. If this is the case and 20 GL calls are still being made then I do believe this could be fixed??

However, I believe depth testing is off for non-3D nodes by default? And if this is the case then it’s a function of how the scene graph and layering works with the node hierarchy. When you interleave the labels with the sprites they are rendered in order of when they are added as the engine doesn’t know whether your labels and sprites area all in the same position and thus doesn’t know if they overlap or not so it must render them in the desired order (default is arrival order).

To test this theory you can set the local Z order of all labels to be rendered last after all the sprites. Theoretically this should order the nodes correctly so that the material batching should work to give you 2 GL calls instead of 20.

addChild(label, 100);
addChild(logo);

Alternatives if your hierarchy isn’t flat like the example where you have let’s say each label is a child of each logo.

  • Set the global Z order to inform the renderer what you want for the render order even if the graph order is different.
  • Use OpenGL Z position (setPosition3D( _ , _ , Z), enable depth test, and use alpha discard shader.

Thanks for help!

It turns out that material used is the same (same texture, shader, blend function), but sprites use TrainglesCommand for batch rendering, while BM labels use QuadCommand. Renderer draws batched quads and triangles seperately, hence draw calls add up.

Tough one. Any ideas? :smile:

ideas? well did you try reducing them by using one of the reordering methods? Those should allow batching of all labels and all sprites into only 2 GL calls, assuming I’m correct?

Did you notice if they set different values for the uniforms? Assuming they used Quad instead to reduce data size sent/stored on GPU, maybe a switch for “optimizeWithQuads” or “useTrianglesForBatching” or whatever could be used. Otherwise maybe the perf is similar and it can be just converted over to triangles.

If I find some time this week I’ll look for a test in cpp-test or setup a new project and take a quick look and explore further.

Edit: also shadows and outlines may require diff shader or uniforms. I still believe that only all similar labels (same font definition and shadow/outline/etc) should be batched and all sprites using same sheet should be batched. I’m not sure there’s benefit in trying to get everything to batch into a single GL call/draw.

Yes, I played with Z index and it reduced draw calls to 2. However I want to display each sprite with a label in front of it, and setting global Z order breaks this view hierarchy (I don’t know how to implement the 2nd alternative with setPosition3D).

Labels with shadows are not batched at all. I didn’t notice setting uniforms in draw() method.

I tried to use TrianglesCommand for BM labels, based on code from commits:
https://github.com/cocos2d/cocos2d-x/commit/6d9f850e2c5510565a9ce74fb5921d4cac1bbc94
https://github.com/cocos2d/cocos2d-x/commit/2363622a67a95b16466477094a166ffe9ca90b89

It uses single draw call for sprites interweaved with labels, but there was something wrong with coverting Quads into Triangles and only the first letter of the label was displayed:

void Label::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
  // ...
  auto textureAtlas = _batchNodes.at(0)->getTextureAtlas();
  static unsigned short indices[6] = {0, 1, 2, 3, 2, 1};
  TrianglesCommand::Triangles triangles;
  triangles.indices = indices;
  triangles.verts = (V3F_C4B_T2F*)textureAtlas->getQuads();
  triangles.vertCount = 4;
  triangles.indexCount = 6;
  _trianglesCommand.init(_globalZOrder, textureAtlas->getTexture()->getName(), getGLProgramState(), _blendFunc, triangles, transform, flags);
  renderer->addCommand(&_trianglesCommand);
  // ...
}

Thanks for help!

Some other work that may also help in this area in the future.