Text Performance, Fitting and Wrapping

Explore advanced techniques for optimizing and controlling text rendering when using SDL_ttf
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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.

#include <iostream>
#include <SDL.h>
#include <SDL_ttf.h>
#include "Text.h"

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "My Program", SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED, GetWidth(), 300, 0);
  }

  void Render() {
    SDL_FillRect(
      GetSurface(), nullptr, SDL_MapRGB(
        GetSurface()->format, 50, 50, 50
      )
    );
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() {
    return SDL_GetWindowSurface(SDLWindow);
  }

  ~Window() { SDL_DestroyWindow(SDLWindow); }
  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;
  
  int GetWidth() const { return WindowWidth; }

private:
  SDL_Window* SDLWindow;
  int WindowWidth{600};
};

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  TTF_Init();
  SDL_Event Event;
  bool shouldQuit{false};

  Window GameWindow;
  Text TextExample{"Hello World"};

  while (!shouldQuit) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        shouldQuit = true;
      }
    }
    GameWindow.Render();
    TextExample.Render(GameWindow.GetSurface());
    GameWindow.Update();
  }

  TTF_Quit();
  SDL_Quit();
  return 0;
}

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.

#pragma once
#include <SDL.h>
#include <SDL_ttf.h>

class Text {
 public:
  Text(std::string Content, int FontSize = 100)
  : Font{LoadFont(FontSize)} {
    CreateSurface(Content);
  }

  void SetFontSize(int NewSize) {
    TTF_SetFontSize(Font, NewSize);
  }

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, nullptr,
      DestinationSurface, &DestinationRectangle
    );
  }

  ~Text() {
    SDL_FreeSurface(TextSurface);
    if (TTF_WasInit()) {
      TTF_CloseFont(Font);
    }
  }
  Text(const Text&) = delete;
  Text& operator=(const Text&) = delete;

protected:
  Text(int FontSize)
  : Font{LoadFont(FontSize)} {}

  TTF_Font* LoadFont(int FontSize) {
   TTF_Font* LoadedFont{TTF_OpenFont(
     "Roboto-Medium.ttf", FontSize)};
    if (!LoadedFont) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    return LoadedFont;
  }

  void CreateSurface(std::string Content) {
    SDL_FreeSurface(TextSurface);
    TextSurface = TTF_RenderUTF8_Blended(
      Font, Content.c_str(), {255, 255, 255}
    );
    if (!TextSurface) {
      std::cout << "Error creating TextSurface: "
        << SDL_GetError();
    }
  }

  TTF_Font* Font{nullptr};
  SDL_Surface* TextSurface{nullptr};
  SDL_Rect DestinationRectangle{0, 0, 0, 0};
};

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:

Screenshot of the program running

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();
    }
  }
  // ...
};
Screenshot of the program running

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();
    }
  }
  // ...
};
Screenshot of the program running

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();
    }
  }
  // ...
};
Screenshot of the program running

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:

A texture atlas of glyphs

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.

UTF-8 Encoding

Character encoding is the process of representing text as binary data that computers can store and transmit. UTF-8 (Unicode Transformation Format 8-bit) is a widely used encoding standard that can represent virtually all characters in use worldwide.

When you see UTF8 in SDL_ttf function names like TTF_RenderUTF8_Blended(), it indicates that the function expects the input text to be encoded in UTF-8 format.

This is the most common encoding for text in modern systems, making it a safe default choice for handling text in your applications. We cover character encoding in more depth in our advanced course:

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.

TTF_SizeUTF8()

TTF_SizeUTF8() helps us understand how big a surface 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 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()
  };
  // ...
}
Screenshot of the program running

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()
  };
  // ...
}
Screenshot of the program running

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()
  };
  // ...
}
Screenshot of the program running

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);
  }
  // ...
};
Screenshot of the program running

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.

Was this lesson useful?

Next Lesson

Engine Overview

An introduction to the generic engine classes we'll use to create the game
Abstract art representing computer programming
New: AI-Powered AssistanceAI Assistance

Questions and HelpNeed Help?

Get instant help using our free AI assistant, powered by state-of-the-art language models.

Ryan McCombe
Ryan McCombe
Updated
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Rendering Images and Text
  • 44.GPUs and Rasterization
  • 45.SDL Renderers
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 46 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Engine Overview

An introduction to the generic engine classes we'll use to create the game
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved