SchOS/tools/ttftest.c
Nexoniarz 5618164754 font: the console draws outlines now, and a byte could not have held Ą
"Ą Ę Ś Ł Ż" drew as "?????" and the obvious diagnosis -- the font has no
glyphs for them -- was wrong twice over. `struct vtcell.ch` was **one byte**
holding a CP437 index, and the console mapped UTF-8 down to it on the way in.
U+0104 does not fit in a byte. No font could have fixed that; the cell had to
get wider first.

So: `ch` is a uint32 codepoint, the mapping on the way in is gone, and the cell
draws through kernel/dev/ttf.c against JetBrains Mono linked into the image.
Cell size comes from the font's measured advance instead of a hardcoded 8x16.
The grid costs 864 KB of BSS instead of 432 KB, which is the honest price of
holding a codepoint per cell for six terminals at 240x75.

The rasteriser needed three fixes before it could be trusted with this.

**21 KB of locals on a 4 KB kernel stack.** `ttf_render` held an outline (512
points as three arrays) and a raster (1024 edges) as automatics. KSTACKSIZE is
4096. They walked off the bottom of the stack into whatever was underneath,
which surfaced as user processes faulting on addresses they never touched and
then a panic inside freevm() on an overwritten page directory. Nothing pointed
anywhere near a font.

**And then again, recursively.** load_outline() held a 4736-byte outline as a
local *and recursed* for composite glyphs. Composite glyphs are exactly Ą, Ę,
Ś, Ł, Ż -- so the second crash arrived the moment the first fix let Unicode
reach the screen, looking like a fresh bug. Now a static outline per recursion
level with a depth cap of 4 (the font's deepest real nesting is 2), and the
leaf parse split into load_simple() so its 512-byte flag array is not
multiplied by the depth. Worst chain measured from the object file: ~2.3 KB.

Neither showed in tools/ttftest, which has an 8 MB stack. That is the standing
hazard of one source file built for host and kernel: the host is most forgiving
about exactly the resource the kernel is strictest about.

**Component transforms were being skipped**, on the stated theory that a wrong
transform is worse than none. It is not. `‘` in this font is the comma glyph
with a scale of -1 -- a 180-degree rotation -- and the offset that ships with
it only lands correctly after the rotation. Without it the mark went twelve
pixels into a ten-pixel cell and was clipped away entirely, so the character
read as missing from the font rather than as misplaced. F2Dot14 2x2 now, with
SCALED_COMPONENT_OFFSET honoured.

The glyph cache was ASCII-only, so every accented letter re-rasterised on every
repaint: a screenful of Polish did hundreds of bezier flattenings per scroll
where the same screenful of English did none. 95 reserved ASCII slots plus 224
shared slots keyed by codepoint, 64 KB pool. Verified 13680 cached renders
byte-identical to direct ones across collisions and a pool flush.

tools/ttftest speaks UTF-8 now. Iterating bytes meant it could only ever ask
for ASCII, and ASCII is entirely simple glyphs -- it could not reach the code
path that mattered.

Also: resolvconf's success line moves behind /proc/bootmode, same rule as
vgpud. A quiet boot opened with "resolvconf: nameserver 10.0.2.3" where the
one line was supposed to be "Welcome to SchOS!". Failures stay loud.

Verified on screen, not just on the host: Ą Ę Ś Ł Ż ć ń ó ź, Ω λ π Œ Ð Æ ß,
∞ ‘x’ → ≤ all render, panic=0 across four boots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:35:34 +02:00

107 lines
3.9 KiB
C

// Host harness for kernel/dev/ttf.c.
//
// A rasteriser is checked by looking at it. This renders a string with the real
// font and writes a PGM, which is the smallest image format that needs no
// compression -- the point is the glyphs, not the container.
//
// build/tools/ttftest <font.ttf> <px> "text" out.pgm
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TTF_HOST
#include "../kernel/dev/ttf.c"
int ttf_render(struct ttf *, uint32, struct glyph *, unsigned char *, int, int *, int *);
// The console speaks UTF-8, so the harness has to as well -- otherwise the
// only glyphs it can ever ask for are ASCII, and ASCII is entirely made of
// simple glyphs. Every composite in a Latin font is an accented letter, which
// is to say: the byte-at-a-time version could not reach the code path that
// mattered.
//
// text.h declares utf8_next and kernel/dev/text.c defines it, but that file
// pulls in the kernel headers and cannot be compiled on the host. Define the
// symbol here instead: a second decoder is the cost of the harness existing
// at all, and the two agree on the only thing that matters here, which is
// always advancing.
uint32 utf8_next(const char **pp)
{
const unsigned char *p = (const unsigned char *)*pp;
uint32 c = *p++;
int extra = 0;
if ((c & 0xe0) == 0xc0) { c &= 0x1f; extra = 1; }
else if ((c & 0xf0) == 0xe0) { c &= 0x0f; extra = 2; }
else if ((c & 0xf8) == 0xf0) { c &= 0x07; extra = 3; }
while (extra-- > 0 && (*p & 0xc0) == 0x80)
c = (c << 6) | (*p++ & 0x3f);
*pp = (const char *)p;
return c;
}
int main(int argc, char **argv)
{
if (argc < 5) { fprintf(stderr, "usage: ttftest font.ttf px \"text\" out.pgm\n"); return 1; }
const char *path = argv[1];
int px = atoi(argv[2]);
const char *text = argv[3];
FILE *fp = fopen(path, "rb");
if (!fp) { perror(path); return 1; }
fseek(fp, 0, SEEK_END); long n = ftell(fp); fseek(fp, 0, SEEK_SET);
unsigned char *buf = malloc(n);
if (fread(buf, 1, n, fp) != (size_t)n) { fprintf(stderr, "short read\n"); return 1; }
fclose(fp);
struct ttf f;
if (ttf_open(&f, buf, (uint32)n, px) != 0) {
fprintf(stderr, "not a TrueType font this can read\n"); return 1;
}
printf("unitsPerEm %d, %d glyphs, ascent %d descent %d, %s loca\n",
f.units_per_em, f.num_glyphs, f.ascent, f.descent,
f.long_loca ? "long" : "short");
struct font *fo = ttf_font(&f);
int H = fo->ops->height(fo) + px; // room for descenders and padding
int base = fo->ops->baseline(fo) + px / 2;
int W = 16;
for (const char *p = text; *p; ) { utf8_next(&p); W += px; } // generous
unsigned char *img = calloc(W * H, 1);
int pen = 8, drawn = 0, missing = 0;
for (const char *p = text; *p; ) {
uint32 cp = utf8_next(&p);
struct glyph g;
// Through the provider, not ttf_render, so the glyph cache is on the
// path being tested rather than bypassed by the harness.
if (fo->ops->glyph(fo, cp, &g) != 0) {
missing++;
pen += px / 2;
continue;
}
for (int gy = 0; gy < g.h; gy++)
for (int gx = 0; gx < g.w; gx++) {
int X = pen + g.bearing_x + gx;
int Y = base - g.bearing_y + gy;
if (X < 0 || Y < 0 || X >= W || Y >= H) continue;
int v = img[Y * W + X] + g.cov[gy * g.w + gx];
img[Y * W + X] = v > 255 ? 255 : v;
}
pen += g.advance;
drawn++;
}
// White on black inverted to black on white, which is how text is read.
for (int i = 0; i < W * H; i++) img[i] = 255 - img[i];
FILE *o = fopen(argv[4], "wb");
fprintf(o, "P5\n%d %d\n255\n", W, H);
fwrite(img, 1, W * H, o);
fclose(o);
printf("%d glyphs drawn, %d missing, %d px advance, image %dx%d -> %s\n",
drawn, missing, pen - 8, W, H, argv[4]);
return drawn ? 0 : 1;
}