Skytrias' Programming Cloud

Share this post
Variable Font Sizes using Storage Buffers
skytrias.substack.com

Variable Font Sizes using Storage Buffers

Rendering fonts at different font sizes with ease

Skytrias
May 9
Share this post
Variable Font Sizes using Storage Buffers
skytrias.substack.com

An issue I have in my todo list project Todool is to support rendering various glyphs at different sizes. Sadly there are only a few good solutions, each with their own tradeoffs, to render glyphs at different sizes in a performant way.

When researching I found fontstash to be a good approach to this issue. From what I can gather it builds the wanted glyph pixels into a texture - probably tightly packs them.

This way you can build each glyph at any font size and render it cost free. The only issue would be to support X font sizes for the same glyph.

Last week i was working on a Vulkan Renderer for my project and I got the idea that you could avoid having to deal with the texture insertion of these glyphs. Instead you could embed each glyph at runtime into a 1D Texture or a big Storage Buffer and gather the 2D glyph textures you want to render.


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 doesnt 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 if you want to remove built glyphs?

  • You don’t need to as there is no texture packing

  • Clear glyph_buffer and let the system rebuild the wanted glyphs

What if you want fine grained scaling animations of every glyph?

  • I doubt this solution will fit your needs then

  • Try out SDF fonts or vertex textures

Big font sizes

  • bad as they can quickly fill up the glyph_buffer with their enourmous size

  • can easily take up +1MB

Share this post
Variable Font Sizes using Storage Buffers
skytrias.substack.com
Comments

Create your profile

0 subscriptions will be displayed on your profile (edit)

Skip for now

Only paid subscribers can comment on this post

Already a paid subscriber? Sign in

Check your email

For your security, we need to re-authenticate you.

Click the link we sent to , or click here to sign in.

TopNew

No posts

Ready for more?

© 2022 Skytrias
Privacy ∙ Terms ∙ Collection notice
Publish on Substack Get the app
Substack is the home for great writing