SchOS/.claude/memory/schunix-ttf-rasteriser.md
Nexoniarz 3182da3965 Carry project memory in the repo, with tools/memory-sync.sh
Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:

  - D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
    invisible until moved across by hand.
  - Development happens from Windows and from Linux, and each looks in a
    different place for the same checkout.

Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.

The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
2026-08-09 14:27:22 +02:00

4.4 KiB
Raw Blame History

name description metadata
schunix-ttf-rasteriser The TrueType rasteriser — why glyf and not CFF, and the composite-glyph trap that loses the letter "i".
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.