--- name: schunix-ttf-rasteriser description: The TrueType rasteriser — why glyf and not CFF, and the composite-glyph trap that loses the letter "i". metadata: type: project --- Built 2026-08-09. `include/ttf.h` + `kernel/dev/ttf.c`, the second `font_ops` provider behind [[schunix-text-rendering]]. Host harness: `tools/ttftest.c` → `build/tools/ttftest font.ttf px "text" out.pgm`. Renders real JetBrains Mono at 32 px: correct advances, anti-aliased edges, the slashed zero, dots on i and j. ## glyf, not CFF — and that was checked, not assumed `.ttf` and `.otf` are both sfnt containers with the same table directory, cmap and hmtx. Only the *outlines* differ: TrueType puts quadratic B-splines in `glyf`; OpenType/PostScript puts cubic béziers in `CFF ` as Type 2 charstrings, a stack-based interpreted format and a separate project. **Both chosen fonts ship `glyf` and neither ships `CFF `** — verified by dumping JetBrains Mono's table directory before writing a line. So CFF buys nothing today. `ttf_open()` rejects `OTTO` explicitly rather than failing later with an empty glyph and no explanation. ## The trap: composite glyphs are not an edge case First working render produced **"Schun x"** — the `i` was missing. `i` is a *composite* glyph: dotless-i plus the dot, assembled from two other glyphs at offsets. Skipping composites (`numberOfContours < 0`) does not just lose accented characters, it loses a letter from the project's own name. Now assembled: parse the component list (flags, glyph index, dx/dy, optional scale), recurse, merge contours with the offset applied. Only the **translation** case is handled — components with a scale or 2×2 matrix are skipped, because applying a transform wrongly is worse than not applying one. ## Other things that bite - Every sfnt field is **big-endian**; read byte by byte, never cast. The file is not guaranteed aligned and a cast byte-swaps everything on x86. - Glyph flags are **run-length encoded** (bit 3 = "next byte is a repeat count"). Missing that turns any glyph with a repeated flag into garbage. - Coordinates are **deltas** in one of three encodings per axis, selected by two flag bits — including "same as previous", which occupies no bytes. - TrueType **omits the on-curve point between two consecutive off-curve points**; it must be synthesised at the midpoint or curves render as corners. - `hmtx` stores `numberOfHMetrics` full entries then bearings only — clamp the index. - cmap format 4's `idRangeOffset` is a byte offset **from its own address**, which is why that arithmetic looks the way it does. ## Glyph cache (added the same day) Direct-mapped over printable ASCII 32..126 into a 48 KB pool, filled linearly, never freed. No hashing, no eviction policy, no allocation — the contents are a pure function of (codepoint, size), so nothing ever goes stale. A **size change drops the whole cache**, which is the only event that invalidates it. It fixes two things, not one: the obvious cost of re-rasterising every glyph, and the contract — the single `ttf_cov` scratch buffer meant a glyph was valid only until the next call, so a caller could not hold two at once. Cached glyphs now point into the pool. Codepoints outside ASCII, and anything that arrives after the pool is full, still render correctly through the scratch buffer under the old contract. `tools/ttftest.c` calls through `fo->ops->glyph`, not `ttf_render`, so the cache is on the tested path rather than bypassed by the harness. ## Still not done - **No font loaded from disk.** The kernel compiles the rasteriser but no `.ttf` is in the tree and nothing reads one. This is what blocks pointing the console at it. - **Hinting ignored** (`fpgm`/`prep`/`cvt ` skipped). Fine above ~16 px, blurry below. Real hinting is a bytecode interpreter — a project, not a fix. - **No GSUB**, so ligatures do not form: JetBrains Mono's `==` renders as two glyphs. Correct, just not what the font can do. - **CFF/OTF outlines.** Requested 2026-08-09 for people who want a non-TrueType font in the terminal. Not started, and it is not "more of the same": Type 2 charstrings are a stack-based bytecode to *interpret* — INDEX/DICT structures, charset, local and global subrs with the biasing rule, ~40 operators, cubic béziers. Roughly the size of everything else here, and a subtly wrong interpreter draws plausible-looking wrong glyphs. It plugs in as a third `font_ops` provider, so nothing above it changes.