How Alacritty Loads and Caches Glyphs for OpenGL ES 2.0 and GLSL 3 Rendering
Alacritty rasterizes fonts using crossfont, stores glyphs in a CPU-side HashMap within GlyphCache, and uploads bitmaps to 1024×1024 GPU texture atlases via the LoadGlyph trait, enabling both OpenGL ES 2.0 and GLSL 3 renderers to share identical caching logic while using renderer-specific batching for final display.
Alacritty achieves high-performance terminal font rendering by decoupling font rasterization from GPU presentation. The implementation supports both legacy OpenGL ES 2.0 contexts and modern OpenGL 3.3 (GLSL 3) drivers through a unified glyph caching architecture. This article examines how the GlyphCache module coordinates with renderer-specific backends to load, cache, and display glyphs across both graphics APIs.
The Font Rendering Pipeline: From Character to GPU
The journey from a Unicode character to a screen pixel follows a six-stage pipeline that remains consistent across both renderer backends:
- Initialization – The
Renderer(Gles2RendererorGlsl3Renderer) creates a GL program, allocates VAO/VBO/IBO buffers, and initializes an atlas list (Vec<Atlas>) to hold texture atlases. - Cache Construction –
GlyphCache::new(lines 82–94 inglyph_cache.rs) instantiates acrossfont::Rasterizer, stores keys for four font faces (normal, bold, italic, bold-italic), and records font metrics. - Preloading –
GlyphCache::load_common_glyphswarms the cache by rasterizing ASCII characters for all four faces, ensuring the first frame renders without stutter. - Lookup and Rasterization –
GlyphCache::get(lines 200–226) queries theHashMap<GlyphKey, Glyph>. On cache miss, it callsrasterizer.get_glyphand delegates the bitmap to the renderer'sLoadGlyphimplementation. - GPU Upload –
LoadGlyph::load_glyphuploads theRasterizedGlyphbitmap into the current texture atlas viaAtlas::load_glyph, returning aGlyphstruct containing UV coordinates and metrics. - Batch Submission –
RenderApiaccumulates cells in aBatchstruct; when the API handle drops, it submits a single draw call (glDrawElementsfor GLES2,glDrawElementsInstancedfor GLSL3).
GlyphCache: The CPU-Side HashMap
The GlyphCache struct serves as the authoritative source for rasterized glyph data, residing entirely in CPU memory while maintaining references to GPU resources.
pub struct GlyphCache {
cache: HashMap<GlyphKey, Glyph, RandomState>,
rasterizer: Rasterizer,
font_key: FontKey,
bold_key: FontKey,
italic_key: FontKey,
bold_italic_key: FontKey,
font_size: Size,
metrics: Metrics,
builtin_box_drawing: bool,
}
According to the Alacritty source code in alacritty/src/renderer/text/glyph_cache.rs, the cache stores four separate FontKey instances to handle style variations without re-querying the font database. The builtin_box_drawing flag enables a fallback bitmap font for box-drawing characters when the primary font lacks Unicode support.
When GlyphCache::get encounters a missing glyph, it either rasterizes the character via crossfont or substitutes a built-in box-drawing glyph, then immediately forwards the resulting bitmap to the active renderer's LoadGlyph implementation for texture upload.
The LoadGlyph Trait: Abstracting Renderer Backends
Alacritty unifies OpenGL ES 2.0 and GLSL 3 code paths through the LoadGlyph trait, defined in alacritty/src/renderer/text/mod.rs. This abstraction allows GlyphCache to remain renderer-agnostic.
pub trait LoadGlyph {
fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph;
fn clear(&mut self);
}
Both concrete implementations delegate to Atlas::load_glyph, differing only in their RenderApi context:
- OpenGL ES 2.0 – Implemented in
alacritty/src/renderer/text/gles2.rs(lines 57–63). TheRenderApimaintainsactive_tex,atlas, andcurrent_atlasreferences. - GLSL 3 – Implemented in
alacritty/src/renderer/text/glsl3.rs(lines 64–70). The logic is identical, though theRenderApistructure contains additional state for instanced rendering.
This trait-based architecture ensures that adding a new renderer backend requires only implementing LoadGlyph and TextRenderer, leaving the complex font rasterization and caching logic untouched.
Atlas Management: Packing Glyphs into GPU Textures
The Atlas module (alacritty/src/renderer/text/atlas.rs) manages 1024×1024 RGBA texture atlases, packing glyphs row-wise to minimize GPU memory fragmentation.
Key operations include:
room_in_row– Checks if the current glyph fits in the active row's remaining horizontal space.advance_row– Moves to the next row when the current row fills; allocates a new atlas when the texture exhausts vertical space.insert– Uploads bitmap data viaglTexSubImage2Dand calculates normalized UV coordinates for the vertex shader.
When Atlas::load_glyph detects insufficient space, it automatically creates a new texture atlas, appending it to the renderer's Vec<Atlas>. The system maintains a linked list of active atlases, with older atlases remaining valid for glyphs still visible on screen.
Renderer Implementations: GLES2 vs GLSL3 Batching
While both renderers share the same GlyphCache and Atlas logic, they diverge in how they submit vertices to the GPU:
OpenGL ES 2.0 (Gles2Renderer)
Uses immediate-mode-style batching where each cell generates four vertices stored in Vec<TextVertex>. The implementation in gles2.rs (lines 75–118) builds vertex data containing position, color, and UV coordinates. Drawing occurs via glDrawElements with a standard vertex array, as shown in lines 190–222.
GLSL 3 (Glsl3Renderer)
Employs hardware instancing for reduced CPU overhead. The Batch struct holds Vec<InstanceData> containing per-glyph transforms and texture coordinates. The vertex shader expands each instance into a quad using a static VBO of unit squares. Implementation details appear in glsl3.rs (lines 42–74 for instance creation, lines 23–61 for drawing), using glDrawElementsInstanced.
Both implementations flush the batch automatically when RenderApi drops out of scope, ensuring a single draw call per frame regardless of text complexity.
Practical Example: Rendering Text with GlyphCache
The following Rust snippet demonstrates the complete workflow for setting up a renderer, initializing the cache, and drawing a line of text. This pattern appears throughout Alacritty's display module:
use alacritty::renderer::text::{Gles2Renderer, GlyphCache, LoadGlyph};
use alacritty_terminal::config::font::Font;
use alacritty_terminal::term::cell::Flags;
// Initialize the GL context (handled by Alacritty's event loop)
let mut renderer = Gles2Renderer::new(false, true)
.expect("GL renderer initialization failed");
// Create the glyph cache with default system fonts
let font = Font::default();
let mut cache = GlyphCache::new(renderer.rasterizer.clone(), &font)
.expect("Failed to initialize glyph cache");
// Preload ASCII characters to avoid frame drops on first render
let mut loader = renderer.loader_api();
cache.load_common_glyphs(&mut loader);
// Render a line of text
renderer.with_api(&size_info, |mut api| {
for (col, ch) in "Hello, OpenGL!".chars().enumerate() {
let key = GlyphKey {
font_key: cache.font_key,
character: ch,
size: font.size(),
};
// Retrieve or rasterize; uploads to GPU if necessary
let glyph = cache.get(key, &mut api, true);
// Construct a renderable cell (normally generated by terminal state)
let cell = RenderableCell {
point: Point { column: col.into(), line: 0 },
fg: Rgb { r: 255, g: 255, b: 255 },
bg: Rgb { r: 0, g: 0, b: 0 },
flags: Flags::empty(),
..Default::default()
};
// Add to batch; draws automatically when `api` drops
api.batch().add_item(&cell, &glyph, &size_info);
}
});
Under the hood, cache.get performs the HashMap lookup, falls back to crossfont rasterization on miss, and invokes api.load_glyph to populate the texture atlas. The add_item call stores instance data; when the closure ends and api drops, RenderApi::drop triggers the final glDrawElements or glDrawElementsInstanced call.
Summary
GlyphCachemaintains aHashMap<GlyphKey, Glyph>for O(1) lookup of rasterized glyphs, with separate font keys for bold, italic, and bold-italic variants.- The
LoadGlyphtrait abstracts GPU upload logic, allowing identical cache code to serve both OpenGL ES 2.0 and GLSL 3 renderers. Atlasmanages 1024×1024 RGBA textures, packing glyphs row-wise and allocating new atlases automatically when space exhausts.- Batching collects per-cell data into a single draw call; GLES2 uses vertex arrays while GLSL3 uses hardware instancing via
glDrawElementsInstanced. - Source references include
glyph_cache.rs(lines 82–94, 200–226),gles2.rs(lines 57–63, 75–118, 190–222),glsl3.rs(lines 23–61, 42–74, 64–70), andatlas.rs(lines 24–46, 124–152, 190–226).
Frequently Asked Questions
How does Alacritty handle missing font glyphs?
When GlyphCache::get fails to locate a glyph in the primary font, it first attempts rasterization via crossfont. If the font lacks the Unicode character entirely, Alacritty falls back to a built-in bitmap font for box-drawing characters (defined in builtin_font.rs), ensuring terminals render frames and lines correctly regardless of font coverage.
What is the maximum texture size for glyph atlases?
Alacritty allocates glyph atlases as 1024×1024 RGBA textures. When the current atlas fills, the system creates a new texture rather than expanding existing ones, maintaining a Vec<Atlas> that functions as a texture paging system. This 1024×1024 dimension balances GPU memory efficiency with the need to minimize texture switches during batch rendering.
Why does Alacritty support both OpenGL ES 2.0 and GLSL 3?
The dual-backend architecture maximizes hardware compatibility. OpenGL ES 2.0 supports older embedded systems and constrained environments, while GLSL 3 (OpenGL 3.3) enables hardware instancing and modern shader features on desktop GPUs. The LoadGlyph trait and shared GlyphCache ensure both paths deliver identical visual output without code duplication.
How are glyphs batched for drawing?
Both renderers accumulate cells into a Batch struct during the with_api closure. The GLES2 renderer stores individual vertices in Vec<TextVertex> and draws with glDrawElements, while the GLSL3 renderer stores instance data in Vec<InstanceData> and draws with glDrawElementsInstanced. The batch flushes automatically when the RenderApi handle drops, ensuring exactly one draw call per frame per atlas.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →