~cdervis/BMFGen

48c05815b9cd874fa31b2f138a0bb47c7e5f9c33 — Cem Dervis 2 months ago 6f384ad
Add SDL demo and C++ API header
A demo/.clang-format => demo/.clang-format +32 -0
@@ 0,0 1,32 @@
---
AccessModifierOffset: -2
AlignConsecutiveDeclarations: None
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: None
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: None
ColumnLimit: 120
ConstructorInitializerAllOnOneLineOrOnePerLine: false
BreakConstructorInitializersBeforeComma: true
Cpp11BracedListStyle: true
Language: Cpp
MaxEmptyLinesToKeep: 2
PointerAlignment: Left
SortIncludes: CaseInsensitive
SortUsingDeclarations: true
SpaceBeforeParens: ControlStatements
Standard: Latest
TabWidth: 2
IndentWidth: 2
UseTab: Never
BreakBeforeBraces: Custom
AlwaysBreakTemplateDeclarations: Yes
BraceWrapping: {
  BeforeElse: true,
  BeforeCatch: true,
}

...

A demo/CMakeLists.txt => demo/CMakeLists.txt +21 -0
@@ 0,0 1,21 @@
cmake_minimum_required(VERSION 3.10)

find_package(SDL REQUIRED)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)

project(libbmfgen)
add_library(libbmfgen INTERFACE)

add_executable(bmfgen-example-sdl main.cpp)

target_link_libraries(bmfgen-example-sdl PRIVATE
  libbmfgen
  SDL::SDL
)

target_include_directories(bmfgen-example-sdl PRIVATE
  "${SDL_DIR}/include"
  "-DFONTS_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/fonts/\""
)
\ No newline at end of file

A demo/bmfgen.hpp => demo/bmfgen.hpp +758 -0
@@ 0,0 1,758 @@
/**
 *
 * BMFGen C++ library for loading and displaying BMFGen-generated fonts.
 * It is designed as framework-agnostic and therefore does not depend
 * on any third-party libraries or frameworks other than the C++ standard library.
 *
 * It includes simple extensions for well-known frameworks such as SDL,
 * but is able to be extended easily for your own use cases.
 *
 * At the moment, text layouting is rudimentary.
 * It does not perform any text shaping.
 * You would have to use this library together with a text shaping library
 * such as Harfbuzz to accomplish that.
 * However, for most non-complex text rendering tasks, it should do well enough.
 *
 * IMPORTANT:
 * This library is header-only and consists of only this file.
 * You may include it from multiple translation units, but at most one of them should
 * #define BMFGEN_IMPLEMENTATION prior to including this header, so that all method
 * implementations are generated.
 *
 * EXTENSIONS:
 * Currently, the following extensions are implemented:
 * - SDL: Enable this by #defining BMFGEN_ENABLE_SDL prior to including this header.
 *
 * Version: 1.0.0
 *
 * Copyright (C) Cemalettin Dervis 2022-2023
 * License: MIT
 * https://bmfgen.com
 *
 */

#pragma once

#include <algorithm>
#include <string>
#include <vector>

#if __cplusplus >= 201703L
#include <string_view>
#endif

#if __cplusplus >= 202002L
#include <span>
#endif


// ----------------------------------------
// Core API
// ----------------------------------------

namespace bmfgen {
/**
 * Represents the scaling factor of a font.
 * The scaling factor is directly related to the font's variations.
 * BMFGen can generate a font together with variations of it, in a single file.
 * This is useful for multiple targeted display resolutions.
 * A typical workflow would be to design a font for a 1080p resolution, but include
 * a variation with a 2x scaling factor, for when the font should be rendered on 4K displays.
 * The font would contain all glyphs for both sizes (1x and 2x scaling), and using
 * this enum, a switch at run-time is possible.
 */
enum class ScaleFactor {
  One = 0,
  OneAndAHalf = 1,
  Half = 2,
  Two = 3,
};

/**
 * Represents a unique glyph in the font.
 */
struct Glyph {
  uint32_t character;
  uint32_t pageIndex;
  int32_t x;
  int32_t y;
  int32_t width;
  int32_t height;
  int32_t leftBearing;
  int32_t rightBearing;
  int32_t horizontalAdvance;
};

#if __cplusplus >= 201703L
using StringView = std::string_view;
#else
using StringView = const std::string&;
#endif

/**
 * Represents a font that was generated by BMFGen.
 * Contains information about the glyphs, pages and variations.
 * A font is self-contained, meaning that it includes all necessary resources
 * such as page images.
 */
class Font {
public:
  /**
   * Represents a page in the font, including its data.
   */
  struct Page {
    uint32_t width{};                // The width of the page, in pixels.
    uint32_t height{};               // The height of the page, in pixels.
    std::unique_ptr<uint8_t[]> data; // The pixel data of the page.
  };

  /**
   * Represents a variation of the font.
   * A variation contains all of its glyphs and metrics that are necessary for laying out text.
   */
  struct Variation {
    /**
     * Looks up a glyph by its character.
     * @param character The character to search for.
     * @return A pointer to the glyph if found; nullptr otherwise.
     */
    const Glyph* findGlyph(uint32_t character) const;

    int32_t ascent{};
    int32_t descent{};
    int32_t lineGap{};
    int32_t lineHeight{};
    int32_t pixelSize{};
    ScaleFactor scaleFactor{};
    int32_t underlinePos{};
    std::vector<Glyph> glyphs;
  };

  /**
   * Loads a font from a file on the disk.
   * @param filename The full path to the file.
   */
  explicit Font(StringView filename);

#if __cplusplus >= 202002L
  /**
   * Loads a font from memory.
   * @param data The span that represents the font's data.
   */
  explicit Font(std::span<const uint8_t> data);
#endif

  /**
   * Loads a font from a pointer and a size.
   * @param data The pointer that points to the font's data.
   * @param dataSize The size of the font's data.
   */
  Font(const void* data, size_t dataSize);

  Font(const Font&) = default;

  Font& operator=(const Font&) = default;

  Font(Font&&) noexcept = default;

  Font& operator=(Font&&) noexcept = default;

  ~Font() noexcept = default;

  /**
   * Checks whether the font contains a specific character.
   * @param character The character to look up.
   * @return True, if the font contains the character; false otherwise.
   */
  bool hasCharacter(uint32_t character) const;

  const std::vector<uint32_t>& characters() const;

  uint32_t baseSize() const;

  StringView name() const;

  const std::vector<Page>& pages() const;

  const std::vector<Variation>& variations() const;

  /**
   * Looks up a variation by its scaling factor.
   * @param scaleFactor The scaling factor to use.
   * @return A pointer to the font variation if found; nullptr otherwise.
   */
  const Variation* findVariation(ScaleFactor scaleFactor) const;

  /**
   * Gets a value indicating whether this font is valid and can therefore be used for rendering.
   * @return True, if the font is valid; false otherwise.
   */
  explicit operator bool() const;

  /**
   * Gets a unique key (hash code) for this font, so that it can be used in font caches.
   * Does not guarantee that it cannot collide with other fonts.
   * @return The hash code of this font.
   */
  uint64_t cacheKey() const;

private:
  void loadFromMemory(const void* data, size_t dataSize);

  bool m_isValid;
  uint32_t m_baseSize;
  std::string m_name;
  std::vector<uint32_t> m_characters;
  std::vector<Page> m_pages;
  std::vector<Variation> m_variations;
  uint64_t m_cacheKey;
};
} // namespace bmfgen


// ----------------------------------
// Extension API for SDL
// ----------------------------------

#ifdef BMFGEN_ENABLE_SDL

#include <SDL.h>
#include <unordered_map>

namespace bmfgen {
class RendererSDL {
public:
  RendererSDL();

  explicit RendererSDL(SDL_Renderer* sdlRenderer);

  RendererSDL(const RendererSDL&) = delete;

  void operator=(const RendererSDL&) = delete;

  RendererSDL(RendererSDL&&) noexcept = default;

  RendererSDL& operator=(RendererSDL&&) noexcept = default;

  void drawText(const Font& font, StringView text, double x, double y,
                bmfgen::ScaleFactor scaleFactor = bmfgen::ScaleFactor::One);

  void uncacheFont(const Font& font);

private:
  class CachedFont {
  public:
    CachedFont(SDL_Renderer* sdlRenderer, const Font& font, bool& success);

    CachedFont(const CachedFont&) = delete;

    void operator=(const CachedFont&) = delete;

    CachedFont(CachedFont&&) noexcept = default;

    CachedFont& operator=(CachedFont&&) noexcept = default;

    ~CachedFont() noexcept;

    std::vector<SDL_Texture*> pageTextures;
  };

  const CachedFont* lookupCachedFont(const Font& font);

  struct GlyphToDraw {
    SDL_Vertex vertices[4];
    uint32_t pageIndex;
  };

  SDL_Renderer* m_sdlRenderer;
  std::vector<GlyphToDraw> m_glyphsToDraw;
  std::vector<SDL_Vertex> m_vertices;
  std::vector<int> m_indices;
  std::unordered_map<uint64_t, CachedFont> m_cachedFonts;
};
} // namespace bmfgen

#endif // BMFGEN_ENABLE_SDL


// ----------------------------------------
// Core API Implementation
// ----------------------------------------
#ifdef BMFGEN_IMPLEMENTATION

#include <cassert>
#include <fstream>

namespace bmfgen {
namespace details {
class BinaryReader {
public:
  BinaryReader(const void* data, size_t dataSize)
      : m_data(static_cast<const uint8_t*>(data))
      , m_position(0)
      , m_dataSize(dataSize) {
    std::ignore = m_dataSize;
  }

  int32_t readInt32() {
    return this->readValue<int32_t>();
  }

  uint32_t readUInt32() {
    return this->readValue<uint32_t>();
  }

  uint64_t readUInt64() {
    return this->readValue<uint64_t>();
  }

  std::string readString() {
    const size_t length = size_t(this->readUInt32());
    std::string str;
    str.resize(length);
    this->read(&str[0], length);
    return str;
  }

  void read(void* dst, size_t size) {
    assert(m_position + size <= m_dataSize);
    std::memcpy(dst, this->currentData(), size);
    m_position += size;
  }

private:
  const uint8_t* currentData() const {
    return m_data + m_position;
  }

  template <typename T>
  T readValue() {
    assert(m_position + sizeof(T) <= m_dataSize);
    const T value = *reinterpret_cast<const T*>(this->currentData());
    m_position += sizeof(T);
    return value;
  }

  const uint8_t* m_data;
  size_t m_position;
  size_t m_dataSize;
};
} // namespace details

Font::Font(StringView filename) {
  std::ifstream ifs{filename, std::ios::binary | std::ios::ate};
  if (!ifs) {
    return;
  }

  const size_t dataSize = size_t(ifs.tellg());
  ifs.seekg(0, std::ios::beg);

  auto data = std::make_unique<uint8_t[]>(dataSize);
  ifs.read(reinterpret_cast<char*>(data.get()), std::streamsize(dataSize));

  this->loadFromMemory(data.get(), dataSize);
}

#if __cplusplus >= 202002L
Font::Font(std::span<const uint8_t> data) {
  this->loadFromMemory(data.data(), data.size());
}
#endif

Font::Font(const void* data, size_t dataSize)
    : m_isValid(false)
    , m_baseSize(0)
    , m_cacheKey(0) {
  this->loadFromMemory(data, dataSize);
}

const Font::Variation* Font::findVariation(ScaleFactor scaleFactor) const {
  const auto it = std::find_if(m_variations.cbegin(), m_variations.cend(), [scaleFactor](const Variation& variation) {
    return variation.scaleFactor == scaleFactor;
  });

  return it != m_variations.cend() ? &(*it) : nullptr;
}

bool Font::hasCharacter(uint32_t character) const {
  const auto it = std::lower_bound(m_characters.cbegin(), m_characters.cend(), character);

  return it != m_characters.cend() && *it == character;
}

const std::vector<uint32_t>& Font::characters() const {
  return m_characters;
}

uint32_t Font::baseSize() const {
  return m_baseSize;
}

StringView Font::name() const {
  return m_name;
}

const std::vector<Font::Page>& Font::pages() const {
  return m_pages;
}

const std::vector<Font::Variation>& Font::variations() const {
  return m_variations;
}

Font::operator bool() const {
  return m_isValid;
}

uint64_t Font::cacheKey() const {
  return m_cacheKey;
}

void Font::loadFromMemory(const void* data, size_t dataSize) {
  details::BinaryReader reader{data, dataSize};

  // Read version
  {
    const auto major = reader.readInt32();
    const auto minor = reader.readInt32();
    const auto revision = reader.readInt32();

    // Unused at the moment.
    std::ignore = major;
    std::ignore = minor;
    std::ignore = revision;
  }

  m_name = reader.readString();
  m_baseSize = reader.readUInt32();

  // Characters
  {
    const auto characterCount = size_t(reader.readUInt32());
    m_characters.resize(characterCount);
    reader.read(m_characters.data(), characterCount * sizeof(uint32_t));
  }

  // Pages
  {
    const auto pageCount = size_t(reader.readUInt32());
    m_pages.reserve(pageCount);

    for (size_t i = 0; i < pageCount; ++i) {
      Page page{};
      page.width = reader.readUInt32();
      page.height = reader.readUInt32();

      const auto pageDataSize = size_t(reader.readUInt32());
      page.data = std::make_unique<uint8_t[]>(pageDataSize);
      reader.read(page.data.get(), pageDataSize);

      m_pages.push_back(std::move(page));
    }
  }

  // Variations
  {
    const auto variationCount = size_t(reader.readUInt32());

    for (size_t v = 0; v < variationCount; ++v) {
      Variation variation{};
      variation.scaleFactor = ScaleFactor(reader.readInt32());
      variation.pixelSize = reader.readInt32();
      variation.lineHeight = reader.readInt32();
      variation.ascent = reader.readInt32();
      variation.descent = reader.readInt32();
      variation.lineGap = reader.readInt32();
      variation.underlinePos = reader.readInt32();

      const auto glyphCount = size_t(reader.readUInt32());

      variation.glyphs.reserve(glyphCount);

      for (size_t g = 0; g < glyphCount; ++g) {
        Glyph glyph{};
        glyph.character = reader.readUInt32();
        glyph.x = reader.readInt32();
        glyph.y = reader.readInt32();
        glyph.width = reader.readInt32();
        glyph.height = reader.readInt32();
        glyph.pageIndex = reader.readUInt32();
        glyph.horizontalAdvance = reader.readInt32();
        glyph.leftBearing = reader.readInt32();
        glyph.rightBearing = reader.readInt32();

        variation.glyphs.push_back(glyph);
      }

      m_variations.push_back(std::move(variation));
    }
  }

  m_cacheKey = reader.readUInt64();

  m_isValid = true;
}

const Glyph* Font::Variation::findGlyph(uint32_t character) const {
  // Glyphs are sorted by character, so use a binary search.
  const auto it =
      std::lower_bound(this->glyphs.cbegin(), this->glyphs.cend(), character, [](const Glyph& lhs, uint32_t rhs) {
        return lhs.character < rhs;
      });

  if (it != this->glyphs.cend() && it->character == character) {
    return &(*it);
  }
  else {
    return nullptr;
  }
}

template <typename Action>
static inline void forEachGlyph(const Font& font, StringView text, ScaleFactor scaleFactor, const Action& action) {
  if (text.empty()) {
    return;
  }

  const auto variation = font.findVariation(scaleFactor);

  double x = 0.0;
  double y = 0.0;

  const auto* spaceGlyph = variation->findGlyph(32U);

  for (const char ch : text) {
    switch (ch) {
    case ' ': {
      if (spaceGlyph != nullptr) {
        x += double(spaceGlyph->horizontalAdvance);
      }
      break;
    }
    case '\r': {
      break;
    }
    case '\n': {
      x = 0.0;
      y += double(variation->lineHeight);
      break;
    }
    default: {
      const Glyph* glyph = variation->findGlyph(ch);

      if (glyph == nullptr) {
        glyph = spaceGlyph;
      }

      if (glyph != nullptr) {
        action(x, y, *glyph);
        x += double(glyph->horizontalAdvance);
      }

      break;
    }
    }
  }
}

static inline std::pair<double, double> measureText(const Font& font, StringView text,
                                                    ScaleFactor scaleFactor = ScaleFactor::One) {
  double width = 0;
  double height = 0;

  bmfgen::forEachGlyph(font, text, scaleFactor, [&width, &height](double x, double y, const Glyph& glyph) {
    width = std::max(width, x + double(glyph.width));
    height = std::max(height, y + double(glyph.height));
  });

  return {width, height};
}
} // namespace bmfgen


// ----------------------------------------
// Extension API for SDL Implementation
// ----------------------------------------

#ifdef BMFGEN_ENABLE_SDL

namespace bmfgen {
RendererSDL::RendererSDL()
    : m_sdlRenderer(nullptr) {
}


RendererSDL::RendererSDL(SDL_Renderer* sdlRenderer)
    : m_sdlRenderer(sdlRenderer) {
}


void RendererSDL::drawText(const Font& font, StringView text, double x, double y, bmfgen::ScaleFactor scaleFactor) {
  const CachedFont* cachedFont = this->lookupCachedFont(font);
  if (cachedFont == nullptr) {
    // Failed to cache the font (probably failed to create GPU-based resources).
    // There's nothing we can do to recover, so don't draw the text at all.
    return;
  }

  m_glyphsToDraw.clear();
  m_glyphsToDraw.reserve(text.length());

  bmfgen::forEachGlyph(font, text, scaleFactor, [this, &font, x, y](double gx, double gy, const Glyph& glyph) {
    // Vertices of a glyph:
    // 0 --- 1
    // |  /  |
    // 2 --- 3

    const auto left = float(x + gx);
    const auto top = float(y + gy);
    const auto right = left + float(glyph.width);
    const auto bottom = top + float(glyph.height);

    const auto& page = font.pages()[glyph.pageIndex];
    const auto recWidth = 1.0F / float(page.width);
    const auto recHeight = 1.0F / float(page.height);

    const auto uvLeft = float(glyph.x) * recWidth;
    const auto uvTop = float(glyph.y) * recHeight;
    const auto uvRight = float(glyph.x + glyph.width) * recWidth;
    const auto uvBottom = float(glyph.y + glyph.height) * recHeight;

    const SDL_Color color{255, 255, 255, 255};

    GlyphToDraw glyphToDraw{};
    glyphToDraw.vertices[0] = SDL_Vertex{{left, top}, color, {uvLeft, uvTop}};
    glyphToDraw.vertices[1] = SDL_Vertex{{right, top}, color, {uvRight, uvTop}};
    glyphToDraw.vertices[2] = SDL_Vertex{{left, bottom}, color, {uvLeft, uvBottom}};
    glyphToDraw.vertices[3] = SDL_Vertex{{right, bottom}, color, {uvRight, uvBottom}};
    glyphToDraw.pageIndex = glyph.pageIndex;

    m_glyphsToDraw.push_back(glyphToDraw);
  });

  if (m_glyphsToDraw.empty()) {
    return;
  }

  // Sort glyphs based on page index so that we have as few texture switches as possible.
  std::sort(m_glyphsToDraw.begin(), m_glyphsToDraw.end(), [](const GlyphToDraw& lhs, const GlyphToDraw& rhs) {
    return lhs.pageIndex < rhs.pageIndex;
  });

  // Do the glyph rendering.
  // The idea is to iterate through all glyphs that we have to draw.
  // While doing so, we keep track of the current page texture we're supposed to use.
  // Until that page texture changes (because a glyph references another page), we build up
  // the vertex and index buffers. When the page texture changes, we render all of the vertices
  // that we've built up and clear the buffers, so that we can start build the geometry of the *now new*
  // page texture.
  m_vertices.clear();
  m_indices.clear();

  m_vertices.reserve(m_glyphsToDraw.size() * 4);
  m_indices.reserve(m_glyphsToDraw.size() * 6);

  uint32_t previousPageIndex = 0;

  const auto flushBatch = [this, &previousPageIndex, cachedFont]() {
    SDL_RenderGeometry(m_sdlRenderer, cachedFont->pageTextures[previousPageIndex], m_vertices.data(),
                       int(m_vertices.size()), m_indices.data(), int(m_indices.size()));

    m_vertices.clear();
    m_indices.clear();
  };

  for (const auto& glyphToDraw : m_glyphsToDraw) {
    if (glyphToDraw.pageIndex != previousPageIndex) {
      flushBatch();
    }

    const auto baseVertex = int(m_vertices.size());

    m_vertices.push_back(glyphToDraw.vertices[0]);
    m_vertices.push_back(glyphToDraw.vertices[1]);
    m_vertices.push_back(glyphToDraw.vertices[2]);
    m_vertices.push_back(glyphToDraw.vertices[3]);

    m_indices.push_back(baseVertex);
    m_indices.push_back(baseVertex + 1);
    m_indices.push_back(baseVertex + 2);

    m_indices.push_back(baseVertex + 2);
    m_indices.push_back(baseVertex + 1);
    m_indices.push_back(baseVertex + 3);

    previousPageIndex = glyphToDraw.pageIndex;
  }

  // Draw the last batch.
  if (!m_vertices.empty()) {
    flushBatch();
  }
}


void RendererSDL::uncacheFont(const bmfgen::Font& font) {
  m_cachedFonts.erase(font.cacheKey());
}


RendererSDL::CachedFont::CachedFont(SDL_Renderer* sdlRenderer, const Font& font, bool& success) {
  this->pageTextures.reserve(font.pages().size());

  for (const auto& page : font.pages()) {
    // We only support 32-bit RGBA pages at the moment.
    SDL_Texture* texture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STATIC,
                                             int(page.width), int(page.height));

    if (texture == nullptr) {
      return;
    }

    SDL_UpdateTexture(texture, nullptr, page.data.get(), int(page.width * sizeof(uint8_t) * 4));

    // Enable alpha blending for this texture.
    SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);

    this->pageTextures.push_back(texture);
  }

  success = true;
}


RendererSDL::CachedFont::~CachedFont() noexcept {
  for (SDL_Texture* texture : this->pageTextures) {
    SDL_DestroyTexture(texture);
  }
  this->pageTextures.clear();
}


const RendererSDL::CachedFont* RendererSDL::lookupCachedFont(const Font& font) {
  auto it = m_cachedFonts.find(font.cacheKey());

  if (it == m_cachedFonts.cend()) {
    bool success{};
    const auto it2 = m_cachedFonts.emplace(font.cacheKey(), CachedFont{m_sdlRenderer, font, success});

    if (!success) {
      // Failed to create the cached font.
      return nullptr;
    }

    if (!it2.second) {
      return nullptr;
    }

    it = it2.first;
  }

  return &it->second;
}
} // namespace bmfgen

#endif // BMFGEN_ENABLE_SDL

#endif // BMFGEN_IMPLEMENTATION

A demo/fonts/Font_Subtitle1.bfont => demo/fonts/Font_Subtitle1.bfont +0 -0
A demo/fonts/Font_Subtitle2.bfont => demo/fonts/Font_Subtitle2.bfont +0 -0
A demo/fonts/Font_Title1.bfont => demo/fonts/Font_Title1.bfont +0 -0
A demo/main.cpp => demo/main.cpp +78 -0
@@ 0,0 1,78 @@
#include "SDL.h"

#define BMFGEN_IMPLEMENTATION
#define BMFGEN_ENABLE_SDL

#include "bmfgen.hpp"

int main() {
  // Set up SDL.
  SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);

  SDL_Window* window = SDL_CreateWindow("BMFGen SDL Demo", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640,
                                        480, SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI);

  SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

  // Create the SDL-based font renderer.
  bmfgen::RendererSDL fontRenderer{renderer};

  // Load up the BMFGen-generated fonts.
  const std::string fontPath{FONTS_PATH};

  bmfgen::Font titleFont{fontPath + "Font_Title1.bfont"};
  bmfgen::Font subtitleFont1{fontPath + "Font_Subtitle1.bfont"};
  bmfgen::Font subtitleFont2{fontPath + "Font_Subtitle2.bfont"};

  if (!titleFont || !subtitleFont1 || !subtitleFont2) {
    // Loading the fonts failed.
    return 1;
  }

  bool shouldQuit{};

  Uint64 currentTime = SDL_GetPerformanceCounter();
  Uint64 previousTime = 0;
  double totalTime = 0.0;

  while (!shouldQuit) {
    SDL_Event event{};
    while (SDL_PollEvent(&event)) {
      if (event.type == SDL_QUIT) {
        shouldQuit = true;
      }
    }

    previousTime = currentTime;
    currentTime = SDL_GetPerformanceCounter();

    const auto elapsedTime =
        double((currentTime - previousTime) * 1000 / (double)SDL_GetPerformanceFrequency()) * 0.001;

    totalTime += elapsedTime;

    const SDL_Color clearColor{100, 149, 237, 255};
    SDL_SetRenderDrawColor(renderer, clearColor.r, clearColor.g, clearColor.b, clearColor.a);
    SDL_RenderClear(renderer);

    const auto textOffset1 = std::sin(totalTime * 2.4) * 50.0;
    const auto textOffset2 = std::cos(totalTime * 4.0) * 20.0;

    fontRenderer.drawText(titleFont, "Hello World from BMFGen!", 70.0 + textOffset1, 20.0);
    fontRenderer.drawText(subtitleFont1, "And from SDL!", 200, 220 + textOffset2);
    fontRenderer.drawText(subtitleFont2,
                          "This is a simple demo,\nshowcasing some\nBMFGen-generated fonts.\nhttps://bmfgen.com", 230,
                          450);

    SDL_RenderPresent(renderer);
  }

  // Release the font renderer.
  fontRenderer = {};

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);
  SDL_Quit();

  return 0;
}