ADR-018: Unicode Rendering Architecture for VR Overlays
Status
Accepted
Context
The VR overlay system was displaying "tofu" (□) blocks for unicode characters including:
- Greek letters (μ, Σ, π)
- Mathematical symbols (∑, ∏, √, ∫)
- Box drawing characters (┌, ┐, └, ┘)
- Technical symbols (°, •, ✓)
- Arrows and other special characters
Investigation revealed that the DirectX 12 backend was blocking font atlas updates after initial texture creation. The code had a check if self.texture_generation == 0 that prevented egui from dynamically loading new glyphs as they were encountered.
This was a critical issue for:
- System monitoring displays showing units like "μs" (microseconds)
- Technical dashboards with mathematical symbols
- UI elements using box drawing for borders
- Status indicators using checkmarks and bullets
Decision
We implemented a comprehensive unicode rendering solution with three key components:
1. Enable Dynamic Font Atlas Updates
Modified the DirectX 12 backend to allow font atlas updates at any time:
// Allow updates - egui knows when it needs to update the atlas
self.texture_generation += 1;
font_atlas_updated = true;
2. Multi-Font System with Fallback
Implemented a two-font system:
- Primary: Fira Code Nerd Font - Modern monospace font with programming ligatures and Nerd Font icons
- Fallback: DejaVu Sans - Comprehensive unicode coverage ensuring no missing glyphs
3. Embedded Font Loading
Fonts are embedded in the binary and loaded at initialization:
const FIRA_CODE_NERD: &[u8] = include_bytes!("../../fonts/FiraCodeNerdFont-Regular.ttf");
const DEJAVU_SANS: &[u8] = include_bytes!("../../fonts/DejaVuSans.ttf");
Technical Implementation
Font Configuration
fn configure_unicode_fonts(egui_ctx: &EguiContext) {
let mut fonts = egui::FontDefinitions::default();
// Load fonts
fonts.font_data.insert(
"Fira Code Nerd Font".to_owned(),
FontData::from_static(FIRA_CODE_NERD),
);
fonts.font_data.insert(
"DejaVu Sans".to_owned(),
FontData::from_static(DEJAVU_SANS),
);
// Configure fallback chain
fonts.families.get_mut(&FontFamily::Proportional).unwrap()
.extend(["Fira Code Nerd Font", "DejaVu Sans"]);
fonts.families.get_mut(&FontFamily::Monospace).unwrap()
.extend(["Fira Code Nerd Font", "DejaVu Sans"]);
egui_ctx.set_fonts(fonts);
}
Texture Update Flow
- egui detects missing glyphs and rasterizes them
- Font atlas texture is updated with new glyphs
- DirectX 12 backend receives texture delta
- Texture manager handles partial updates efficiently
- GPU texture is updated preserving existing glyphs
Pre-warming Strategy
To avoid runtime hitches, we pre-warm the font atlas with commonly used characters:
pub fn warm_up_fonts(egui_ctx: &EguiContext) {
let chars = "αβγδεζηθικλμνξοπρστυφχψωΓΔΘΛΞΠΣΦΨΩ∑∏√∫∂∇≈≠≤≥±∞°℃℉•◦‣✓✗→←↑↓";
// Render off-screen to force glyph loading
}
Consequences
Positive ✅
- Complete Unicode Support: All required characters render correctly
- Dynamic Loading: New characters loaded on-demand without restart
- Optimal Performance: Only loaded glyphs consume memory
- Consistent Appearance: Fallback ensures no tofu blocks
- Developer Friendly: Easy to add new fonts or change font preferences
- Production Ready: Font warming prevents runtime hitches
- No External Dependencies: Embedded fonts work on any system
Negative ❌
- Binary Size: ~6MB increase from embedded fonts
- Initial Load Time: Slight increase due to font parsing
- Memory Usage: Font atlas grows with unique characters used
- Font Licensing: Must ensure fonts are properly licensed for embedding
Alternatives Considered
1. Single Unicode Font
- Rejected: No single font has perfect coverage and good aesthetics
- Fira Code lacks some symbols, DejaVu lacks programming features
2. System Font Loading
- Rejected: System fonts vary across Windows installations
- Would require fallback logic for missing fonts
- Inconsistent appearance across systems
3. Static Font Atlas
- Rejected: Would require pre-generating all possible characters
- Massive memory waste for rarely used glyphs
- Inflexible for adding new character support
4. Disable Partial Updates
- Rejected: Would require full texture recreation for new glyphs
- Performance impact and potential flickering
- Goes against egui's design
5. Custom Font Rendering
- Rejected: Massive implementation effort
- Would duplicate egui's well-tested font system
- Maintenance burden
Performance Impact
- Binary Size: +6MB (embedded fonts)
- Initialization: +10-50ms (font parsing)
- Runtime: Negligible (only when new glyphs encountered)
- Memory: ~2-4MB for typical font atlas
- GPU: Minimal texture updates
Future Improvements
- Font Subsetting: Include only required unicode ranges
- Async Loading: Load fonts in background
- Font Caching: Persist rasterized font atlas between runs
- Custom Font UI: Allow users to select preferred fonts
- Font Metrics: Track which characters are actually used
References
- unicode-rendering-solution.md - Implementation details
- text-rendering-methodology.md - Complete pipeline documentation
- egui font documentation: https://docs.rs/egui/latest/egui/struct.FontDefinitions.html
Date
2025-01-12
Authors
- kittyn (via Claude Code)