Short blog post I had on substack, which I'm retiring and posting an update on here. The technique I'm describing avoids using a 2D texture and stores the built glyph directly in bytes.
As you can imagine this is pretty bad in comparison to the 2D texture approach. Storing glyphs in 2D Textures has benefits like tightly packing, overriding sections of a texture and most importantly the fragment shader can most likely read faster from a texture than reinterpreting from our storage buffer.
With that said let's just see if we can get this "alternative" working :)
Initialization & Building
I’m using Odin for the code examples and the wonderful stb truetype library for loading and dealing with fonts.
Let’s look into a simple way to implement this when looking at the data.
// mapping codepoint + pixel_size to the built glyph in the glyph buffer
Glyph_Key :: struct {
codepoint: rune,
pixel_size: f32,
}
// glyph information
Glyph_Slot :: struct {
offset: u32, // offset into the glyph_buffer
xoff, yoff: i16, // glyph visual offset
width, height: u16, // glyph dimensions
ascent: f32, // based on used pixel_size
xadvance: f32, // based on used pixel_size
}
Renderer_State :: struct {
// ...
glyph_buffer: []byte, // storing all glyph pixels
glyph_buffer_index: int, // write position
glyph_buffer_map: map[Glyph_Key]Glyph_Slot, // mapping key to built glyph
font_regular: Font_Keep, // storing the font info + font bytes
}
Building the wanted codepoint into the glyph_buffer is also pretty simple. rs.vrs is my global Vulkan Renderer State.
vrs_build_glyph :: proc(
info: ^stbtt.fontinfo,
codepoint: rune,
pixel_size: f32,
) -> (res: Glyph_Slot) {
m := &rs.vrs.glyph_buffer_map
// request codepoint + pixel_size
key := Glyph_Key {
codepoint,
pixel_size,
}
if glyph, ok := m[key]; ok {
res = glyph
} else {
// get correct offset bytes
b := rs.vrs.glyph_buffer[rs.vrs.glyph_buffer_index:]
// convert to glyph once, instead of per call
glyph := stbtt.FindGlyphIndex(info, codepoint)
// glyph bounding box
x0, y0, x1, y1: i32
scale := stbtt.ScaleForPixelHeight(info, pixel_size)
stbtt.GetGlyphBitmapBox(info, glyph, scale, scale, &x0, &y0, &x1, &y1)
w := x1 - x0
h := y1 - y0
// build glyph into bytes
// stride is width of glyph! as seen in stbtt internals
stbtt.MakeGlyphBitmap(info, raw_data(b), w, h, w, scale, scale, glyph)
// gather glyph positional info
ascent := font_ascent(info, scale)
xadvance, lsb: i32
stbtt.GetGlyphHMetrics(info, glyph, &xadvance, &lsb)
// create glyph slot
res = Glyph_Slot {
offset = u32(rs.vrs.glyph_buffer_index),
xoff = i16(x0),
yoff = i16(y0),
width = u16(w),
height = u16(h),
ascent = ascent,
xadvance = f32(xadvance) * scale,
}
m[key] = res
// offset write index by width and height
rs.vrs.glyph_buffer_index += int(w) * int(h)
}
return
}
When rendering glyphs we can call this procedure to retrieve the wanted glyph slot - whenever one doesn't exist, it will be built it into the glyph_buffer.
Per font you will need one of these maps to lookup each codepoint + pixel_size you want. In case you only want to use one map - you could designate a few KB per font and offset on each font. Font 1 takes 0-2KB, Font 2 takes 2-4KB, …
Storage Buffer
I won’t go into the details of how to create a Storage Buffer - as that should be simple enough in most cases.
Update the Storage Buffer to the glyph_buffer data every frame or whenever a new glyph gets built into the buffer.
Shader
Now we have to reconstruct the glyph texture in the fragment shader. The code below will assume you can feed the Glyph_Slot data into the vertex shader.
// glyphs only store 1 byte per pixel (bpp) so the R (red) value, we need to get the correct value out of each 4 byte value though
layout(std430, binding = 3) readonly buffer Glyph_Buffer {
uint data[];
} glyph_buffer;
// based on https://stackoverflow.com/a/60469946
// provide wanted 1B index you want from 4B (uint) based array
uint get_byte(uint byte_idx) {
uint byte_in_uint = byte_idx % 4;
uint vec_idx = byte_idx / 4;
uint bytes = glyph_buffer.data[vec_idx];
return (bytes >> ((byte_in_uint) * 8)) & 0xFF;
}
void main() {
// ...
// get screen position, v_* values are provided through vertex shader
float x = (gl_FragCoord.x - v_pos_x);
float y = (gl_FragCoord.y - v_pos_y);
uint y_off = uint(y) * v_glyph_width;
uint byte = get_byte(uint(v_glyph_offset + x + y_off));
float alpha = float(byte) / 255;
frag_color = v_color_goal * alpha;
}
Maybe this could be done better or more efficient, let me know if you know better ways to achieve this kind of extraction.
Result
That’s about it, pretty simple right?

Notes:
What is this even good for?
- Easier to create than a good texture cache but less efficient
- Nothing else
What if you want to remove built glyphs?
- Would need better allocation scheme where you can reset regions like a free list
- Clear
glyph_bufferand let the system rebuild the wanted glyphs