PVG
7. Text Rendering and Memory Allocators — Program Video Games

7. Text Rendering and Memory Allocators

[[programvideogames]]So far we haven't done anything that requires worrying about memory allocation.

However, to do text rendering using Raylib, we need to create what's called "c-strings".
C-strings are strings compatible with the C programming language.
They are a series of bytes with a null-terminator at the end.
We usually say it's a zero at the end, but that's a bit confusing.
If it's a zero, then why can we write the number zero in strings no problem?

Here's a little drawing of a string "Hello there" in the C format.
Above is the text we see, below are the byte values as a number.
![[Pasted image 20240723075036.png]]

Notice that 0, 1, 2, 3 are not represented by the byte values 0, 1, 2, 3.
When programmers are talking about strings terminated by 0 - they refer to the byte value, not the value we see.
If you want to learn more about this, look into ASCII encoding followed by Unicode.

I'll show you two ways to draw text with Raylib.
The first uses a local memory buffer.
The second uses the default temporary memory allocator.

Local Memory Buffer

You may be familiar with this pattern from C when using sprintf.

// create a zero-valued array of bytes with 128 elements
buf: [128]byte
// buffer-print-format
fmt.bprintf(buf[:], "Position: %v", pos)
// convert buf to string
str := string(buf[:])
// convert string to cstring
// we can do this because our buffer is filled with 0s
// need to import "core:strings" for this procedure
cstr := strings.unsafe_string_to_cstringarbitrary(str)
// finally, draw the text
rl.DrawText(cstr, 8, 8, 20, rl.WHITE)

[:] gets a slice from an array. A slice is a pointer + size.
There is optionally a beginning (inclusive) and end (exclusive) index on either side of the colon. Without defining these, the whole thing is sliced.
buf[0:4] is a slice containing Posi.
buf[4:6] is a slice containing ti.
%v is a format verb that tells the compiler what the string representation of our pos variable should look like.
%v means "default". I think of it as "let the compiler figure it out".
There are a bunch more, if you want something specific, check out: https://pkg.odin-lang.org/core/fmt/

The advantage of this method is that it's all locally scoped and uses stack memory.
You don't have to worry about memory leaks.

The disadvantage is it's pretty cumbersome.

Temporary Memory Allocator

Here's the version I prefer to use:

cstr := fmt.ctprintf("Position: %v", pos)
rl.DrawText(cstr, 8, 8, 20, rl.WHITE)

Odin has a couple of built in procedures that allocate strings to the "temporary allocator".

What is the temporary allocator? It's a memory buffer that we can put stuff into and then clear whenever is convenient for us.

Due to how it works, it's very cheap to allocate memory - unlike traditional malloc or new.
It's also very cheap to clear.

The advantage of this method is it's dead simple.
The disadvantage of this method is there is a memory leak if we don't clear it.

So, here's how I usually clear it in games:

for !rl.WindowShouldClose() {
    free_all(context.temp_allocator)
    // ... the rest of our code
}

context is an invisible variable passed into every procedure call in Odin.
It contains some useful values, one of them is the temp_allocator.

For more information, check here: https://odin-lang.org/docs/overview/#implicit-context-system

This turns our temp_allocator into a "frame-allocator".
That is, the memory inside only lives for 1 frame.
Useful for data such as physics results, strings, or anything else we have an unknown size for each frame.

If we want data to persist between frames, we need to either:

  • allocate using context.allocator and don't clear it
  • Create our own special allocator for the purpose (easy with Odin's standard library allocators)

Now that we understand a bit more about rendering text and memory, we can move onto the next lecture: Introduction to Scenes.