Dynamic Text

Forma exposes the same text contracts in its MonoGame and FNA variants. Runtime-loaded text uses UIFontFace, DynamicUIFont, TextLayoutEngine, and device-scoped glyph atlases. Existing SpriteFont applications remain supported through SpriteFontAdapter.

Installing the runtime-matched dynamic package gives each new UIContext a packaged Inter DynamicUIFont at 16 logical pixels. Set FormaDynamicTextDefaultEnabled=false in the application project to disable this initializer. Core-only and opted-out applications continue to resolve only explicitly assigned fonts. Applications can replace the default through UIContext.Theme.FontFamily or call DynamicTextDefaults.Install before constructing contexts.

Packages and Deployment

Use the package matching the application's framework:

Runtime Native-free compatibility Dynamic text
MonoGame Forma.MonoGame Forma.DynamicText.MonoGame
FNA Forma.FNA Forma.DynamicText.FNA

The core package is the native-free compatibility profile for restricted platforms and authorized console ports. It does not include FreeType, HarfBuzz, or their native libraries. Trim-only and NativeAOT package consumers are validated for both peers on macOS arm64, including native-free SpriteFont and optional dynamic-text graphical profiles. Run bash scripts/test-nativeaot-package-consumer.sh to reproduce the packed-artifact gate. Other RIDs remain unsupported until equivalent executable gates pass. Actual console support remains conditional on the selected MonoGame/FNA platform port, platform-holder approval, and validation on authorized hardware.

The optional dynamic package resolves FreeType and HarfBuzz native assets for its declared runtime identifiers. Publish and test every target RID in a clean environment; do not rely on system-installed libraries. Ship font licenses and the repository third-party notices with redistributed fonts. Forma selects the permissive FreeType License and HarfBuzz's MIT license. Binary redistribution requires retaining their acknowledgments and notices, which make compliance enforces; neither selected license requires a source offer. Modified or source redistribution must be reviewed against the corresponding upstream terms rather than inferred from this binary-package conclusion.

Internal Backend Boundary

UIFontFace, DynamicUIFont, and TextLayoutEngine do not expose FreeTypeSharp, HarfBuzzSharp, native handles, or platform font types. UIFontFace delegates face metadata, character/glyph lookup, metrics, variations, shaping, rasterization, diagnostics, and disposal to an internal backend contract. The normal Forma.DynamicText.<Runtime> build selects FreeTypeHarfBuzz and preserves the existing desktop package behavior.

Authorized source builds may set FormaDynamicTextBackend=External and provide FormaDynamicTextBackendSource pointing to a source file compiled into Forma.DynamicText. That file defines the internal ExternalDynamicTextBackend implementation. Selection is compile-time: there is no assembly scanning, reflection activation, runtime generic construction, or public backend API. The source remains in authorized infrastructure when it contains platform SDK details.

Run make static-font-backend to publish and execute the NDA-neutral platform-adapter spike for both runtime peers. The gate verifies the unchanged public face API and rejects FreeType/HarfBuzz managed dependencies and sidecar libraries from the spike output. This proves the replacement boundary and packaging shape; target-specific font quality, lifecycle, and policy remain platform validation work.

DynamicTextNativeDiagnostics.Current reports the target RID, logical native library names, and NuGet packaging sources without scanning loaded modules or exposing handles. Forma owns one direct entry point, FT_Set_Var_Design_Coordinates, against FreeTypeSharp's freetype library name. All other FreeType calls use FreeTypeSharp; shaping uses HarfBuzzSharp and its libHarfBuzzSharp native assets. Forma owns pinned-memory, FreeType-library, and FreeType-face safe handles. HarfBuzzSharp owns its blob, face, font, and buffer handles. This path registers no unmanaged callbacks and uses no runtime-generated marshalling.

Missing, incompatible, or rejected native font libraries fail face creation with FontLoadException and FontLoadErrorCode.NativeFailure. The public message is bounded and stable; loader details remain available through InnerException for host diagnostics. Run make native-font-failures to exercise missing files, invalid binaries, and valid libraries with missing FreeType exports in fresh processes for both peers. Packed dynamic consumers also require exactly one loaded FreeType and HarfBuzz module from their publish directory.

MGCB/XNB is not required for dynamic text. MonoGame MGCB SpriteFonts and FNA-compatible XNB SpriteFonts are optional offline compatibility routes.

Release Budgets

The dual-runtime render smoke enforces these deliberately conservative ceilings on supported graphical CI hosts. The August 2026 Apple M4 Max baseline measured MonoGame/FNA respectively at 1.9/9.7 ms cold face load, 0.19/0.24 ms first shape, 0.24/0.27 ms first raster plus upload, 1.0/1.1 ms per 1,000 warm layout lookups, 6.4/2.5 ms per 100 warm draws, 4.6/5.5 ms fallback-heavy layout, and 2.5/3.3 ms atlas churn.

Operation Release ceiling
Cold face load 1,000 ms
First shape 500 ms
First glyph raster and upload 500 ms
1,000 warm layout lookups 500 ms
Warm layout cache hit rate at least 99%
100 unchanged warm draws 1,000 ms and zero managed allocation
Fallback-heavy layout 500 ms
One-page atlas churn 2,000 ms
Core managed assembly 2 MiB
Dynamic-text managed assembly 256 KiB

The retained layout cache is bounded at 512 entries. Device-scoped Alpha8 atlases are bounded at eight 2048x2048 pages and 32 MiB. Budget failures block release; measurements are performance gates, not cross-machine throughput promises.

Loading and Ownership

using var latinFace = UIFontFace.FromProjectFile(projectDirectory, "Fonts/Inter-Regular.ttf");
using var arabicFace = UIFontFace.FromStream(File.OpenRead("Fonts/NotoSansArabic.ttf"));
var font = new DynamicUIFont(latinFace, 18, UIFontHinting.Default, arabicFace);

var label = new Label
{
    Text = "Forma مرحبا",
    UIFont = font,
    Language = "ar",
};

Faces can also be loaded from ReadOnlyMemory<byte>. Forma copies and pins bounded source bytes for the native face lifetime. The application owns faces and must keep them alive while fonts or layouts can use them, then dispose them idempotently. Controls, DynamicUIFont, and immutable TextLayout instances do not own faces.

Fallback order is deterministic and resolved per grapheme cluster. Put the normal UI face first, then script and emoji faces. Unsupported input reaches glyph 0 (.notdef) after the chain is exhausted; it is not replaced with ? and does not throw.

Use UIFontHinting.Light for small grayscale UI text that needs vertical pixel alignment while preserving fractional horizontal advances and inter-glyph spacing. Dynamic glyphs rasterize at a minimum 2x density and downsample linearly on 1x displays, avoiding low-resolution spacing artifacts without changing HarfBuzz layout metrics.

Layout and Display Density

Font sizes and all TextLayout geometry use logical UI units. UIContext.DisplayScale controls the physical glyph raster size. Moving from 1x to 2x rerasterizes or reuses density-specific cache entries without changing line breaks, caret positions, or logical bounds.

var options = new TextLayoutOptions(
    maxWidth: 320,
    wrapping: TextWrapping.Word,
    direction: TextDirection.Auto,
    locale: "he");
var layout = ui.TextLayoutEngine.Layout(font, text, options);
var caret = layout.GetCaretPosition(utf16Index);
var hit = layout.HitTest(pointerInLayout);
var selection = layout.GetSelectionRectangles(startUtf16, lengthUtf16);

UTF-16 offsets, grapheme clusters, visual clusters, and glyph IDs are distinct. Use layout movement, hit-testing, word-boundary, and selection APIs instead of incrementing code units or measuring substrings.

OpenType and Variable Fonts

TextLayoutOptions.OpenTypeFeatures accepts immutable four-character OpenType tags. Label forwards features with SetOpenTypeFeatures. Variable coordinates belong to DynamicUIFont identity:

var variable = new DynamicUIFont(
    face,
    24,
    UIFontHinting.Default,
    new[] { new UIFontVariationCoordinate("wght", 650) });
label.UIFont = variable;
label.SetOpenTypeFeatures(new[]
{
    new UIFontOpenTypeFeature("liga", 1),
    new UIFontOpenTypeFeature("kern", 1),
});

Cache Budget and Recovery

Each UIContext owns a glyph cache per GraphicsDevice. The default hard limits are eight 2048x2048 Alpha8 pages and 32 MB. UIContext.DynamicGlyphDiagnostics reports pages, glyphs, occupancy, hits, misses, uploads, evictions, failures, and bytes. Immutable page snapshots are available through GetDynamicGlyphAtlasPages; ClearDynamicGlyphCache clears pages between draws. Device reset recreates textures from retained grayscale pages. Active-frame budget exhaustion skips the unavailable glyph, records a diagnostic, and retries normally on later frames instead of allocating beyond the budget or terminating the process.

SpriteFont Compatibility

Keep SpriteFont when the application needs a fixed glyph set, pixel-art sampling, a deterministic offline atlas, minimal native dependencies, or legacy XNA-compatible deployment. Assigning a control's existing Font property installs a cached SpriteFontAdapter; no source rewrite is required.

Migrate without changing layout intent:

// Before: offline SpriteFont.
var column = new VBoxContainer { Separation = 8 };
column.AddChild(new Label { Font = content.Load<SpriteFont>("UI"), Text = "Settings" });
column.AddChild(new Button { Font = content.Load<SpriteFont>("UI"), Text = "Apply" });

// After: retain the same controls and layout properties; change only font selection.
using var face = UIFontFace.FromProjectFile(projectDirectory, "Fonts/Inter-Regular.ttf");
var uiFont = new DynamicUIFont(face, 16);
column.Children.OfType<Label>().Single().UIFont = uiFont;
column.Children.OfType<Button>().Single().UIFont = uiFont;

During rollout, keep the original SpriteFont loaded and expose an application switch:

void SelectCompatibility(bool compatibility)
{
    label.UIFont = compatibility ? new SpriteFontAdapter(spriteFont, 16) : dynamicFont;
}

The catalog header demonstrates this rollback path in both runtime hosts.

Compatibility Policy

Forma 0.x preserves the parallel Font and UIFont properties during migration. Additive text contracts are minor-version changes. Existing Font members will not be silently reinterpreted or removed; any future obsoletion forwards through SpriteFontAdapter for at least one minor release, and removal requires a documented major-version decision. Assigning both properties remains last-assignment-wins.

Catalog Stories

The exact typography story names are Dynamic Sizes, Letter Spacing, Display Density, Fallback Chain, Shaping and Features, Bidirectional Text, Wrapping and Selection, SpriteFont Compatibility, Atlas Inspector, and Failure States. Their stable identifiers are the kebab-case names prefixed with catalog-, such as catalog-fallback-chain.