What are my options to improve text rendering performance?

In my situation, I need to be able to render a lot of text with varying font sizes and styles.

Here’s some pseudo of how I typically draw a string on screen every frame:


void DrawText(text, position, font)
{
    foreach(char c in text)
    {
        DrawTexture(font.GetTexture(c), position); // Pushes 4 vertices and the glyph texture into a render queue which eventually all gets drawn at the same time
        position += font.spacing; // Move the position of the cursor over to account for spacing and kerning between letters
    }
}

Basically, when drawing a lot of text on screen (big or small, doesn’t matter) it really starts to take a toll on performance. The amount of text I’m drawing doesn’t seem unreasonable either, maybe 1000 characters at the most. Binding the texture for each character doesn’t seem to have that significant of a performance hit. The problem appears to be all of the vertices used (4 per character).

So, my question, is there a way to reduce the amount of vertices used when drawing text? How are many games able to draw so much text on screen with a seemingly negligible performance decrease?

Can you provide more detail about your “DrawTexture” function? The source code if it’s not too much, otherwise some info about how you’re drawing: immediate mode, vertex arrays, display lists, VBOs, etc? If using VBOs can you say how you’re updating them? Can you also state if you’re using a separate texture per-character?

I can quite confidently say that the number of vertices is not a problem here. 1000 characters, or 4000 vertices, is nothing. Quake handled that kind of vertex count with absolute ease in 1996 on an original 3DFX, so it’s really small small stuff and not worth worrying about - your problem isn’t in the vertex count, it’s elsewhere.

Yes, I’m using a separate texture per character.

So, what I’m doing is pushing all of my vertices into a dynamic array (this includes textures, primitive rectangle shapes, text, etc.), then drawing them all at once with 1 vbo and multiple draw calls (to switch textures).

I’ve pasted the relative bits of code here for better formatting: http://snipt.org/Aiijd9#expand

All of my DrawText(), DrawTexture() calls end up in the DrawRectangle() function where the vertices are created and pushed into a dynamic array. Most of this should be straightforward except for the “RenderQueueItem” stuff. A RenderQueueItem provides some state flags that I can use to toggle shader effects such as “UseTextRendering”, “UseTextureRendering”, “UseMonochromeBlending”, etc. The RenderQueueItem also contains a reference to the texture relative to the set of vertices that were just pushed into the “vertices” list.

After calling all of my Draw() calls for textures, text, and primitives I should end up with a big list (dynamic array in C#) of vertices. Now I do the actual drawing and send all of the data to the GPU by calling FlushBuffers(), in there you’ll see where I bind the data and draw all of the vertices.

Hopefully I’ve explained myself well. Do you think this is the wrong approach for, let’s say, a 2D game?

OK, that description sounds reasonably sensible aside from the separate-texture-per-character part; you need to look at using a texture atlas instead to cut down on those texture changes. The texture changes may not seem like much overhead in your benchmarks, but it’s certainly interfering with the driver’s ability to batch-up commands. You can test and confirm this by just not changing texture at all (i.e. draw everything with the same texture) and see the difference it makes.

Going over to the code now, I’d also advise that you add some state filtering to your glScissor calls because right now you’re also doing a full scissor rect change per-character, which is not helping either.

I’m not seeing where you’re clearing your “vertices” list after you’re done flushing; maybe it’s in code you’ve not posted, but maybe you’ve forgotten to do so? If you’re not clearing it, you’re going to be drawing the previous batch of characters as well as the current batch, and the number of vertices you draw will just grow each frame.

Using a fixed buffer size can also help a lot, rather than respecifying the size each time; the driver can detect this usage pattern and adapt accordingly, by just handing you back previously-allocated blocks of memory when they become free for reuse rather than having to reallocate each time. Pick a size that’s a reasonable average (maybe 200 or so? You’d know what’s better for your program than I do), if it fills you flush, clear, and start over again.

Finally, rather than allocating the vertices dynamically, I’d advise creating a fixed size vertices list (make it the same size as your buffer) and copying into that. I’m not familiar with GC behaviour in Java, but I’ve a hunch it’s not being your friend here.

Phew, that’s quite a bit, and none of these in isolation are going to give really bad performance with the kind of vertex count you have, but taken all together they’re a world of hurt.

That’s going to hurt. You should either have one texture in total (which may require the use of an array texture, depending upon the number of fonts and the number of glyphs per font) or at worst one texture per font. All of the characters using a given font should be drawn before changing to the next font, so you should have at most one draw call per font.

Thanks for all the great suggestions here.

I planned on getting texture atlasing working but I haven’t found any good resources to really explain the whole process to me. There’s so many things I wonder about them like how should the atlases be stored once they’re made, how should they even be made, and some other uncertainties that I can’t think of at the moment.

[QUOTE=mhagain;1255562]I’m not seeing where you’re clearing your “vertices” list after you’re done flushing; maybe it’s in code you’ve not posted, but maybe you’ve forgotten to do so? If you’re not clearing it, you’re going to be drawing the previous batch of characters as well as the current batch, and the number of vertices you draw will just grow each frame.

Using a fixed buffer size can also help a lot, rather than respecifying the size each time; the driver can detect this usage pattern and adapt accordingly, by just handing you back previously-allocated blocks of memory when they become free for reuse rather than having to reallocate each time. Pick a size that’s a reasonable average (maybe 200 or so? You’d know what’s better for your program than I do), if it fills you flush, clear, and start over again.

Finally, rather than allocating the vertices dynamically, I’d advise creating a fixed size vertices list (make it the same size as your buffer) and copying into that. I’m not familiar with GC behaviour in Java, but I’ve a hunch it’s not being your friend here.[/QUOTE]

Sorry, the language here is C#. Forgot to mention that earlier. Also I am calling a ClearBuffers() function which is clearing the vertex lists:


        public void ClearBuffers()
        {
            vertices.Clear();
            renderQueue.Clear();
            currentRenderQueueItem = new RenderQueueItem();
            rendererFlags = RenderQueueFlag.None;
        }

The way C# Lists work (dynamic C# array, equivalent to C++'s Vector), or so I’ve read, is when they’re Clear()'d, they just delete all of the data but keep the same amount of space allocated in anticipation that roughly the same amount of space will be used again. I’ll have to test this further and see if it’s actually doing that much reallocation each frame.

Can you elaborate on the fixed buffer size a bit? Or link me to a tutorial that would explain how to effectively use it? Hell, I’m willing to buy and read a book if it’ll effectively explain all of this to me in modern and relevant OpenGL.

That’s going to hurt. You should either have one texture in total (which may require the use of an array texture, depending upon the number of fonts and the number of glyphs per font) or at worst one texture per font. All of the characters using a given font should be drawn before changing to the next font, so you should have at most one draw call per font.

This is the first I’ve heard of an “array texture”.

What would be preferential here, a texture atlas or an array texture?

EDIT: If I comment out the GL.BindTexture() in my FlushBuffers() function, I should see a significant performance increase if all the texture binding was an issue, correct? I’m seeing a negligible performance difference from doing so.

An array texture just lets you have a bigger texture atlas. If the total number of glyphs is large, you might not be able to fit them all into a single 2D texture. An array texture has multiple layers, i.e. packs multiple 2D textures into a single texture. That way, you don’t need to split up the rendering to change textures.

That’s something else I was wondering about with texture atlases: what if I couldn’t fit all my textures onto the max supported texture size of the graphics card. I guess the answer to that is to use these array textures. Would you be able to lead me towards a well done tutorial or book that could teach me how to use array textures? I feel like I could search Google all day and only end up with more questions and frustration because of all the deprecated or wrong info.

A 2D array texture is similar to a 3D texture except that no interpolation, mipmapping or wrapping is performed for the third dimension, and the third texture coordinate (which selects the layer) should be the layer index rather than a value between 0 and 1.

Note that array textures are only available when using shaders; there is no glEnable(GL_TEXTURE_2D_ARRAY) or equivalent.

If you’re limited to the fixed-function pipeline, then array textures aren’t an option. Instead, you would need to organise the text into separate batches for each 2D texture.

[QUOTE=GClements;1255589]A 2D array texture is similar to a 3D texture except that no interpolation, mipmapping or wrapping is performed for the third dimension, and the third texture coordinate (which selects the layer) should be the layer index rather than a value between 0 and 1.

Note that array textures are only available when using shaders; there is no glEnable(GL_TEXTURE_2D_ARRAY) or equivalent.

If you’re limited to the fixed-function pipeline, then array textures aren’t an option. Instead, you would need to organise the text into separate batches for each 2D texture.[/QUOTE]

Thanks this is a pretty understandable explanation. I’m not using the fixed function pipeline so this isn’t a problem for me.