Text shaping and glyph rendering
Follow font fallback, shaping, bidirectional line layout, caret geometry, subpixel rasterization, and glyph-atlas reuse.
Asterune separates text into logical content, positioned glyphs, and rasterized coverage. The game core measures and positions text without a graphics device. The client turns those positioned glyphs into atlas-backed quads. Layout, editing, and rendering use the same text metrics so their wrapping and caret geometry follow the displayed text.
A character, a grapheme, and a glyph are different units. A grapheme can contain several Unicode code points, and shaping can combine or reposition characters. The engine retains source byte offsets and grapheme indices alongside glyph positions instead of assuming one byte, one character, and one rendered shape have the same index.
Direct layout and shaped paragraphs
Text that uses the primary face without bidirectional behavior follows a direct layout path. It reads glyph advances and pair kerning, applies letter spacing, and handles wrapping and tab stops. Ordinary Latin, Greek, and Cyrillic text does not pass through the paragraph shaper solely because it is non-ASCII.
The shaped path activates when text needs the supported fallback-script handling or bidirectional ordering. Right-to-left characters and explicit direction controls trigger that path. A supported fallback-script character also triggers it when the selected primary face lacks that character. Selected symbol ranges can request fallback for the same reason.
The shaper splits explicit paragraphs at newlines and identifies their Unicode bidirectional levels. It divides logical text into grapheme clusters with source byte ranges, selected fonts, direction levels, and rich-text run identities. Adjacent compatible clusters form font-and-direction runs; tabs form separate layout boundaries.
HarfRust shapes each run into glyph IDs, advances, and X and Y offsets. The buffer receives direction and neighboring context, which lets contextual joining and mark positioning use more than an isolated character. Shaping plans are cached by font, direction, and script. Font-unit positions are scaled to the resolved pixel size before line placement.
The engine keeps the mapping back to the source clusters. Visual reordering can then move glyphs without discarding their logical text positions. Rich-text color and decoration remain associated with their originating runs rather than being inferred from the final left-to-right drawing position.
Font fallback and regional forms
Font selection first checks whether the primary face covers the character. Fallback fills missing coverage rather than replacing every character from a particular script. The bundled fallback families cover Arabic, Hebrew, Japanese, Korean, Simplified Chinese, and Traditional Chinese, with weights chosen from the available faces.
A paragraph containing kana selects Japanese ideograph forms. Otherwise, hangul selects Korean forms. A paragraph without either uses the configured Han-region preference. The choice applies to the paragraph, avoiding a per-character mixture caused solely by the global preference.
Spaces, digits, and punctuation can stay in a neighboring fallback font when that font covers them. This avoids splitting a joined run at every space or punctuation character. Symbol fallback also tries the bundled East Asian fonts when the primary face lacks a covered symbol. Bundled font coverage does not include an emoji font.
Fallback font files are embedded in compressed form and decompressed on first use. Parsed font and shaping data can then be reused. A fallback-generation counter invalidates text caches when the regional choice changes; retaining old positions after selecting different glyph forms could make measurement and rendering disagree.
Line breaking and visual order
Shaped paragraphs collect Unicode line-break opportunities before wrapping. When the accumulated advance exceeds the available width, the layout uses the latest valid opportunity. If none exists after the line start, it breaks at the current grapheme boundary. This handles ideographic breaks as well as space-separated words without splitting UTF-8 sequences.
Wrapping is computed in logical cluster order. Each resulting line then resets trailing whitespace direction to the paragraph level and applies visual bidirectional reordering. The reordering reverses runs from the highest level down to the lowest odd level. Right-to-left glyphs consequently appear in visual order while their source indices remain logical indices.
Wrapped-line placement removes trailing whitespace from the visible line extent. Tabs advance to stops based on 4 space advances, relative to the current pen position. Letter spacing participates in both measurement and placement, including the width recalculated after a wrap.
The resulting layout contains positioned glyphs, line widths, block bounds, baseline ascent, and line step. Font size rounds to a whole pixel and clamps to 1 through 512 pixels. Line step rounds the font-size and line-height calculation, with a minimum of 1 pixel. These resolved metrics feed both automatic interface size and final text alignment.
Carets, selections, and composition
Editing stores caret and selection positions as UTF-8 byte offsets. Normal left, right, backspace, and delete operations move across grapheme boundaries. This preserves combining sequences during ordinary single-step editing rather than treating each byte as an independent character.
Caret placement uses the displayed line's cluster positions. A logical start and end have opposite visual edges in a right-to-left cluster. Pointer-to-index lookup compares the pointer with those edges and selects the nearest caret position. It does not estimate a text index by dividing the line width by the number of characters.
A logical selection can produce several visual spans when it crosses mixed directions. The layout collects the selected clusters in visual order and merges adjacent spans. The renderer draws each span rather than filling one large rectangle between the logical endpoints, which could cover unrelated text in the middle of a bidirectional line.
The client text-geometry layer also accounts for alignment, wrapping, and horizontal scrolling when mapping between a byte index and a screen position. Vertical caret movement can use that geometry to preserve the visual column across wrapped lines. Focused editable text uses its plain content during editing so markup is not interpreted as hidden control syntax.
Input method editor (IME) preedit belongs to the interface host's transient composition state. Its insertion and commit behavior is described in Interface layout and input routing. The committed string and transient composition do not share one mutation lifetime.
Raster coverage and subpixel phases
The GUI renderer rasterizes font outlines on the CPU with Swash and uploads
alpha coverage to a 2,048 × 2,048 R8Unorm atlas. A glyph key includes font,
glyph ID, resolved pixel size, and horizontal subpixel phase. The renderer
uses hinted outline rasterization rather than evaluating font curves in the
GUI fragment shader.
The number of phase variants decreases with size:
| Pixel size | Horizontal phases | Position increment |
|---|---|---|
| 1 through 24 | 4 | 0.25 pixels |
| 25 through 64 | 2 | 0.5 pixels |
| Above 64 | 1 | 1 pixel |
A glyph's X position rounds to the nearest phase. The integer part places its quad; the phase supplies the fractional offset used during outline rasterization. This retains finer placement for small text without caching an unbounded variant for every possible floating-point coordinate.
Raster output includes its bearing and pixel dimensions, so the quad's position accounts for the distance from the baseline to the actual ink. A glyph with no rasterized coverage retains an empty cached entry rather than repeatedly attempting to draw a zero-sized image.
Atlas allocations use shelf packing with a 1-pixel transparent border around each image. The border prevents linear filtering from sampling adjacent glyph coverage. The fragment shader multiplies coverage by the run's color and opacity. For small, light-colored text, it also applies a coverage adjustment that fades between 14 and 32 pixels, reducing the polarity-dependent thinning of bright text.
Bundled vector icons can share this alpha atlas, but enter through a separate SVG rasterization path keyed by icon and requested dimensions. Their source being vector data does not change GUI glyphs into vector meshes or make every draw evaluate the SVG again.
Atlas pressure and cached quad validity
A cache hit updates a glyph's usage counter. Before expanding changing runs, the renderer also touches every glyph referenced by reusable visible runs. A static label later in painter order therefore remains recent even if an earlier changing label inserts new glyphs first.
When shelf allocation fails, the atlas retains the most recently used half of the glyph entries, clears its packing state, and rasterizes those retained entries into a compacted layout. Vector-icon cache entries are cleared during this rebuild. If the requested glyph still cannot fit, the renderer can reset the cache and attempt allocation again.
Repacking changes UV coordinates. The atlas increments a generation counter,
and cached text quads tied to the previous generation become invalid. Expansion
retries against the resulting generation rather than drawing old UVs over
newly packed images. After 3 expansion attempts without a stable generation,
it reports visible GUI text exceeds the glyph atlas capacity.
The retry limit is a visible-working-set condition, not a fixed maximum number of characters. Large sizes, multiple fonts, and different phase variants consume different amounts of atlas space. A long string that reuses a few glyphs has different storage needs from a smaller string containing many unique shapes.
Measurement and render caches
Line-range caching uses the source text, face selection, font size, tracking, line height, and wrap width. Rich-text entries include the style changes that can affect face metrics. Asset-library and fallback-generation changes clear the cache so a font arriving later cannot retain measurements from an earlier face. This per-thread cache clears after its stored entry count exceeds 4,096.
The renderer separately caches positioned text layouts and expanded text quads. A layout key includes the text, resolved style, wrapping width, and face/run information. Paint-only changes can reuse positioned glyphs even when their quads need a different color. Quad reuse additionally requires the complete text-run state and a matching atlas generation.
Prepared GUI data has another cache boundary. Viewport changes, image slots, scene-image locations, or fallback changes can require preparation even when one label's shaped positions remain valid. These layers prevent a single "text changed" flag from forcing every measurement, rasterization, and GPU upload to repeat.
The interface guide covers text properties and editable controls. Interface layout explains how text bounds participate in automatic sizing and how the resulting quads enter the ordered GUI draw stream.