Failed Optimisation
Published: 2026-06-11
Background
I took some time today integrate tracy in one of my games(currently private) to see where the bottlenecks were in my game.
Nothin looked out of the ordinary, everything was okay, but there was this one function draw_centered_text which drew my attention. It, in itself didn’t take much time (50-150μs) but there was something a bit shocking about it.
draw_centered_text :: proc(text: ^ttf.Text, x_offset: f32 = 0.0, y_offset: f32 = 0.0) {
if text == nil do return
w, h, tw, th: i32
sdl.GetWindowSize(window, &w, &h)
ttf.GetTextSize(text, &tw, &th)
x := (f32(w - tw) * 0.5) + x_offset
y := (f32(h - th) * 0.5) + y_offset
ttf.DrawRendererText(text, x, y)
} It is very simple function that renders text in the middle of the screen.
Now, a question for you: What part of this function do you think takes the most amount of time?
draw_centered_text :: proc(text: ^ttf.Text, x_offset: f32 = 0.0, y_offset: f32 = 0.0) {
if text == nil do return
w, h, tw, th: i32
// Getting the window size?
sdl.GetWindowSize(window, &w, &h)
// Getting the text size?
ttf.GetTextSize(text, &tw, &th)
// Doing the calculations?
x := (f32(w - tw) * 0.5) + x_offset
y := (f32(h - th) * 0.5) + y_offset
// Drawing the text in a texture?
ttf.DrawRendererText(text, x, y)
} The correct answer is
draw_centered_text :: proc(text: ^ttf.Text, x_offset: f32 = 0.0, y_offset: f32 = 0.0) {
if text == nil do return
w, h, tw, th: i32
sdl.GetWindowSize(window, &w, &h)
ttf.GetTextSize(text, &tw, &th)
x := (f32(w - tw) * 0.5) + x_offset
y := (f32(h - th) * 0.5) + y_offset
ttf.DrawRendererText(text, x, y)
} Actually this single takes more than 90% of total time! (~40-130μs)!
The Fix
There were many static texts (texts that didn’t change). So I decided to cache their height and width during initialisation. Simple fix, I made a struct to hold the data and initialise them at startup.
// file: ttf.odin
Text :: struct {
text: ^ttf.Text,
width: u32,
height: u32,
}
welcome_text: Text
createText :: proc(t: ^Text, str: cstring) -> int {
t.text = ttf.CreateText(engine, font, str, 0)
if t.text == nil {
// ... error things
return 1
}
ttf.SetTextColor(t.text, 255, 255, 255, 255)
ttf.GetTextSize(t.text, &t.width, &t.height)
return 0
}
initFonts :: proc() -> int {
// ... other font init stuff
t: cstring : "Welcome To [Redacted]\nHope you enjoy playing!"
if createText(&welcome_text, t) != 0 do return 1
return 0
}
closeFonts :: proc() {
// close fonts
} Ofcourse it won’t work with texts that Wanna know what happened next? I saved a total of 2μs! BUT WHYYY?
Denial
I dived into the C source code of SDL_ttf. I found out that SDL devs are smart. They don’t like to do the same calculations twice. I think, when I called GetTextSize() it did all the hardworking calculations of kerning, generating glyphs, and other font stuff - then cache the results for later use.
When I call DrawRendererText(), it does most of the calculations GetTextSize() needs to do, but if I already called GetTextSize(), it uses the cached values.