2
\$\begingroup\$

I'm working on game which uses LibGDX's Stage2d for GUI.

All textures for UI are packed into one texture atlas.

Fonts (BitmapFont) are generated in runtime by gdx-freetype for different locales and font sizes (at least 6 locales and 4 font sizes for each locale) and then placed into separate texture.

Game UI contains much different elements (strategy game with lots of numbers) - buttons, icons, text labels, which usually alternate between each other and trigger many texture switches when drawn. I wonder if there's any way to reduce the number of texture switches as it sometimes takes up to 5ms per frame for stage.draw().

The only way I see is to draw fonts only when all UI textures are drawn, when it is possible, but it will produce awful code and will harm the hierarchy of elements in stage.

I didn't merge textures (but thought of it), because:

  1. Textures use different filtering (MipMapLinearNearest for UI textures, Linear for fonts) and it will require manual texture filtering switches or mess with stage2d overriden Label class.
  2. UI texture already has a size of 2048x2048px, and I can't add all font size textures to it as it will require higher texture sizes, which are not supported by many old devices.

Is there any good way of solving this issue?

\$\endgroup\$
  • \$\begingroup\$ Can you clarify how many textures you have? I can't tell which textures you're talking about in various parts of the question. \$\endgroup\$ – user1118321 Oct 3 '17 at 5:19
  • \$\begingroup\$ Have you tried attaching a graphics debugger and verifying that the draw calls are indeed happening in the incorrect order? I don't know a whole lot about Stage2d unfortunately, but I would hope a library like this would correctly handle situations like these. Does Stage2d have a concept of render layers that would allow you to encourage the optimal rendering behavior? \$\endgroup\$ – Nick Caplinger Oct 5 '17 at 0:23
  • \$\begingroup\$ @Nick Caplinger i used Android debugger which shows all method calls and their execution time, I'm sure that texture switches are caused by wrong order of labels and other stuff. Stage2d doesn't have render layers. Its actors have method "setZIndex" but it only changes actor's index in parent's children list, which doesn't help with hierarchy. \$\endgroup\$ – therainycat Oct 5 '17 at 8:10
  • \$\begingroup\$ What about using a depth buffer for UI rendering? Have some queue, that you first schedule draws like you're doing now, but the rendering process will be deferred. \$\endgroup\$ – Sopel Oct 7 '17 at 11:46
  • \$\begingroup\$ ad. Have some queue (or multiple of them, one for each type of object), that you first schedule draws like you're doing now (keeping the hierarchy you have), but the rendering process will be deferred and will draw text separately to other ui elements (with order defined by fragment depth of course). \$\endgroup\$ – Sopel Oct 7 '17 at 11:53
2
+50
\$\begingroup\$

First thing is to use distance field font rendering and pack every glyph into a single texture using all 4 channels as individual gray-scale textures. This allows you to use only 1 font size in your atlas.

It looks like this inside the texture when tightly packed:

enter image description here

You can pack a ridiculous amount of characters at very low resolution and they still look great when scaled up. You can see the little distortions around the corners:

enter image description here

The font in the atlas in the above example is only 24 pixels high.

This gives you space for over 20,000 glyphs (or ~10,000 Asian glyphs as they need a bit more definition) in a single 2048x2048x32bpp texture.

This eliminates the need for multiple sizes of the same font in the font atlas texture.


The next step is to assign ALL your UI textures atlases to a single pixel shader that will sample all texture atlases but only effectively use 1 texture / texture plane out of all the textures.

You can use a UV encoding to let the pixel shader know which textures to throw out and which to use: eg: U range [-2..-1), [-1..0), [0..1), and so on..

Make sure you sample all the textures: conditional/dependent texture sampling hurts GPU performance more than sampling excessively (within reason)

// sample all the textures at the same time
vec4 glyph_combined = texture(glyph_texture, uv.xy);
vec4 ui_graphics1 = texture(ui_graphics_texture1, uv.xy);
vec4 ui_graphics2 = texture(ui_graphics_texture2, uv.xy);
vec4 ui_graphics3 = texture(ui_graphics_texture3, uv.xy);

// DecodeDistanceFieldMagic is outside the scope of this question
// look up distance field font rendering linked at the top of the answer
vec4 glyph = DecodeDistanceFieldMagic(glyph_combined); 

// uv.x < 1 selects glyph, uv.x >= 1 selects gfx 1
vec4 glyph_or_gfx1 = mix(glyph, ui_graphics1, step(1.0, uv.x));

// uv.x < 3 selects gfx 2, uv.x >= 3 selects gfx 3
vec4 gfx2_or_gfx3 = mix(ui_graphics2, ui_graphics3, step(3.0, uv.x));

// uv.x < 2 selects first "pair", uv.x >= 2 selects second "pair"
vec4 last_selection = mix(glyph_or_gfx1, gfx2_or_gfx3, step(2.0, uv.x));

output = last_selection * vertex_color;

Make sure to set all your textures to repeat.

With this method each individual texture can still have their own filtering.

If the graphic card supports GL_TEXTURE_2D_ARRAY you can do part of the selection in the vertex shader by passing to the vertex shader which texture in the array to use which will reduce the texture sampling in the pixel shader but all textures in the array must share the same filtering setting.

On current GPUs you can have 16 or 32 textures bound at once (Depending on your definition of current GPU). You still want to minimize this for performance reasons by using atlases.

\$\endgroup\$
  • \$\begingroup\$ Thank you for the answer, it is very helpful! Fortunately, LibGDX has DistanceFieldFont class as example, so I used it but changed few things to get rid of "if" condition in fragment shader and also removed excessive SpriteBatch flush() calls thanks to your example of UV encoding. Distance field font for 6 languages with glyph size of 32px and resulting texture size of 512x256px fit into UI atlas and works good with MipMapLinearNearest filtering, which is used by UI graphics so I did not even have to change it. Thank you once again, I was looking for this tip for a long time. \$\endgroup\$ – therainycat Oct 8 '17 at 0:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.