SchOS/.claude/memory/schunix-ttf-rasteriser.md
Nexoniarz a1c965da1e include: one folder per subsystem, and includes that name it
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".

Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 15:05:12 +02:00

120 lines
6.1 KiB
Markdown
Raw Permalink 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/gfx/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.
## 2026-09-21: it is the console font now
Linked into the kernel image (`consfont` in the Makefile), opened at 16 px
before `vt_init()`. Three things had to be fixed first, and two of them were
the same bug twice:
- **`ttf_render` held 21 KB of locals** on a 4096-byte kernel stack. Now
`static`, safe because every caller is serialised behind `cons.lock`.
- **`load_outline` did it again, recursively**, with a 4736-byte outline per
level. Composite glyphs are exactly Ą/Ę/Ś/Ł/Ż, so the second crash arrived
the moment the first fix let Unicode reach the screen and looked like a
fresh bug. Static outline per level, depth capped at 4 (the font's real
maximum nesting is 2), and `load_simple()` split out so the leaf's 512-byte
flag array is not multiplied by depth.
- **Component transforms were being skipped.** `'` is the comma glyph scaled
by -1 — a 180° rotation — and its offset only lands right after the
rotation. Without it the mark went 12 px into a 10 px cell and was clipped
away, so the character read as *missing from the font* rather than as
misplaced. F2Dot14 2x2 is applied now.
Neither stack bug showed in `tools/ttftest`, which has an 8 MB stack. **That is
the standing hazard of this file**: one source built for host and kernel, and
the host is most forgiving about exactly the resource the kernel is strictest
about. Check frame sizes with `llvm-objdump -d` on the kernel object, not the
host build.
The harness speaks UTF-8 now. Iterating bytes meant it could only ask for
ASCII, and ASCII is entirely simple glyphs — it could not reach the path that
mattered.
Glyph cache: 95 reserved ASCII slots plus 224 shared slots keyed by codepoint,
64 KB pool, flushed whole when it fills.