Text & Fonts
The text stack lives under Runtime/Text/. CSS text properties (the author
surface) are documented on CSS Text; this page describes the
engine.
Two-tier shaping: ATG primary, SDF fallback
Weva shapes text through a two-tier adapter
(AtgPrimaryFallbackAdapter, Runtime/Text/Atg/):
- ATG (Advanced Text Generator) is tried first — Unity's FreeType-based hinted-bitmap glyph rasterizer, the same engine UI Toolkit uses for "Chrome-quality" small UI text. Glyph stems snap to whole pixels because the font's hinting bytecode is applied.
- SDF (signed distance field) is the fallback (
Runtime/Text/Sdf/). It handles everything ATG can't:text-shadowblur, transforms/animation, large text, and font-fallback chains not yet wired ATG-side.
Both backends emit the same SdfGlyphQuad type, so the downstream renderer is
unaware of the source. Shaping is Latin + simple scripts only — no bidi
reordering, no complex-script shaping in v1 (TextShaper,
Runtime/Text/TextCore/).
There is also a TextMeshPro adapter (Runtime/Text/Tmp/) and a low-level
UnityFontEngineBackend over FontEngine.LowLevel; the project's v1 text path
is the FontEngine-backed SDF baker, escaping to TextCore.Text.FontAsset only
when forced.
Font resolution
FontResolver (Runtime/Text/TextCore/FontResolver.cs) maps a CSS
font-family list to a loadable face:
- Each comma-separated family is tried in order.
- Each token matches (case-insensitive) registered families first
(
RegisterFont/@font-face), then the OS-default mapping (sans-serif,monospace,serif,system-ui). - If nothing matches, it falls back to
DefaultFamily(defaultsans-serif).
Custom fonts are registered explicitly (via @font-face or RegisterFont),
which keeps headless tests deterministic.
Default-face policy
The default sans-serif face is Segoe UI, not Arial — a locked product
decision. This intentionally diverges from Chrome on Windows: Segoe's
line-height: normal is ≈1.36× the font size versus Arial's ≈1.15×. The
resulting uniform vertical shifts relative to a Chrome baseline are accepted
divergence, not bugs (see SAMPLE_AUDIT.md's font-drift rows). Only
structural layout differences are treated as defects.
Font-pipeline gotcha
The ATG primary and the SDF/FontEngine fallback can resolve sans-serif to
different faces. When ATG genuinely can't shape a run and it falls to the
SDF path, the SDF fallback historically resolved sans-serif to the bundled
Weva-Default.ttf (a different face than ATG's SegoeUI). This was the cause of
intermittent "font-flap" and baseline bugs (e.g. the /U+00A0
atlas-repack flap, now fixed by pre-rasterizing nbsp into the ATG atlas). If you
see a run render in the "wrong" face, this fallback divergence is the usual
suspect.
Glyph atlases
Glyphs are packed into shelf-packed atlases that grow then LRU-evict
(GlyphAtlasPacker, AtlasRegistry, GlyphAtlas). The ASCII range plus
U+00A0 is prepopulated so common runs shape without an on-demand repack.
Shaped runs are cached in TextRunSnapshotCache (Runtime/Rendering/URP/) for
the session.
Gradient text (background-clip: text)
background-clip: text with a non-opaque color fills the glyphs with the
element's topmost background gradient. DrawTextCommand.TextFillGradient
carries the gradient; SdfTextRendering samples it per glyph (a CSS-Images
linear projection over the run bounds) and composites the text color over it:
color: transparent→ pure gradient fill.- An opaque
colorkeeps the solid fast path (matching Chrome, where an opaque color covers the clipped gradient). - Horizontal-dominant gradients slice each glyph into ~2px UV strips for a smooth within-letter fill; vertical-dominant gradients sample per glyph.
v1 refinements left open: the gradient spans each run's bounds, so a multi-line clipped element restarts the gradient per line; radial/conic fall back to the gradient midpoint color; vertical gradients don't shade within the line height.
Next: Rendering
Weva