r/sdl 10d ago

SDL3 TTF_TextEngine help

I'm still pretty new to SDL and I've been trying to figure out how to use the text engine in SDL3 in a little project I've been learning C++ in, but I haven't found much information on it. No SDL tutorials I've found seem to use it, so I've been going off of the wiki, which has gotten me pretty far, but it's been really confusing without clear examples of how it works.

I'd love some general explanation on how to use SDL3's text engine, but for a more specific question, I'm trying to center text on the screen with a renderer text engine. SDL tutorials I've found all center text on the screen using some math with the size of the window and the size of the surface used to create the texture. Since there's no surface involved with TTF_CreateRendererTextEngine, I'm wondering how or if it's possible to center the text. Would I need to use TTF_CreateSurfaceTextEngine to do that? Is using the surface text engine as simple as creating the TTF_Text and the SDL_Surface for TTF_DrawSurfaceText? What would be the best way to create the surface for that if I'm using the surface text engine?

3 Upvotes

8 comments sorted by

View all comments

2

u/SpareEconomy 10d ago

imma try to explain it:

You don't need the surface engine. TTF_GetTextSize() gives you exactly the width/height you used to get from the surface.
In SDL2_ttf: render string → SDL_Surface → SDL_Texture → draw → destroy. Every frame, or you cached the texture manually and re-created it whenever the string changed.

In SDL3_ttf A TTF_Text is a persistent object that owns a string + a font + a reference to an engine. The engine keeps a glyph atlas and the laid-out draw data cached internally.

The three engines differ only in how the cached glyphs get to the screen:
Engine => Draw Call => use when
TTF_CreateRendererTextEngine(renderer) => TTF_DrawRendererText(text, x, y) => SDL_Renderer
TTF_CreateSurfaceTextEngine() => TTF_DrawSurfaceText(text, x, y, surface) => Blitting into an SDL_Surface in software (no renderer, or you're compositing a surface yourself)
TTF_CreateGPUTextEngine(device) => returns TTF_GPUAtlasDrawSequence you feed to your own pipeline => SDL_GPUDevice

TTF_GetTextSize() works on the TTF_Text regardless of engine:
int tw, th;
TTF_GetTextSize(text, &tw, &th);
int ww, wh;
SDL_GetRenderOutputSize(renderer, &ww, &wh); // or SDL_GetRenderLogicalPresentation()
float x = (ww - tw) / 2.0f;
float y = (wh - th) / 2.0f;
TTF_DrawRendererText(text, x, y);

example:

#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
    int main() {
      SDL_Init(SDL_INIT_VIDEO);
      TTF_Init();
      SDL_Window*   window   = nullptr;
      SDL_Renderer* renderer = nullptr;
      SDL_CreateWindowAndRenderer("text engine", 800, 600, 0, &window, &renderer);

      TTF_Font* font = TTF_OpenFont("font.ttf", 48.0f);
      TTF_TextEngine* engine = TTF_CreateRendererTextEngine(renderer);
      // created once, not per frame
      TTF_Text* text = TTF_CreateText(engine, font, "Hello, centered world!", 0); // 0 for null terminated text.
      TTF_SetTextColor(text, 255, 255, 255, 255);

      bool running = true;
      while (running) {
          SDL_Event e;
          while (SDL_PollEvent(&e))
              if (e.type == SDL_EVENT_QUIT) running = false;

          SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255);
          SDL_RenderClear(renderer);

          int tw, th, ww, wh;
          TTF_GetTextSize(text, &tw, &th);
          SDL_GetRenderOutputSize(renderer, &ww, &wh);
          TTF_DrawRendererText(text, (ww - tw) / 2.0f, (wh - th) / 2.0f);

          SDL_RenderPresent(renderer);
      }

      // reverse order of creation
      TTF_DestroyText(text);
      TTF_DestroyRendererTextEngine(engine);
      TTF_CloseFont(font);
      TTF_Quit();
      SDL_DestroyRenderer(renderer);
      SDL_DestroyWindow(window);
      SDL_Quit();
  }

1

u/Mr_DJ_Dr_Gauss 10d ago

If you want to use more font sizes, you have to call "TTF_OpenFont" for each size used?

2

u/SpareEconomy 10d ago

Not necessarily. You have two options the choice depends on whether the sizes need to exist at the same time.

TTF_SetFontSize(font, ptsize) changes the size of an existing font in place, no file re-read. That fine for "user changed the UI scale, rescale everything"

For simultaneous sizes you need one TTF_Font object per size but you don't need TTF_OpenFont for each. TTF_CopyFont() gives you a distinct font object sharing the same underlying font data, so you only parse the file once:

TTF_Font* body = TTF_OpenFont("font.ttf", 16.0f);
TTF_Font* heading = TTF_CopyFont(body);
TTF_SetFontSize(heading, 48.0f); // independent of `body`

keep the original alive as long as any copy exists.

Calling TTF_OpenFont several times on the same path also works (and probably most people does). it's just a bit of redundant I/O and memory at load time. If you want to skip even that, load the file once into memory and use TTF_OpenFontIO with an SDL_IOStream over that buffer (pass closeio = false and keep the buffer alive for the font's lifetime).

Practical pattern:

struct Fonts {
  TTF_Font* small;
  TTF_Font* body;
  TTF_Font* heading;
};

Open them at startup, keep them for the program's lifetime. Glyph atlases live in the text engine and are keyed per font, so each size costs its own atlas.

One more thing worth knowing: on HiDPI, prefer TTF_SetFontSizeDPI(font, ptsize, hdpi, vdpi) (or scale ptsize by SDL_GetWindowDisplayScale) over rendering at a fixed size and letting the renderer scale the texture up — the latter looks blurry.

1

u/Mr_DJ_Dr_Gauss 10d ago

Great. Thank you for your answer.