Skip to main content

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

  1. egui detects missing glyphs and rasterizes them
  2. Font atlas texture is updated with new glyphs
  3. DirectX 12 backend receives texture delta
  4. Texture manager handles partial updates efficiently
  5. 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

  1. Font Subsetting: Include only required unicode ranges
  2. Async Loading: Load fonts in background
  3. Font Caching: Persist rasterized font atlas between runs
  4. Custom Font UI: Allow users to select preferred fonts
  5. Font Metrics: Track which characters are actually used

References

Date

2025-01-12

Authors

  • kittyn (via Claude Code)