Rust native text render cache

Status: accepted. The native render-cache gate composes the already accepted shaping and outline boundaries into KiCad-compatible polygon caches. Work is intentionally staged so a final contour mismatch remains attributable to shaping, outline extraction, curve decomposition, placement, or hole fracture.

Evidence ladder

  1. Accepted fixed shaping records establish glyph IDs, clusters, advances, offsets, and optional flags.
  2. Accepted fixed outline records establish raw line, quadratic, cubic, composite, collection-face, gvar, CFF, and CFF2 commands.
  3. Rack L0_055 establishes the safe-Rust port of KiCad's BEZIER_POLY curve approximation in libs/kimath/src/bezier_curves.cpp before any placement transform. The text integration path is common/font/outline_decomposer.cpp.
  4. Rack L0_056 composes HarfRust shaping, a reusable configured ttf-parser face, the curve kernel, cursor advances, offsets, anisotropic sizing, Y inversion, and caller origin into bounded raw text-run contours. Fixed uharfbuzz and FontTools records keep this placement stage attributable and explicitly unhinted.
  5. Rack L0_057 applies single-line horizontal/vertical alignment, mirroring, and clockwise rotation about the authored text origin. Fixed records invoke the Python renderer's alignment and transform helpers directly.
  6. Rack L0_058 and L0_059 establish bounded cache hole fracture and typed cache S-expression read/write.
  7. Rack L0_060 establishes bounded multiline and tab-delimited block layout while reusing one configured hinted font and shaping face.
  8. Rack L2_009 compares the separate embedded-hinted path against a live KiCad save oracle for representative line-only, curved, holed, multiline, tabbed, and non-default-line-spacing Arial cases.
  9. P5_010 connects that accepted engine to board text, text-box, and table carriers through a caller-supplied bounded font/shaping sidecar, including native outline line breaking and explicit generated-cache provenance. The shared contract distinguishes existing-file, Python-generated, and native-generated cache payloads.

Curve-decomposition boundary

flatten_quadratic_bezier and flatten_cubic_bezier operate in caller-declared finite coordinates and reproduce the current Python/KiCad approximation, including inflection-aware cubic splitting and KiCad's nonpositive-tolerance fallback. The API is independent of font parsing and placement so the same kernel can consume TrueType, CFF, or future close-to-format curves. Source provenance is pinned to KiCad revision 5f555f4d63b970e410d567d1f79e05e8ce41b9d8, matching the Python renderer's documented reference revision.

Retained points and decomposition work have separate inclusive limits. Quadratic segment counts are rejected before iteration when they exceed the work ceiling. Cubic fallback uses an explicit bounded stack rather than recursive call-stack growth. Every append and work item is charged before it is retained or processed; failures publish no output. Direct adversarial quadratic and high-curvature cubic exhaustion tests protect both paths.

Raw contour-placement boundary

FontOutlineFace validates the buffer, face, variation axes, and hash once, then extracts every shaped glyph without reparsing or rehashing per operation. The public wrapper continues to support independent single- glyph calls. shape_text_contours_a0 keeps font bytes out of the metadata request and applies KiCad's truncated 1433 face scaler and 1.4 outline compensation in a documented coordinate space.

Shaping glyph count and bytes, per-face outline commands, aggregate outline commands, curve work, peak temporary curve points, retained contours, and retained points are independently bounded. The temporary ceiling makes the maximum live contour memory the explicit sum of retained-output and one-curve scratch allowances instead of permitting every late curve another implicit full output-sized allocation. Missing outlines such as a space contribute their shaped advance but no contour; a backend failure after any outline callback is invalid font data, never whitespace. Failures return no partial output. This intermediate deliberately uses unhinted OpenType design-unit outlines; final KiCad save-oracle evidence remains responsible for deciding whether FreeType hinting creates material drift that requires a separate native backend.

Single-line layout boundary

layout_single_line_text_a0 measures and renders one shaped run once, applies left/center/right width offsets and top/center/bottom offsets using KiCad's 1.17 height factor, then mirrors on the X axis and rotates clockwise around the authored position. Transform work mutates the already-bounded contour output in place and creates no second point graph. Nonfinite angles, aligned positions, and transformed points fail closed. Horizontal layout supports both left-to-right and right-to-left shaped runs; top-to-bottom and bottom-to-top shaping fail with a structured invalid-input diagnostic until vertical-flow layout exists. Returned advance_x/advance_y values remain the pre-layout local shaped-run metrics after the contour points are positioned, mirrored, and rotated.

KiCad-compatible hinted outline boundary

HintedFontOutlineFace uses the safe-Rust Skrifa 0.46 embedded TrueType hinting engine at KiCad's effective 358.25 pixels-per-em. Skrifa returns pixel coordinates; the adapter applies four internal units per pixel, equivalent to KiCad's FreeType 26.6 coordinates followed by its 72 / 1152 high-resolution scale. The existing unhinted ttf-parser APIs and fixed design-unit records remain unchanged; callers select the hinted cache operation explicitly.

Font bytes, metadata, variations, outline commands, and per-glyph hinting scratch memory have independent fail-closed ceilings. Scratch memory is sized before allocation, with inclusive exact and one-under tests. The initial hinted boundary deliberately rejects nonempty variation coordinates until a variable-font KiCad oracle is promoted. No package-owned unsafe code is used.

The dependency is the Google Fontations Skrifa crate under its MIT/Apache-2.0 terms. The workspace pins 0.46.0 in Cargo.lock, reuses the existing read-fonts version, disables default features, and enables only std; the unused autohint-shaping feature does not enter the graph. KiCad Monkey continues to own all request validation, resource limits, coordinate conversion, topology, and cache I/O.

Multiline and tabbed block boundary

layout_text_block_hinted_a0 mirrors KiCad's FONT::getLinePositions and OUTLINE_FONT::GetTextAsGlyphs behavior: newline-delimited lines use the font-metrics 1.68 interline pitch and the authored line- spacing multiplier; each line receives its own horizontal alignment; vertical alignment uses the complete block height; and tabs advance to the next 4 * 0.6-em stop. Mirroring and clockwise rotation are then applied around the authored block origin.

The block rules are source-pinned to KiCad revision d6ff4c23641ee5236b7c9fac19eb6af1849294f5 in common/font/font.cpp, common/font/outline_font.cpp, and include/font/font_metrics.h.

The block creates one validated HarfRust/Skrifa session and shapes each non-overlapping run once. Font bytes are neither rehashed nor reparsed per run. UTF-8 feature ranges are intersected and rebased in source order for each run. Aggregate feature inspections are charged before each collection scan, and matching feature applications are charged before cloning. Line, run, aggregate cloned metadata, feature inspection/application, glyph, outline-command, curve-work, contour, and point ceilings are all charged before publication; delimiter-heavy input stops at the configured span ceiling without a full delimiter-counting pass. The typed cache wrapper composes those ceilings with the existing text, topology, and cache-output limits.

Live native evidence currently covers left/top, center/center, rotated mirrored, tabbed, explicit-line-spacing, right-aligned multi-glyph, kerning, and whitespace blocks at tolerance 0.002. The hinted path follows KiCad's integer VECTOR2I cursor semantics: each Harf advance is scaled by 72 / 1152 into internal units and truncated toward zero before the next glyph. This closes the prior Arial SS alignment gap without widening the comparison tolerance.

Markup styling boundary

Each line is parsed into KiCad's ^{}, _{}, and ~{} markup tree with the accepted Python _parse_markup semantics: a marker immediately followed by { opens a group, } closes the innermost group, a bare marker or unmatched } is literal, and unterminated groups extend to the end of the line. Every retained text or group node is charged against one block-wide markup-node ceiling before creation, and both the parser and the layout walk use explicit heap stacks so nesting depth is bounded by that same ceiling.

Subscript and superscript runs shape and hint on KiCad's rounded styled face size round(1433 * 0.64) = 917; the hinted session builds the second Skrifa hinting instance at 229.25 ppem only when the block text contains a styled marker. Styled positions convert through the 917 scaler while the millimeter conversion stays on the base 1433 scale, which yields the 0.64 visual ratio. Baseline offsets are -0.25 and +0.45 of the styled scaler; the subscript flag wins for nested markers and offsets never accumulate across nesting. Cursor quantization uses the styled internal-unit scale.

An overbar group appends one stroke polygon after its children: the bar runs from the group's start to its end advance, trimmed by 0.1 * size_x at each end, at 1.23 * size_y above the baseline, stroked with the authored font thickness. Matching CALLBACK_GAL::DrawGlyph(), the endpoints are transformed into final coordinates first and the ERROR_INSIDE TransformOvalToPolygon() port then strokes there with error strokeWidth / 180, giving the constant 24-segment, 28-point oval and preserving the reference point order under mirroring. A non-positive stroke width suppresses the bar; retained bars are charged against the aggregate contour and point ceilings before deferral.

Live native evidence covers Arial subscript, superscript, plain overbar, and rotated mirrored overbar blocks at tolerance 0.002.

Fake bold and italic boundary

When the resolved face lacks a real bold or italic variant, KiCad synthesizes the style in FreeType: fake italic installs a FT_Set_Transform shear of -12 degrees with xx = cos, xy = sin truncated to 16.16 fixed point and identity y, applied after hinting and after shaping, so advances never change; fake bold then runs FT_Outline_Embolden(outline, 1 << 6) on the glyph's 26.6 control points with its return code ignored. The Rust port recovers each hinted outline command onto the exact 26.6 grid (round(value * 16) is exact because hinted values sit on the 1/64-pixel grid), then executes exact 32-bit integer ports of FT_MulFix, FT_MulDiv, FT_Vector_NormLen, FT_Outline_Get_Orientation, and FT_Outline_EmboldenXY from the pinned FreeType 2.13.3 sources, including their deliberate wrap-around arithmetic, with FT_Long fixed at 32 bits to match the Windows ABI KiCad ships. Indeterminate orientation leaves the outline unchanged, mirroring the ignored FreeType error before any point has moved.

Two representational differences from FreeType's raw point array are deliberate and bounded: the pen stream materializes implied on-curve midpoints, which are collinear with their neighbouring controls and receive the same lateral shift up to fixed-point normalization rounding, and the pen's closing return-to-start point is folded back onto the contour's first point before the integer pass so cyclic anchor walking sees FreeType's point count. Which style is faked is resolved by the caller against the actual face; the Rust request takes explicit fake_bold and fake_italic flags, and the unhinted design-unit path rejects them because the synthesis is defined on hinted 26.6 coordinates. The collected point pass is bounded by the existing outline-command ceiling, and out-of-range 26.6 coordinates fail closed.

Live native evidence covers Impact fake bold, Impact fake italic, and Arial bold-italic resolving to the real bold face with only the italic shear faked, all at tolerance 0.002.

Face resolution itself stays caller-side: the Rust request receives font bytes, and the caller resolves board faces through KiCad's own bundled fontconfig (the family:Bold/:Italic FcNameParse match of FONTCONFIG::FindFont in common/font/fontconfig.cpp) before any fallback, so a face not installed on the machine substitutes to the same installed file the KiCad oracle picks. Live Ubuntu Sans regular and bold substitution save-oracle cases pin this.

Cache contour topology

fracture_text_contours_a0 trims one repeated closing point, ignores contours with fewer than three retained points, assigns later contours to the first containing exterior, and bridges holes using the deterministic leftmost-edge ordering of the Python KiCad reference. Holes bridge in ascending (min x, min y) order, matching KiCad's fractureSingleCacheFriendly path sort. Each hole bridges at the first strict-min-x point scanned cyclically from the hole's Simplify seam in stored traversal order: KiCad scans stored order for the first point at the strict minimum x with no tie-break, and Fracture()'s Clipper2 Simplify() emits every ring starting just past its min-y run exit, so a vertical hole left edge bridges at its bottom vertex (Arial D) while a hole whose seam reaches the upper min-x vertex first bridges there (embedded Wavenumber W). Live double-hole (Arial B), vertical-left-edge-hole (Arial D), and corpus complex_font save-oracle cases pin this. One topology call covers exactly one glyph's polygon set: KiCad's saved-cache writer fractures each drawn glyph independently and only when that glyph has holes (if (glyph.HasHoles()) glyph.Fracture()), so cache composition threads per-glyph contour group sizes from layout and scopes fracture to each group. Within a holed group, Simplify() first emits every ring in a fixed orientation (exteriors positive shoelace area in the cache frame, holes negative) regardless of input winding — mirrored text flips every ring's winding, so holed groups orientation-normalize before the seam/bridge rules, while hole-free groups keep their stored winding; live mirrored Arial D/R8 save-oracle cases and mirrored corpus footprint texts pin this. Simplify() also rewrites every ring seam: rings restart one past the exit of the min-y run containing the smallest-x min-y point, where a run is a maximal stretch of consecutive stored points at the ring's minimum y and, with several min-y runs, the smallest-x run wins, matching Clipper2's local-minima sweep order — and Clipper2's LocMinSorter reorders the group's outers by bottom-most vertex (largest y first, ties smallest x, otherwise stable). Hole-free groups see no Simplify() and keep font contour order and ring seams untouched. Live Arial % (one glyph mixing a hole-free slash outer with holed rings), Tp%, and i:j;!? save-oracle cases pin this scoping. The fractured ring is assembled through KiCad's SHAPE_LINE_CHAIN::Append, which drops points equal to the last one: when a bridge split point coincides exactly with the hole's bridge vertex (as happens at small glyph sizes where distinct outline points collapse after scaling), the pair collapses to a single published point; a coincident-bridge unit case and live small bold Arial C203/R80 save-oracle cases pin this. The pairwise containment and prior-edge bridge scans have independent work ceilings; input points, edge slots, link traversal, polygons, and published points are also bounded. Finite comparisons intentionally preserve Python's signed-zero equality and stable tie ordering. Overflowed intersection or distance intermediates are invalid input. The operation returns no partial topology on any invalid input or resource-limit failure.

Typed cache and KiCad S-expression I/O

generate_text_render_cache_a0 composes the accepted horizontal layout and topology stages into a typed cache containing resolved text, authored angle, polygons, contours, and points. The cache is a KiCad source-model type, not a public interchange DTO, so it does not create a new TypeSpec or WASM contract. read_text_render_cache_a0 consumes the lexical token stream directly and never builds a generic S-expression tree. write_text_render_cache_a0 emits escaped text and nested polygon/points forms directly into a byte-bounded buffer. Read/write limits separately cover source, decoded text, polygons, contours, points, and output bytes, and semantic read-write-read is stable.

The shape of the writer is pinned to KiCad revision d6ff4c23641ee5236b7c9fac19eb6af1849294f5, function PCB_IO_KICAD_SEXPR::formatRenderCache in pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.cpp. Fixed byte formatting evidence comes from Python RenderCache.to_sexp plus build_sexp; final coordinate authority remains the KiCad save oracle.

Still open

The current render-cache strata now establish live KiCad save-oracle parity for representative plain single-line, multiline, tabbed, and explicit line-spacing, right-aligned, kerning, whitespace, and markup Arial text, plus fake bold/italic synthesis on Impact and the real-bold Arial face, double-hole and vertical-left-edge hole fracture ordering, and per-glyph fracture scoping with Simplify seam rewrite and outer reordering (the former Arial % gap, now closed — it was per-glyph outer output ordering plus hole-free ring seam rewriting, not geometry cleanup). The Phase 5 exit gate adds a focused transient authored Arial board and live save-oracle case. The accepted boundary does not claim exhaustive font, composite-glyph, or every-corpus-object coverage. One known limitation sits in the Python reference (not the Rust port): without a provisioned KICAD_HARFBUZZ_DLL its pure-uharfbuzz fallback shaping drifts a few microns per glyph at larger x positions; with the hb_ft path it matches the oracle, and the native hinted path always quantizes advances like KiCad. Those remain active parts of render-cache-parity, not deferred packaging work. Font discovery stays native-only and outside the platform-neutral byte-input core; WASM packaging remains in its later selected-operation phase.