Variable Font Sizes using Storage Buffers
Rendering fonts at different font sizes with ease
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
Create your profile
Only paid subscribers can comment on this post
Check your email
For your security, we need to re-authenticate you.
Click the link we sent to , or click here to sign in.