Text Performance, Fitting and Wrapping
Explore advanced techniques for optimizing and controlling text rendering when using SDL_ttf
In this lesson, we'll build on our text rendering capabilities with some new tools:
- The ability to adjust the rasterizing process to optimize performance when appropriate
- Dynamically choosing a font size to fit a target pixel size for our rasterized surface
- Calculating how much text we can fit within a designated space
- Rendering multi-line text areas using word wrapping, and controlling the alignment of the lines
Our main.cpp
is below. Compared to the last lesson, we've added a GetWidth()
method to the Window
class.
Our Text
class is below. Compared to the previous lesson, we've changed the private
section to protected
, as we'll create some derived classes in this section.
We've also added a constructor that initializes the Font
, but not the SDL_Surface
needed for blitting. This constructor will be useful to those derived classes, but we don't want it to be called in any other context. Therefore, we've marked it as protected
.
Text Rendering Performance
Rendering text is surprisingly expensive in terms of performance cost, and is something we should be mindful of when our program is using a large amount of dynamic text. This performance impact comes in two types:
- Rasterization - how expensive it is to generate pixel data from the provided font and content
- Blitting - how expensive it is to blit that pixel data onto other surfaces, such as the window surface to display to users
We're currently using TTF_RenderUTF8_Blended()
. "Blended" rendering maximizes quality and flexibility at the cost of performance:

Faster Rasterization - Solid
Much of the cost of rasterization comes from calculating the smooth edges of our font, sometimes called anti-aliasing. If we don't need anti-aliasing, we can use "solid" rendering functions, such as TTF_RenderUTF8_Solid()
.
It uses the same arguments as a blended function but does not use anti-aliasing. This is faster to render, but leaves the edges of our text looking more jagged:
// Text.h
// ...
class Text {
// ...
protected:
// ...
void CreateSurface(std::string Content) {
SDL_FreeSurface(TextSurface);
TextSurface = TTF_RenderUTF8_Solid(
Font, Content.c_str(), {255, 255, 255}
);
if (!TextSurface) {
std::cout << "Error creating TextSurface: "
<< SDL_GetError();
}
}
// ...
};

Faster Blitting - Shaded
Most of the performance cost of blitting involves working with transparency. The SDL_Surface
generated by functions like TTF_RenderUTF8_Blended()
and TTF_RenderUTF8_Solid()
has a transparent background.
This maximizes flexibility - the text can be blitted onto any surface and will maintain the background color of that destination surface.
However, this comes at a cost. If we don't want to pay for it, we can create our text surface with an opaque background, which makes blitting much faster.
The TTF_RenderUTF8_Shaded()
function anti-aliases our text in the same way as TTF_RenderUTF8_Blended()
, except it rasterizes onto an opaque background. We pass the SDL_Color
we want the background to be as an additional argument:
// Text.h
// ...
class Text {
// ...
protected:
// ...
void CreateSurface(std::string Content) {
SDL_FreeSurface(TextSurface);
TextSurface = TTF_RenderUTF8_Shaded(
Font,
Content.c_str(),
{255, 255, 255}, // Text Color (White)
{0, 0, 90} // Background Color (Blue)
);
if (!TextSurface) {
std::cout << "Error creating TextSurface: "
<< SDL_GetError();
}
}
// ...
};

In this program, our text is being blitted onto a solid color anyway - the gray of our window surface.
In scenarios like this, TTF_RenderUTF8_Shaded()
could ultimately generate the exact same visuals as TTF_RenderUTF8_Blended()
without the performance cost. We'd simply need to pass the correct background color to TTF_RenderUTF8_Shaded()
Faster Rasterization and Blitting - LCD
Finally, we can fully prioritize performance by combining the rough edges of "solid" rendering with the opaque background of "shaded" rendering. SDL_ttf calls this option LCD:
// Text.h
// ...
class Text {
// ...
protected:
// ...
void CreateSurface(std::string Content) {
SDL_FreeSurface(TextSurface);
TextSurface = TTF_RenderUTF8_LCD(
Font,
Content.c_str(),
{255, 255, 255}, // Text Color (White)
{0, 0, 90} // Background Color (Blue)
);
if (!TextSurface) {
std::cout << "Error creating TextSurface: "
<< SDL_GetError();
}
}
// ...
};

Font Caching / Glyph Caching
To ensure our program can generate high-quality text with minimal performance impact, a font cache is often used.
This means any time we rasterize a character (sometimes called a glyph) we store the calculated pixels in memory so we can reuse them later.
Often, these glyphs are packed into a large image file called a texture atlas, which is a single image containing multiple smaller images (in this case, glyphs) arranged efficiently:

Nicolas P. Rougier - CC BY-SA 4.0
These caching systems include additional bookkeeping to keep track of which glyphs we have cached, where they are in our atlas, which font they used, and the font size.
When a request is made to render the same glyph, using the same font, at the same size, we can reuse the data from our texture atlas rather than recalculate it from scratch.
SDL_ttf includes a basic implementation of glyph caching, which is enough for our needs in this course.
There are open-source examples of more powerful implementations for those interested in pursuing the topic further, such as SDL_FontCache and VEFontCache.
Text Scaling
Before we render text, we often need to perform some calculations relating to size. For example, we might want to dynamically set the font size such that our surface has some specific pixel size. Or, we may want to find out how much text we can fit within a specific area.
SDL_ttf has some utilities that can help us with these tasks.
Using TTF_SizeUTF8()
TTF_SizeUTF8()
helps us understand the size of the surface that would be created if we were to rasterize a piece of content in a given font. It accepts 4 parameters - the font, the content, and two int
pointers that will be updated with the width and height respectively.
int Width, Height;
TTF_SizeUTF8(
Font, "Hello World",
&Width, &Height
);
The results from doing this are equivalent to simply rendering the text and then accessing the w
and h
of the generated SDL_Surface
.
However, TTF_SizeUTF8()
can calculate the dimensions that the source would be without needing to perform the rasterization, meaning it is significantly more performant.
#include <iostream>
#include <SDL_ttf.h>
int main(int argc, char** argv) {
TTF_Init();
TTF_Font* Font{
TTF_OpenFont("Roboto-Medium.ttf", 25)};
int Width, Height;
TTF_SizeUTF8(
Font, "Hello World",
&Width, &Height
);
std::cout << "Width: " << Width
<< ", Height: " << Height;
TTF_Quit();
return 0;
};
Width: 128, Height: 30
If we only care about the width or height of the text, we can pass a nullptr
in the other position.
Let's use this to create a new ScaledText
class that dynamically sets the font size to make our rendered text hit a target width.
We'll arbitrarily initialize our font with a size of 24, and then calculate how wide the surface would be if we rendered our content at that size.
We then determine the ratio between the calculated width and target width, and adjust our font size accordingly before creating the surface for real:
// ScaledText.h
#pragma once
#include <SDL.h>
#include <SDL_ttf.h>
#include "Text.h"
class ScaledText : public Text {
public:
ScaledText(
std::string Content, int TargetWidth)
: Text{BaseFontSize} {
int Width;
TTF_SizeUTF8(
Font, Content.c_str(),
&Width, nullptr
);
float Ratio{static_cast<float>(TargetWidth) / Width};
SetFontSize(BaseFontSize * Ratio);
CreateSurface(Content);
}
private:
static constexpr int BaseFontSize{24};
};
Over in main()
, let's update TextExample
to use this type, and pass the window width as our target:
// main.cpp
// ...
#include "ScaledText.h"
// ...
int main(int argc, char** argv) {
// ...
ScaledText TextExample{
"Hello World",
GameWindow.GetWidth()
};
// ...
}

Using TTF_MeasureUTF8()
Another requirement we're likely to have is to determine how much text can fit within a desired width. The TTF_MeasureUTF8()
can help us determine how many characters can fit within a space. It accepts 5 arguments:
- The
TTF_Font
pointer - The text we want to render
- The maximum width we have available
- A pointer to an
int
, which will be updated with the pixel width of the characters from our string that can fit within that space (the extent) - A pointer to an
int
, which will be updated with the quantity of characters from our string that can fit within our space (the count)
#include <SDL_ttf.h>
#include <iostream>
int main(int argc, char** argv) {
TTF_Init();
TTF_Font* Font{
TTF_OpenFont("Roboto-Medium.ttf", 25)};
int Extent, Count;
TTF_MeasureUTF8(
Font,
"The quick brown fox jumps over the lazy dog",
300, &Extent, &Count);
std::cout << "Extent: " << Extent <<
", Count: " << Count;
TTF_Quit();
return 0;
};
Extent: 288, Count: 24
We can pass a nullptr
to either the extent or the count if we don't care about that result.
In the following example, we create a class that uses this feature. If the entire string won't fit within a space, it will remove characters from the end of the string until it does fit. We also add an ellipsis (...) to indicate it has been truncated.
If TTF_MeasureUTF8()
reports that the number of characters that can fit within the space is less than the size of our Content
string, we use the std::string
's resize()
method to remove the excess characters.
We remove 3
additional characters to make room for the ...
which we add using the std::string
's append()
method:
// TruncatedText.h
#pragma once
#include <SDL.h>
#include <SDL_ttf.h>
#include "Text.h"
class TruncatedText : public Text {
public:
TruncatedText(
std::string Content,
int FontSize,
int MaxWidth
) : Text{FontSize} {
int MaxCharacters;
TTF_MeasureUTF8(
Font, Content.c_str(),
MaxWidth, nullptr, &MaxCharacters
);
if (MaxCharacters < Content.size()) {
Content.resize(MaxCharacters - 3);
Content.append("...");
}
CreateSurface(Content);
}
};
Let's update TextExample
in main()
to use this new type:
// main.cpp
// ...
#include "TruncatedText.h"
// ...
int main(int argc, char** argv) {
// ...
TruncatedText TextExample{
"The quick brown fox jumps over the lazy dog",
36,
GameWindow.GetWidth()
};
// ...
}

Multi-Line Text (Wrapping)
Often, we'll want to create larger blocks of text that extend across multiple lines. If the text exceeds a specific width in pixels, we want to move the rest of the text onto a new line, without splitting a word across multiple lines. This is typically called word wrapping.
All the font rendering functions have variations that accept an additional argument as a max width and will output a multiple-line surface if our text exceeds that length.
These functions have a _Wrapped
suffix. For example, the wrapped version of TTF_RenderUTF8_Blended()
is TTF_RenderUTF8_Blended_Wrapped()
.
Let's create a WrappedText
class that uses it:
// WrappedText.h
#pragma once
#include <SDL.h>
#include <SDL_ttf.h>
#include "Text.h"
class WrappedText : public Text {
public:
WrappedText(std::string Content, int FontSize, int MaxWidth)
: Text{FontSize}, MaxWidth{MaxWidth} {
CreateSurface(Content);
}
private:
void CreateSurface(std::string Content) {
SDL_FreeSurface(TextSurface);
TextSurface = TTF_RenderUTF8_Blended_Wrapped(
Font, Content.c_str(), {255, 255, 255}, MaxWidth
);
if (!TextSurface) {
std::cout << "Error creating TextSurface: " << SDL_GetError();
}
}
int MaxWidth;
};
Over in main()
, let's update our TextExample
to use this WrappedText
type:
// main.cpp
// ...
#include "WrappedText.h"
// ...
int main(int argc, char** argv) {
// ...
WrappedText TextExample{
"The quick brown fox jumps over the lazy dog",
36,
GameWindow.GetWidth()
};
// ...
}

Text Alignment
When rendering wrapped text, we can control its alignment. To do this, we pass the TTF_Font
pointer and the alignment we want to use to TTF_SetFontWrappedAlign()
.
The alignment is a simple integer, for which SDL_ttf provides named variables:
TTF_WRAPPED_ALIGN_LEFT
(default)TTF_WRAPPED_ALIGN_CENTER
TTF_WRAPPED_ALIGN_RIGHT
Let's add an alignment option as a constructor argument to our WrappedText
, and pass it to TTF_SetFontWrappedAlign()
before creating the surface:
// WrappedText.h
#pragma once
#include <SDL.h>
#include <SDL_ttf.h>
#include "Text.h"
class WrappedText : public Text {
public:
WrappedText(
std::string Content,
int FontSize, int MaxWidth,
int Alignment = TTF_WRAPPED_ALIGN_CENTER
) : Text{FontSize}, MaxWidth{MaxWidth} {
TTF_SetFontWrappedAlign(Font, Alignment);
CreateSurface(Content);
}
// ...
};

Summary
In this lesson, we explored advanced text rendering techniques using SDL and SDL_ttf. We covered performance optimization methods, including solid, shaded, and LCD rendering.
We also learned how to dynamically scale text, truncate it to fit within specified dimensions, and implement word wrapping with alignment options.
Engine Overview
An introduction to the generic engine classes we'll use to create the game