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.
86 lines
4.4 KiB
Markdown
86 lines
4.4 KiB
Markdown
---
|
||
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.
|