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

86 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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.