Skip to content

RUNE-306: Text Proposed

Updated2026-08-23

Summary

Rune exposes text shaping as a shared-resource API. rune_shape_text() returns an immutable ShapedText handle whose public query surface currently includes glyph count, total advance, per-glyph advances, ascent/descent metrics, UTF-16 text length, and logical-text-to-glyph cluster_map[]. Glyph IDs, glyph offsets, fallback font runs, caret-stop tables, and hit-test policy remain internal or framework-defined rather than part of the audited public API.

Motivation

Rune needs complex text support for mixed-direction text, ligatures, emoji, and fallback fonts without reintroducing a paragraph object that owns layout policy. The design keeps shaping in native code, keeps line layout in managed code, and preserves the snapshot command model where the renderer replays immutable resources rather than making layout decisions at draw time.

Design

Shaping API and Resource Model

c
RuneShapedText* rune_shape_text(RuneFont* font, float size, const void* text, uint32_t text_length);
uint32_t rune_shaped_text_glyph_count(RuneShapedText* shaped);
float rune_shaped_text_total_advance(RuneShapedText* shaped);
const float* rune_shaped_text_advances(RuneShapedText* shaped);
const uint32_t* rune_shaped_text_cluster_map(RuneShapedText* shaped);
uint32_t rune_shaped_text_text_length(RuneShapedText* shaped);
void rune_shaped_text_metrics(RuneShapedText* shaped, float* ascent, float* descent);
void rune_release(void* handle);

ShapedText is a shared resource like Font, Image, and NineSlice: refcounted, immutable after creation, queried from C#, and released with rune_release() when the caller is done.

Internal Shaping Pipeline

rune_shape_text() currently performs the following native steps:

  1. initialize DirectWrite analysis source/sink over the caller text
  2. run BiDi analysis and script analysis
  3. map actual fallback fonts across the logical text range
  4. intersect font, script, and BiDi runs
  5. reorder those runs into visual order
  6. shape each run into internal glyph IDs, glyph offsets, advances, fallback font-run metadata, and the public cluster_map[]

Glyph output is stored in visual order. cluster_map[] preserves the logical UTF-16 index to visual glyph-start mapping used by managed layout.

Managed Layout and Recording

The shipped Story text sprite performs line layout in C#:

  • one paragraph is shaped at a time
  • prefix sums are built from glyph advances plus optional letter spacing
  • whitespace in the source string is mapped through cluster_map[] to mark break opportunities
  • when no break opportunity fits, the current word is force-broken so the line cannot overflow forever
  • each visual line is recorded as one CmdDrawShapedText glyph range with a baseline y

letterSpacing is post-shaping: layout and replay both apply the same extra gap between adjacent glyphs without mutating the shaped resource.

Cache Status

Rune exposes rune_set_shape_cache_limit(uint32_t bytes) / RuneEngine.SetShapeCacheLimit(uint bytes) as a process-wide budget knob, but the audited Direct2D shaping path does not currently populate a reusable shaped-segment cache. Each rune_shape_text() call shapes fresh input into a fresh ShapedText handle.

Interaction Surface

The current public text surface stops at advances, metrics, and cluster_map[]. There is no exported caret-stop array, glyph ID array, glyph-offset array, or standardized engine-level hit-test API in the audited implementation. Higher-level caret, selection, and hit-test policy remains framework/application responsibility.

Examples

Shape Once, Record Many

A label shapes one paragraph once, then records multiple CmdDrawShapedText glyph ranges from the same handle as wrapping changes.

Mixed Fallback Text

Shaping "Hello 你好 😀" starts from one base font handle, internally maps Latin, CJK, and emoji spans to different actual fonts, and still returns one public ShapedText resource.

Performance

  • expensive script, bidi, and fallback analysis stays in the initial rune_shape_text() call
  • one CmdDrawShapedText per visual line keeps replay cheaper than per-word draw commands
  • the current docs do not promise shaped-segment cache reuse because that cache is not implemented in the audited D2D path

Alternatives Considered

ApproachRejected because
native paragraph handlewould mix shaping, layout, trimming, and interaction into one render-owned special case
documenting public caret-stop and glyph-offset APIsthose exports do not exist in the audited implementation
claiming word-level shaping-cache reusethe current D2D text path does not implement that cache

Dependencies

  • RUNE-304 defines CmdDrawShapedText, the command recorded for each visual line.
  • RUNE-308 defines ShapedText as a shared resource with explicit lifetime via rune_release().

Test Strategy

  1. verify LTR text returns stable advances, metrics, and text length
  2. verify RTL and mixed BiDi text produce visual-order glyph storage with a correct cluster_map[]
  3. verify fallback shaping of mixed-script text uses the right actual fonts while remaining transparent to C# callers
  4. verify Story wrapping and line recording use advances plus cluster_map[] without native paragraph layout
  5. verify the public text surface does not claim caret-stop, glyph-ID, glyph-offset, or hit-test exports that are absent from the audited ABI

Rune Project brings Rune Story authoring together with the Rune Engine rendering foundation.