Skip to content

TrueType bytecode hinting: grid-fit small text, bit-exact vs FreeType - #2

Merged
ynniv merged 36 commits into
masterfrom
hinting
Jul 3, 2026
Merged

TrueType bytecode hinting: grid-fit small text, bit-exact vs FreeType#2
ynniv merged 36 commits into
masterfrom
hinting

Conversation

@ynniv

@ynniv ynniv commented Jul 3, 2026

Copy link
Copy Markdown
Member

Adds a from-scratch TrueType bytecode interpreter so scribe can grid-fit glyph
outlines before rasterizing. At small ppem (~7–12px) unhinted outlines land on
fractional pixel rows and stems smear; hinting snaps stems/baseline/x-height to the
grid so small body text renders crisp.

What's here

  • src/hint.lisp — the interpreter: F26Dot6/F2Dot14 fixed-point, graphics
    state, stack, storage, scaled CVT, the rounding engine (RTG/RTHG/RTDG/RUTG/ROFF/
    SROUND…), ~110 opcodes (push/arith/logic, GS setters, CVT r/w, flow with
    FDEF/CALL/LOOPCALL, DELTAP/DELTAC, and the point movers MDAP/MIAP/MDRP/MIRP/IP/
    IUP/SHP/SHPIX/ALIGNRP/ISECT/…). Phantom points, twilight + glyph zones, composites
    (component placement + ROUND_XY_TO_GRID + the composite's own instructions).
  • Full and light modes*hint-light* grid-fits vertically but keeps the
    original (fractional) X, so horizontal stem rhythm stays smooth (like the browser's
    default Linux rendering); full mode grid-fits both axes (crisper, choppier).
  • rasterize-glyph integration*hinting* (T, nil, or a max-ppem); hinted
    outlines rasterize at scale 1, sub-pixel positioned, memoized per (font, gid, ppem,
    mode) over a per-(font, ppem) hinter cache. Default off; the geometric path is
    unchanged.
  • otl.lisp — cache GSUB/GPOS lookup indices on the font (new %otl slot), so
    shaping a page parses the feature/lookup tables once, not once per word.
  • blend.lisp — optional gamma-space glyph compositing (*blend-gamma*).

Validation

  • Differentially verified against FreeType (classic interpreter v35) at the point
    level
    : all 94 printable ASCII glyphs hint bit-exact on both LiberationSans and
    DejaVuSans across ppem 7–16. Harness + FreeType oracle under inspect/
    (hint-oracle.py, hint-diff.py, hint-weftdump.lisp); plan + notes in
    docs/HINTING.md.
  • scribe's own suite stays green (inspect/run-all.sh, 10/10).
  • Accented Latin-1 composites bit-exact on LiberationSans; a few DejaVu tilde/ring
    diacritics remain ~1/64px off (documented).

Notes

Grayscale smooth-mode target (not ClearType). Interpreter-only where possible;
rasterizer/shaping/compositing changes are additive and behind flags.

ynniv and others added 30 commits July 2, 2026 11:09
blend-coverage composites in linear light — correct for images/gradients, but it
leaves dark-on-light TEXT edges washed-out (~188 vs 127 grey at half coverage),
much lighter than a browser. Browsers blend glyphs in an intermediate gamma
(between linear ~2.2 and naive sRGB 1.0). Add *blend-gamma*: NIL keeps the exact
sRGB-LUT linear path (default — image/gradient rendering byte-unchanged); a number
G composites in power-G space so a text consumer can match browser weight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Start of TrueType bytecode hinting (docs/HINTING.md): interpret the font's own
instructions to grid-fit small text, validated at the point level against
FreeType. inspect/hint-oracle.py dumps FreeType's hinted outline (native bytecode
hinter, classic interpreter v35, F26Dot6 points per contour) as ground truth.
Verified: LiberationSans 'l'@12px hints to a ~1px stem, y 0.00->9.00.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
The TrueType bytecode VM: F26.6/F2.14 fixed point, the rounding engine, graphics
state, stack/storage/CVT, ~85 opcodes (push, arith/logic, flow with IF/ELSE/EIF
+ JMPR/JROT/JROF, FDEF/CALL/LOOPCALL, storage, CVT r/w, GETINFO, ROUND family,
DELTAC/DELTAP, all the GS + rounding-state setters). make-hinter sizes stack/
storage from maxp and scales the CVT to the target ppem.

Milestone: runs LiberationSans-Regular's full fpgm (71 functions) AND prep at
12px with a balanced stack and no unimplemented opcode — the fixed-point core is
proven on real font programs. Still WIP: the glyph pipeline (zones, phantom
points) and the point-mover opcodes (MDAP/MIAP/MDRP/MIRP/IP/IUP/…) needed to hint
an actual glyph and diff it against the FreeType point-oracle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…WIP)

Full end-to-end hinting: load-glyph-zone scales a glyph's points into zone 1,
appends the 4 phantom points (origin/advance/top from hmtx+bbox), sets up the
twilight zone; hint-glyph resets per-glyph graphics state, runs the glyph
program, and extracts the moved points.  Point-movers implemented: MDAP, MIAP,
MDRP/MIRP (all 32 flag variants each), IP, IUP[x/y], SHP, ALIGNRP, GC, MD, SCFS,
CINDEX/MINDEX, the SPVFS/SFVFS/GPV/GFV vector ops, and hmtx-metrics.

Milestone: the interpreter runs LiberationSans 'l' at 12px end-to-end and
produces a recognizable grid-fit stem — x within ~0.1px of FreeType, stem HEIGHT
exact (y 0->9).  Remaining to bit-exact: the IUP interpolation (one baseline
point lands wrong) and the sub-pixel cut-in/rounding details.  This proves the
approach: a from-scratch fixed-point TT interpreter hints real glyphs; the rest
is oracle-driven debugging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Driving M1 to bit-exact.  Three bugs found via point-level diff against the
FreeType oracle:
  1. muldiv (FT_MulDiv) ignored the divisor's sign — result sign is sign(a*b*c),
     not sign(a*b).  Corrupted every projected interpolation (IUP put a baseline
     point at y=17).
  2. hround applied the "don't collapse to zero" rule to a ZERO input, so
     round(0) returned 1px — the baseline landed at y=1.  That rule is
     min-distance (MDRP/MIRP), not the round function.
  3. Phantom points weren't grid-fit: the advance point stayed at 2.67px, so IP
     couldn't shift a stem when the advance rounds to 3.00.  FT_PIX_ROUND the
     phantoms' CURRENT position (ORG stays unrounded for correct measurement).

Result: LiberationSans 'l' @12px hints BIT-EXACT vs FreeType —
(0.91,0)(0.91,9)(1.95,9)(1.95,0), matching the oracle to the F26.6 unit.  This
proves the whole pipeline: fixed-point VM + phantom points + MIAP/IP/SHP/IUP.
Other glyphs are within ~1px; the residual is cap/x-height blue-zone snapping in
the MDAP path (FT rounds an 8.25px cap up to 9 via the font's alignment zones),
the next debug target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…n item)

Bank the checkpoint: P0-P3 done, M1 proof achieved (l bit-exact vs FreeType),
remaining glyphs ~1px off pending cap/x-height blue-zone snapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
M1 closed on the proof: LiberationSans 'l' hints bit-exact vs FreeType. M2's
first task is sharply scoped: H's cap MIAPs to cvt[3]=528 (8.25px)->rounds to 8,
but FreeType yields 9, so its post-prep CVT differs — a prep-correctness issue
(alignment-zone CVT adjustment), not a mover bug (all x-hinting is already exact).
Next: diff FreeType's post-prep CVT against ours at 12px.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Durable oracle-diff tooling for M2 (out of /tmp): hint-weftdump.lisp dumps weft's
hinted F26.6 points (or the post-prep CVT with "cvt"); hint-diff.py runs it beside
the FreeType oracle and reports EXACT / max-Δ per glyph. Bit-exact gate for the
interpreter work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
… bit-exact

Two fixed-point/opcode bugs in prep were pushing the cap/x-height CVTs (and
hence H/o/i/n/e/a) off by up to a full pixel vertically:

1. RUTG (round-up-to-grid) used threshold=64 in the super-round formula, so
   hround(0) returned 64 instead of 0 (an exact-grid value wrongly gained a
   pixel).  Round-up = ceil, whose super-round threshold is period-1 = 63.

2. DELTAC/DELTAP popped the exception spec before the cvt/point index.  Per the
   TrueType spec the stack after n is "... arg1 idx1 n" with the index nearer the
   top, so the index must be popped first.  With the swap every per-ppem
   exception targeted the wrong entry / never matched ppem, so the cap-height and
   x-height CVT deltas at 12px never fired.  ('l' stayed exact because no DELTA
   fires for it at 12px.)

Post-prep CVT for LiberationSans @12px now matches FreeType's classic v35
interpreter bit-for-bit (verified by reading the scaled cvt array out of
TT_Size via /proc/self/mem).  Point error on the cap group collapses:
H 64->4, i 64->1, o 78->14, n 76->12, e 78->14, a 77->19 (F26.6).  'l' still
exact; scribe suite still 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Two more systematic errors that kept every glyph ~1 subpixel off:

1. IP measured original distances from the scaled `org` coords, losing precision
   (font-unit 136 vs 137 both scale to 51 @12px), so interpolated stems landed a
   unit off.  FreeType's IP ratio uses `orus` (original UNSCALED font-unit
   coords).  Added orus-x/orus-y to the zone, populate for real + phantom points,
   and use them (dual-projected) for IP's range/dist ratio.

2. MDRP/MIRP had the round and keep-minimum-distance flag bits swapped: the spec
   (and FreeType) use bit2 (0x04)=round, bit3 (0x08)=min-distance.  We used bit3
   for round, so e.g. the i-dot MDRP[01001] (min-dist, no round) was wrongly
   rounding the dot height (511->512).

LiberationSans @12px: l H i now bit-exact; o n e a down to 1/64px.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType's IUP interpolates untouched interior points using the orus (original
font-unit) ratio, not the scaled org, so sub-pixel spacing is preserved; points
outside the touched span still shift rigidly by the nearest touched delta (scaled
org).  Switched iup-axis to take an orus accessor and use it for the interior
case + range test.

LiberationSans @12px: o, n now bit-exact.  Printable-ASCII exact counts jump to
ppem7 69/94, ppem10 43/94, ppem12 26/94, ppem16 25/94.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
reset-glyph-gs only reset the vectors/rp/zp/loop, leaking a glyph program's
round mode / SMD / SDB / cut-in changes into the next glyph (e.g. 'c' after 'b'
went 0.95px off).  Snapshot the whole GS after prep and restore it before every
glyph, matching FreeType.  'c' and 'g' fixed.  ppem12 26->31/94 exact.  10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType's MDRP/MIRP compute the original distance by scaling the ORUS (font-unit)
difference between the two points once (MulFix), not by subtracting two
independently-rounded scaled coords.  The latter double-rounds and lands ~1/64px
off on unrounded (MDRP no-round / min-distance) moves.  Added orus-dist and use it
for both movers (twilight still falls back to scaled org).

ppem exact counts: 7 70->78, 10 47->73, 12 31->54, 16 28->38 (of 94).  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…A axis bits

Two bugs, both hit glyphs whose programs act before their first SVTCA:

1. reset-glyph-gs restored the vectors/rp/zp/loop from the post-prep snapshot,
   but FreeType resets those to the FIXED default (projection=freedom=dual=x-axis,
   zone 1, rp 0, loop 1) every glyph, keeping only the policy fields (round mode,
   cut-ins, delta, ...) from prep.  This font's prep leaves the vectors on the y
   axis, so glyphs that DELTAP the advance phantom before any SVTCA (v, M, A, y,
   z, m, s, w, ...) moved it in y instead of x — the advance and every
   IP-referenced point came out ~1px wide.  Now bit-exact.

2. SPVTCA/SFVTCA (0x02-0x05) had their axis bit inverted (only SVTCA was right);
   opcode&1 selects the axis (1=x, 0=y).

ppem exact: 7 78->84, 10 73->81, 12 54->62, 16 38->55 (of 94).  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
'$' (and other glyphs) call a fpgm function using SHPIX, which was unimplemented
and trapped — crashing the dump and making every later glyph look composite.
SHPIX shifts each of the loop points by a pixel amount along the freedom vector.

ppem exact (of 94 printable ASCII): 7 84, 10 82, 12 62->89, 16 55->79.  10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType bases the interior interpolation on the endpoint with the smaller orus
coordinate; using the contour-order endpoint rounds the single MulDiv differently
(off by 1/64).  Base on the smaller-orus reference to match.

ppem exact (of 94): 7 87, 10 83, 12 90, 16 90.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…our movers)

DejaVu Sans exercises opcodes Liberation didn't: SPVTL/SFVTL (0x06-0x09, set
projection/freedom vector along or perpendicular to a line of two points, with
FreeType-style unit-vector normalization), ISECT (0x0F, move a point to the
intersection of two lines), and SHC/SHZ (0x34-0x37, shift a whole contour / zone
by a reference point's displacement).  Each was trapping and aborting the dump.

DejaVuSans printable-ASCII exact: ppem 7 64, 10 63, 12 65, 16 63 (was 2, crashing
on SPVTL).  LiberationSans unchanged (90/94 @12).  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…MSIRP

Implemented composite-glyph hinting: each component is hinted standalone (its own
zone + instructions, via hint-subglyph), then transformed (2x2 F2.14) and offset
into the parent (xy offsets grid-fit per ROUND_XY_TO_GRID), components merged, and
the composite's own instruction stream run on the assembled outline.  parse-
composite-components + composite-aware glyph-instructions added; load-glyph-zone
routes both simple and composite through glyph-points.  Also implemented MSIRP
(0x3A/0x3B, move stack indirect relative point), used by accented glyphs.

LiberationSans: accented Latin-1 29/52 bit-exact @12px (bases always exact; some
accents 1px off on the component offset — refinement pending).  ASCII unchanged
(90/94 @12).  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…DejaVu)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
No bit-exact fix landed this round; documenting the precise root-cause analysis of
the three remaining residual classes so the next pass can resume without redoing the
investigation.  Key results: DejaVu post-prep CVT confirmed bit-exact vs FreeType;
the diagonal blow-up is a projection-amplified MIRP whose divergence needs FreeType
interpreter introspection; the LiberationSans 1/64 misses and the composite-accent
1px offsets both trace to the same IUP interior interpolation edge.  Suite 10/10;
Liberation ASCII unchanged (87/90/90 @7/12/16), DejaVu ~63-65/94.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…er blow up)

Found by per-instruction execution diff against a trace-enabled FreeType (see
inspect/hint-fttrace.md): at DejaVu '/'s first diagonal MIRP, weft's projection
after SPVTL was FreeType's rotated 90° — (4734,15697) vs (-15686,4730). The
parallel/perpendicular bit was inverted: FreeType rotates when `opcode & 1`
(SPVTL[1]/0x07 = perpendicular), weft rotated on the EVEN opcode. So weft built a
near-vertical projection where FreeType's is near-horizontal, and the freedom-x
MIRP move amplified ×(16384/4734)=3.46 → '/' pt0 landed at -937 vs FT 189.

Fix: rotate on the odd opcode. DejaVu '/' \\ A y: 17.6px off -> 1/64px; DejaVu
ASCII @12 65 -> 69/94; LiberationSans unchanged (its diagonals already used the
even/parallel path). Adds the weft-side per-instruction STATE trace (env
WEFT_HINT_TRACE) mirroring the patched FreeType build, for execution diffs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…ation

norm14 used isqrt, giving projection vectors ~9/16384 too long; that cascaded into
1-unit MIRP errors on diagonal glyphs (DejaVu / \ A y off by 1/64 even after the
SPVTL parallel/perp fix).  Ported FreeType's Normalize + FT_Vector_NormLen exactly
(Newton-Raphson normalize to 0x10000, then /4) so projection vectors match
bit-for-bit — verified via the per-instruction STATE trace (weft proj (15721,-4643)
-> FreeType's (15713,-4640)).  DejaVu V, X now bit-exact; / \ A y down to 1/64.
Also trace into CALL'd fpgm functions (weft's call-fn now emits STATE) so the
per-instruction diff aligns with FreeType.  Liberation unchanged (90/94 @12); 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
The projection dot product summed two mul2.14 results, rounding each term
separately; FreeType's TT_DotFix14 sums ax*bx + ay*by and rounds ONCE.  The double
rounding differed by 1 on diagonal projections (e.g. pt1 of DejaVu '/': 83 vs
FreeType's 82), cascading into 1-unit MIRP moves.  Found via the per-instruction
STATE trace.  DejaVu ASCII @12 70->75, @16 68->77; 'w' now bit-exact; Liberation
unchanged (90/94 @12).  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…nce vector

FreeType's MDRP/MIRP compute cur_dist = PROJECT(cur[point] - cur[rp0]) — projecting
the difference vector once — whereas weft subtracted two independently projected
points (project(p) - project(rp0)), which differs by a rounding unit on diagonals
(DejaVu '/': -60 vs -61) and cascaded into 1-unit mover errors.  Added cur-dist
(and org-dist) helpers and rewrote the MDRP/MIRP move as (distance - cur_dist).
Root-caused via the per-instruction STATE trace.

DejaVu ASCII @7/12/16: 71/75/77 -> 90/88/88 ('/' '\' A w y V X now bit-exact).
LiberationSans unchanged (87/90/90).  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…ction

Applied the same PROJECT/DUALPROJ-of-the-difference fix to the remaining distance
ops: IP's ranges/dists, MD[0]/MD[1], ALIGNRP, and MSIRP now project the difference
vector once (org via ORUS font units, current via scaled cur) instead of
subtracting two independently-projected points.  Added orus-diff helper.

DejaVu ASCII @7/12/16: 90/88/88 -> 92/93/92.  LiberationSans unchanged (87/90/90).
Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType's iup_worker_interpolate_ is a hybrid: it sorts the two touched refs by
ORUS, but classifies each untouched point (below ref1 / above ref2 / interior)
using the SCALED org coordinate, while the interior interpolation ratio uses ORUS
(font units) via DivFix->MulFix.  weft used orus for the boundary too, so a point
sharing a ref's scaled position (e.g. b pt6: orus 316 vs ref 318, but both scale
to 119) was interpolated instead of shifting with the ref -> off by 1/64.  Ported
FreeType's routine exactly.

LiberationSans ASCII @7/12/16: 87/90/90 -> 94/94/93 (b g % & now bit-exact);
DejaVuSans 92/93/92 -> 93/93/94.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
MIRP applied the control-value cut-in using the un-flipped cvt, so a cvt and org
of opposite sign (|cvt-org| huge) wrongly triggered the cut-in (cvt=org) — e.g.
LiberationSans '%'@16 pt22 came out 1px low.  FreeType auto-flips cvt to org's sign
FIRST, then tests the cut-in (same-zone) on the flipped value.  Reordered to match,
and switched MDRP/MIRP min-distance to FreeType's asymmetric clamp (min-distance
helper).  Kept orus-dist for the original distance.

LiberationSans ASCII now 94/94 at every ppem 7-16.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…PROJ,scale))

FreeType's MDRP/MIRP/IP original distance dual-projects the ORUS (font-unit)
difference first, then scales (MulFix by x_scale, for x_scale==y_scale); weft scaled
each component before dual-projecting, which rounds differently for diagonal
projection vectors.  Now uses hscale(orus-diff).  No count change but FreeType-exact;
Liberation 94/94 all ppem, suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType's Compute_Point_Displacement projects the reference point's (cur - org)
difference with the PROJECTION vector; weft computed project(cur) - dualproj(org)
(mixing projection/dual and subtracting two projections), off by 1 on diagonals.
Fixed sh-ref to dot14(cur-org, projVector).

DejaVuSans ASCII @10/12: 93 -> 94/94.  Liberation 94/94 all ppem.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
When the freedom and projection vectors are near-perpendicular, F_dot_P (their dot
product) is ~0; FreeType's Compute_Funcs clamps |F_dot_P| < 0x400 to 0x4000 and
still moves.  weft skipped the move when fdotp was 0, so a diagonal MIRP with
freedom perpendicular to the projection (DejaVu '$' @7px) didn't move the point
(off by 1px).  Added the clamp.

DejaVuSans ASCII @7 -> 94/94.  Liberation 94/94 all ppem.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
ynniv and others added 6 commits July 2, 2026 23:19
DIV (0x62) used muldiv(a,b,64) — identical to MUL — instead of TrueType's
(n1*64)/n2 = muldiv(a,64,b).  This corrupted any fpgm arithmetic using DIV; found
via the STATE trace on LiberationSans 'é', whose accent-alignment function branched
the wrong way (a DIV gave -46 vs FreeType's -12), leaving the accent 1px off.

LiberationSans accented Latin-1 now 53/53 bit-exact at ppem 7/12/16 (composites
fully exact).  Printable ASCII still 94/94 both fonts.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
… + caching (M3)

rasterize-glyph gains a hinted path: when *hinting* applies (T = all sizes, or a
number N = only ppem<=N), the glyph is grid-fit via the bytecode interpreter and
rasterized at scale 1 on the integer pixel grid, with the grid-fit (integer)
advance so painted widths stay even — no sub-pixel drift.  Falls back to the
geometric path for composites we can't hint, variations, or any error.

- hint-glyph now also returns the hinted advance (phantom pp2-pp1).
- hinted-segments / hinted-advance: memoized per (font,gid,ppem) over a per-
  (font,ppem) hinter cache (fpgm+prep run once); installed into raster via the
  *hinted-segments-fn* hook so raster.lisp needn't depend on the interpreter.
- Default *hinting* nil: the geometric path and the whole suite are unchanged (10/10).

Small text renders crisp: stems/baseline land on pixel boundaries instead of
smearing across fractional rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
… spacing

The first M3 cut used integer advances + integer placement for hinted glyphs, which
quantized inter-glyph spacing and read as bad kerning at small sizes. Match the
browser instead: keep the grid-fit (it matters vertically — crisp stems/baseline)
but rasterize the hinted outline with the sub-pixel x-shift and advance by the
fractional (geometric) advance, so horizontal spacing stays smooth. hint-glyph's
advance value is no longer used by the raster path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
otl-lookup-indices reparsed the script/feature/lookup tables on every call; shaping
a page word-by-word made that quadratic.  Split into a cached wrapper + core, keyed
by (tag . features) on a new font %otl slot (like %gdef/%cmap).  Shape a page's
lookups once, not once per word.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…al X

*hint-light* returns the outline with hinted Y but the original (unhinted) scaled X,
so baseline/x-height/stem heights snap crisp while horizontal stems stay sub-pixel.
Full hinting's X grid-fit is crisp but choppy — uneven inter-letter rhythm; light
matches the browser's default Linux rendering.  Measured on a controlled sample vs
Chromium, light halves the per-pair ink-width error (21 -> 12 over 30 kern pairs).
Default nil (full); the hinted-glyph cache keys on the mode.  Suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
@ynniv
ynniv merged commit 38a1e82 into master Jul 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant