Rendering Glyphs from a Storage Buffer

Skytrias | Michael Kutowski

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? image.png

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_buffer and let the system rebuild the wanted glyphs

Thanks for posting it here.