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>
This commit is contained in:
Nexoniarz 2026-09-20 00:35:34 +02:00
commit 5618164754
11 changed files with 568 additions and 132 deletions

View file

@ -22,12 +22,37 @@
#include "display.h"
#include "gfx.h"
#include "vt.h"
#include "text.h"
#include "ttf.h"
#define BACKSPACE 0x100
#define CHAR_HEIGHT 16
#define CHAR_WIDTH 8
#define COLOR_BG SCH_BG
// The cell size, and why it is not a constant any more.
//
// It was 8x16, the size of the built-in bitmap font, and everything in this
// file multiplied by it. That font is CP437: 256 glyphs, no Ą, no Ę, no Ś, no
// Ω, no ∞. Those did not come out wrong, they came out as `?`, because the
// grid could not hold a codepoint above 255 and mapped what it could not keep.
//
// So the console draws from a real typeface now, and a real typeface decides
// its own metrics. These start at the bitmap's size because the console has to
// work from the first panic onwards -- before gfx_font_init() has run there is
// no font, and 8x16 against font_data is exactly what there always was.
static int cell_w = 8;
static int cell_h = 16;
#define CHAR_WIDTH cell_w
#define CHAR_HEIGHT cell_h
// The typeface, once there is one. `cfont` is the switch: null means fall back
// to the bitmap, which is what early boot and any font failure get.
static struct ttf consttf;
static struct font *cfont;
static int cfont_baseline;
static uint32 blend(uint32 bg, uint32 fg, uint a);
// The scanout, as published by whichever driver owns the display. Zeroed until
// one does, and every entry point checks `fb` before touching memory -- a
// kernel that panics before any display driver has run still has to be able to
@ -222,21 +247,131 @@ gfx_putc(int c, uint32 color)
// right for a console that has one background and wrong for a terminal where
// every cell carries its own.
void
gfx_draw_cell(int col, int row, uchar glyph, uint32 fg, uint32 bg)
gfx_draw_cell(int col, int row, uint32 cp, uint32 fg, uint32 bg)
{
if (!fb)
return;
int x = col * CHAR_WIDTH, y = row * CHAR_HEIGHT;
int x = col * cell_w, y = row * cell_h;
if (x < 0 || y < 0 ||
x + CHAR_WIDTH > (int)scanout.width || y + CHAR_HEIGHT > (int)scanout.height)
x + cell_w > (int)scanout.width || y + cell_h > (int)scanout.height)
return;
for (int r = 0; r < CHAR_HEIGHT; r++) {
uchar bits = font_data[glyph][r];
for (int c = 0; c < CHAR_WIDTH; c++)
gfx_draw_pixel(x + c, y + r, (bits & (0x80 >> c)) ? fg : bg);
// No font yet: the bitmap, exactly as before. Anything it cannot hold is
// drawn as a space rather than as the wrong glyph -- `?` was at least honest
// about being a substitute, and a CP437 picture of something unrelated is
// not.
if (!cfont) {
uchar g = cp < 256 ? (uchar)cp : ' ';
for (int r = 0; r < cell_h; r++) {
uchar bits = font_data[g][r];
for (int c = 0; c < cell_w; c++)
gfx_draw_pixel(x + c, y + r, (bits & (0x80 >> c)) ? fg : bg);
}
return;
}
// Background first, then the glyph blended over it. The cell is always
// filled: a terminal cell owns its rectangle, and leaving the old pixels
// where a narrow glyph does not reach is how ghosting gets in.
for (int r = 0; r < cell_h; r++)
for (int c = 0; c < cell_w; c++)
gfx_draw_pixel(x + c, y + r, bg);
struct glyph gl;
if (cp == ' ' || cfont->ops->glyph(cfont, cp, &gl) != 0)
return; // no such glyph: the cell stays background
// `cov` is w*h bytes of coverage, 0..255, positioned by the bearing from the
// pen. The pen sits on the baseline at the left of the cell.
int gx = x + gl.bearing_x;
int gy = y + cfont_baseline - gl.bearing_y;
for (int r = 0; r < gl.h; r++) {
int py = gy + r;
if (py < y || py >= y + cell_h)
continue; // clip to the cell: a tall glyph must not
for (int c = 0; c < gl.w; c++) { // bleed into the line above or below
int px = gx + c;
if (px < x || px >= x + cell_w)
continue;
uint a = gl.cov[r * gl.w + c];
if (a == 0)
continue;
gfx_draw_pixel(px, py, a == 255 ? fg : blend(bg, fg, a));
}
}
}
// Bring the console typeface up. Called once, early, from the display path.
//
// The font is linked into the kernel (see CONSFONT in the Makefile) because
// the console has to work before there is a filesystem -- a panic during boot
// has to be readable.
//
// A size failure is not fatal and not silent: `cfont` stays null and every
// cell keeps coming from the bitmap, which is what this system has always
// drawn. A console that lost its font would be a much worse outcome than one
// that never had a good one.
void
gfx_font_init(void)
{
extern unsigned char _binary_consfont_start[], _binary_consfont_end[];
uint32 len = (uint32)(_binary_consfont_end - _binary_consfont_start);
struct glyph gl;
if (cfont)
return;
// 16 pixels per em. The bitmap it replaces is 16 tall, so the terminal keeps
// the number of rows it had and nothing that assumed a screenful has to
// change.
if (ttf_open(&consttf, _binary_consfont_start, len, 16) != 0)
return;
struct font *f = ttf_font(&consttf);
if (!f)
return;
// The cell is as wide as the font's own advance. JetBrains Mono is
// monospaced, so measuring one glyph measures all of them -- and measuring
// rather than assuming is what keeps this correct if the font is ever
// changed for another one.
if (f->ops->glyph(f, 'M', &gl) != 0 || gl.advance <= 0)
return;
int w = gl.advance, h = f->ops->height(f);
// Checked *before* they are stored. Writing them first and returning on a
// bad value leaves the console with a cell size no font is drawing at, and
// the bitmap fallback reading past its own 16-row glyphs.
if (w < 4 || w > 64 || h < 6 || h > 64)
return; // implausible metrics: keep the bitmap
cell_w = w;
cell_h = h;
cfont_baseline = f->ops->baseline(f);
cfont = f;
}
// **Nothing here prints.** gfx_font_init() runs before vt_init(), because
// vt_init() has to know how big a cell is before it can work out how many fit
// on the screen -- and by then cons_serial_only may already be clear, so a
// cprintf() would reach vt_current() before there is a current terminal. That
// is a fault during boot with no console to report it on, which is a
// spectacularly unhelpful way to fail.
//
// So main() asks afterwards and says it then.
int
gfx_font_ready(void)
{
return cfont != 0;
}
void
gfx_cell_size(int *w, int *h)
{
if (w) *w = cell_w;
if (h) *h = cell_h;
}
// Move the whole screen up one text row and clear the row it exposes. The