Back to Mirador Blog

Text Rendering With Glyphon

2025-06-30
8 min read
UI/UX
miradorgame-developmentrust

Text rendering in games presents unique performance challenges that can make or break a smooth gaming experience. Poorly optimized text systems can significantly impact frame rates, as I discovered while building a Doom-like engine last summer. My attempt at a custom glyph rasterizer not only halved the framerate but also produced jagged, unreadable results. This frustrating experience led me to seek a proven, battle-tested solution for Mirador. After evaluating several existing libraries, I chose [glyphon](https://github.com/grovesNL/glyphon), a GPU-accelerated text rendering library that integrates seamlessly with wgpu. Glyphon provides high-quality text rendering with support for custom glyphs, advanced typography features, and excellent performance characteristics, making it suitable for Mirador's current needs and future expansion plans.

Integration Challenges

The integration process revealed several unexpected complications that required careful consideration. Mirador doesn't use wgpu directly but accesses it through egui_wgpu to support real-time developer interfaces and debugging tools. This architectural decision, while beneficial for development workflow, created version compatibility issues with glyphon. Glyphon traditionally expects wgpu as a standalone crate with specific version requirements, but the version mismatches between egui_wgpu's bundled wgpu version and glyphon's expected version prevented successful compilation. This constraint forced me to carefully align dependency versions, ultimately requiring the use of glyphon version 0.8, which expects wgpu 24 - exactly the same version provided by egui_wgpu::wgpu. This version alignment ensures compatibility while maintaining the developer interface functionality that makes debugging and development much more efficient. The trade-off of using a slightly older but stable version of glyphon was well worth the benefits of having integrated development tools.

API Design

Glyphon's API, while powerful and feature-rich, requires many parameters for accurate text placement and styling. Rather than passing these parameters directly throughout the codebase and creating maintenance nightmares, I created a comprehensive wrapper that provides a much simpler and more intuitive interface. The wrapper organizes glyphon's complex requirements into nested structs that work together harmoniously to specify text rendering behavior. The TextStyle struct encapsulates all visual appearance properties in a clean, readable format. This includes font family selection, size specifications, line height for proper text spacing, color definitions, font weight for emphasis, and style variations like italic text. By grouping these related properties together, the code becomes more maintainable and the intent clearer. The TextPosition struct handles the spatial aspects of text rendering, including precise positioning coordinates and optional size constraints. The max width and height parameters allow for text wrapping and overflow control, which is essential for responsive UI design. This separation of styling from positioning makes it easier to reuse text styles across different positions and contexts. These components combine in the TextBuffer struct, which serves as the complete specification for what text to render and how. Each buffer maintains its own state including visibility, scaling factors, and the original text content for re-styling operations. This design allows for efficient updates and modifications without recreating entire text buffers.

Renderer Architecture

The TextRenderer struct serves as the central coordinator for all glyphon functionality within Mirador. Since glyphon requires direct window access for proper coordinate system management and DPI handling, the TextRenderer lives in the top-level AppState rather than being nested within the WgpuRenderer. This architectural decision provides several important benefits. This separation allows text rendering concerns to be isolated from general graphics rendering while maintaining the performance benefits of GPU acceleration. The renderer manages font systems, glyph caching, texture atlases, and viewport calculations automatically, freeing the rest of the application from these low-level details. The architecture also enables efficient resource management, with the text renderer handling font loading, glyph rasterization, and texture atlas updates independently of the main graphics pipeline. This independence allows text rendering to be optimized separately from 3D geometry rendering, leading to better overall performance.

Multiple Text Renderers

Mirador currently uses several separate text renderers to handle different UI contexts. This approach, while providing some organizational benefits, comes with significant overhead that I should address in future iterations.

Main Game Text Renderer

The primary TextRenderer in AppState handles all in-game text elements that players see during normal gameplay. This includes dynamic elements like score and level displays that automatically scale with window size, timer text with decimal precision for accurate time tracking, game over screen text with restart instructions, and title screen text with dynamic positioning that adapts to different screen sizes. The main renderer automatically handles DPI scaling and responsive text sizing through algorithms that calculate appropriate font sizes based on screen dimensions. For example, score text scales from a minimum of 16px to a maximum of 48px based on screen dimensions, ensuring readability across different resolutions while maintaining visual hierarchy.

Button System Text Renderer

The ButtonManager contains its own TextRenderer instance for UI button text rendering. This specialized renderer handles button labels with hover and pressed state styling, level indicators on upgrade buttons, tooltip text that appears on hover, and icon positioning relative to text elements. Each button can have multiple text buffers: main text for the primary button label, level text for numerical indicators, and tooltip text for additional information. The button renderer manages these independently, allowing for complex button layouts with proper text alignment and spacing.

Menu System Text Renderers

Both the pause menu and upgrade menu maintain their own TextRenderer instances within their respective ButtonManager components. This separation provides organizational benefits but comes with a significant performance cost - each renderer loads its own copy of the font system and glyph cache. The pause menu renderer handles resume, restart, and quit button text with appropriate styling for each action type. The upgrade menu renderer manages upgrade names, descriptions, and level indicators for the three upgrade slots, with layouts that include icons and detailed tooltips.

Rendering Pipeline Integration

Text rendering occurs in the final pass of Mirador's rendering pipeline, ensuring that all text elements appear above other visual elements. The main game screen rendering follows a carefully orchestrated sequence that maximizes performance while maintaining visual quality. The pipeline begins with a background pass that renders animated starfield effects using the StarRenderer. This creates the immersive space environment that serves as the game's backdrop. Next comes the geometry pass, which renders 3D maze walls and floors with proper depth testing to create the three-dimensional environment. The UI overlay pass follows, rendering timer and stamina bars that provide crucial gameplay information. The compass pass adds the directional compass overlay that helps players navigate the maze. Finally, the text pass renders all text elements on top of everything else. The text pass uses a separate render pass with no depth testing, ensuring text always appears above other elements regardless of their 3D position. Each text renderer prepares its buffers during the update phase, then renders them in the final pass. This approach provides good performance while maintaining visual clarity and proper layering.

Font Management

Mirador implements a font management system that embeds custom fonts using Rust's include_bytes!() macro, which compiles font files directly into the binary. This approach eliminates dependency on system fonts and ensures consistent appearance across different operating systems. The system supports multiple font families with fallback mechanisms. Currently, Mirador includes the Hanken Grotesk font family for consistent branding and visual identity. If custom fonts fail to load for any reason, the system gracefully falls back to "DejaVu Sans", ensuring text always renders even in worst-case scenarios. Font loading occurs during TextRenderer initialization, with each successfully loaded font registered in the loaded_fonts vector for future reference. The system includes error handling that logs font loading failures while continuing to function with fallback fonts. This robustness ensures that Mirador remains functional even on systems with limited font support.

Performance Considerations

The current multi-renderer approach has both benefits and drawbacks. On the positive side, it provides clean separation of concerns and allows each renderer to operate independently. However, it also introduces significant overhead:

  • Redundant Font Loading: Each renderer loads the same fonts independently
  • Memory Duplication: Glyph caches and texture atlases are duplicated across renderers
  • Increased Startup Time: Multiple renderer initialization adds to startup time
  • Complex State Management: Coordinating multiple renderers adds complexity

The system includes performance monitoring through the benchmark system, tracking initialization times and rendering performance. This data helps identify areas for improvement, particularly around the multi-renderer overhead.

Responsive Design

All text renderers implement responsive scaling based on window dimensions and device characteristics. Text sizes are calculated relative to a 1080p reference height, with minimum and maximum bounds that ensure readability across different screen sizes. This approach maintains consistent visual hierarchy while preventing text from becoming too small to read or too large for the interface. The responsive system extends beyond simple scaling - text positioning, padding, and layout constraints all adapt to screen size. This ensures Mirador's UI remains usable and visually appealing across different devices and resolutions, from small laptop screens to large desktop monitors. The system also accounts for different pixel densities and DPI settings, providing crisp text rendering regardless of display characteristics.