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.