Home » Bitmap fonts and Android optimisation – BOUNCE iT log #3

Bitmap fonts and Android optimisation – BOUNCE iT log #3

Bitmap font atlas

While testing my game on Android, I found out that using TTF fonts was affecting the game’s performance, so I started looking into using a bitmap font instead. Testing on Android has always been part of the development as I have always known that I wanted to release the game there. For a while, I have noticed some performance issues, but I have always attributed that to the fact that I was testing on an emulated device.

However, as I edge closer to completing the development of the game, I finally decided to test on a physical Android device. This test revealed that the issues I have noticed on the emulated device also happen on physical devices. Therefore, I decided to investigate it further.

With some research, I found these 2 comments (1 and 2) mentioning some slow down when using SDL_ttf on Android. I disabled text rendering in the game and that improved the performance significantly.

With text rendering proving to be the main source of the laggy rendering, I had two possible options to improve the performance on Android:

  1. Look into why specifically the use of SDL_ttf seems to cause these issues on Android.
  2. As suggested by the Reddit comment, switch to using a bitmap font.

For me, the second solution was more interesting as it’s not something I have tried before. So I decided to use bitmap fonts when rendering text on Android and continue to use SDL_ttf on PCs.

Creating the bitmap font texture

To keep things simple, I decided to follow 3 rules when creating the texture:

  1. Keep it as one texture that is large enough to be rendered at different resolutions.
  2. Focus on ASCII characters since my game has English text only.
  3. Break the texture into tiles of equal size to make it easy to select the different glyphs.
Screenshot from Inkscape showing the bitmap font texture which includes all the ASCII printable characters broken into different equally-sized tiles.
Bitmap font broken into equally-sized tiles

Implementing the code

The bitmap_font struct is very simple. It includes members for the width and height of the texture, the tile_width and tile_height which are defined by the user loading the texture, and rows and cols which are automatically set based on the texture and tile dimensions.

C
struct bitmap_font {
  AssetID font_atlas;
  u64 width;
  u64 height;
  u64 tile_width;
  u64 tile_height;
  u64 rows;
  u64 cols;
};

BitmapFont *create_bitmap_font(AssetManager *am, const Renderer *renderer,
                               const char *filepath, u64 tile_width,
                               u64 tile_height) {
  // Allocate memory for the font
  BitmapFont *font = (BitmapFont *)malloc(sizeof(BitmapFont));
  if (!font) {
    return NULL;
  }

  // Load the font texture
  font->font_atlas =
      load_asset_to_manager(am, filepath, ASSET_TYPE_IMAGE, renderer);

  if (font->font_atlas == INVALID_ID) {
    destroy_bitmap_font(am, &font);

    return NULL;
  }

  // Get the texture dimensions
  SDL_Texture *texture = get_texture(am, font->font_atlas);

  i32 w;
  i32 h;

  if (SDL_QueryTexture(texture, NULL, NULL, &w, &h) != 0) {
    destroy_bitmap_font(am, &font);

    return NULL;
  }

  font->width = w;
  font->height = h;

  font->tile_width = tile_width;
  font->tile_height = tile_height;

  // Calculate number of rows and columns
  font->rows = font->height / font->tile_height;
  font->cols = font->width / font->tile_width;

  return font;
}

I then implemented a function that gets the correct tile for a character based on its index.

C
RenderRect get_character_sprite(BitmapFont *font, u64 index) {
  u64 char_count = font->cols * font->rows;

  // Ensure index is within the bounds of the texture
  if (index >= char_count) {
    return (RenderRect){0};
  }

  u64 col = index % font->cols;
  u64 row = index / font->cols;

  RenderRect rect = {
      .x = col * font->tile_width,
      .y = row * font->tile_height,
      .width = font->tile_width,
      .height = font->tile_height,
  };

  return rect;
}

Rendering code

The rendering function is not very complicated. It allows the user to pass a start_index which will be used to calculate each character’s index in the texture. This is because, in a character set like ASCII for example, not all characters are printable, so they won’t be included in the texture. In the case of ASCII, printable characters start at index 32. Passing a start_index ensures that the correct glyph can be retrieved from the font texture.

One thing to note about this rendering code is that it only renders text on the same line. This is all I needed to implement for my game.

C
void render_bitmap_font_text(AssetManager *am, Renderer *renderer,
                             BitmapFont *font, u64 font_size, const char *text,
                             u64 start_index, RenderRect dst_rect,
                             SDL_Color text_color) {
  if (!am || !renderer || !font || font_size == 0) {
    return;
  }

  u64 length = strlen(text);

  if (length == 0) {
    return;
  }

  SDL_Texture *texture = get_texture(am, font->font_atlas);

  u64 char_index = 0;

  RenderRect src = {0};

  // Calculate the base position and scale for a rendered glyph
  f64 ratio = (f64)font_size / font->tile_height;
  u64 width = font->tile_width * ratio;
  RenderRect dst = {
      .y = dst_rect.y,
      .width = (f64)width,
      .height = font_size,
  };

  // Render each character
  for (u64 i = 0; i < length; ++i) {
    // Calculate the correct character index
    char_index = (u64)(text[i]) - start_index;

    src = get_character_sprite(font, char_index);
    
    dst.x = i * width + dst_rect.x;

    // Set the text colour
    set_texture_color_modulation(texture, text_color);

    render_texture(renderer, texture, &src, &dst);
  }
}

The rendering function is exposed to the game through the game engine API.

C
void render_text_with_bitmap_font(BitmapFont *font, const char *text,
                                  u64 font_size, u64 start_index,
                                  RenderRect dest, SDL_Color color);

Conditionally enabling the bitmap font rendering

With all of the rendering code implemented, all I needed to do was to conditionally compile it when building the game on Android. This is done simply in the general text rendering function render_ui_text, where I select the appropriate rendering function based on the platform.

C
void render_ui_text(UI &ui, const char *text, f32 width_divisor,
                    f32 height_divisor, SDL_Color color, u64 font_size) {
/*
 * REST OF THE CODE REMOVED FOR BREVITY
 */

#ifdef __ANDROID__
  render_text_with_bitmap_font(ui.bitmap_font, text, font_size,
                               ASCII_PRINTABLE_CHARACTERS_START_INDEX,
                               text_dest, color);
#else
  render_text_with_font_asset(ui.font, text, NULL, text_dest, color);
#endif // __ANDROID__
}

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.