gfx: the console's typeface is replaceable, and the loader is what replaces it

Gate 0's last-but-one open item. Drop a TrueType file at \schos\console.ttf on
the ESP and the console is drawn with it; with no file, the face linked into
the kernel image, which is what an early panic needs and is not a degraded
mode.

**The loader reads it, and that is the decision worth writing down.** The
kernel opening the file once the root filesystem is mounted is the obvious
design. It is also wrong: the console has been drawing since the kernel's
first line, so a font arriving later changes the cell size, which changes the
column count, which re-lays out every terminal -- and blanks them. The kernel
log would vanish off the screen at the moment the root filesystem mounted,
which is exactly the log a machine being brought up for the first time is
being read for. Since the next thing on the list is real hardware, losing that
was not a trade worth making.

So the choice happens before anything is drawn. Three details make it cheap:

  - **EfiReservedMemoryType**, not EfiLoaderData. The E820 handoff turns
    loader data into usable memory, on the argument that the loader's own
    pages are the kernel's the moment it runs -- true of the loader, not true
    of a buffer it wants the kernel to keep reading from.
  - **Below 4 GB**, because the kernel reaches this through its direct map and
    that is how far the direct map goes. A font above it would need an ioremap
    the console cannot do before there is a console.
  - **Used in place.** No copy, which matters because the kernel has no
    contiguous allocator to copy 112 KB into -- the only one is the DMA arena,
    and that belongs to devices.

Sized without EFI_FILE_INFO and its GUID: seek past the end and ask where that
landed, which the specification requires to clamp.

Every failure falls back and keeps going -- no file, implausible size, no
memory, short read, unparseable file. The last says so in the log, because a
font that silently did not load looks exactly like a font that did.

Verified by booting with JetBrainsMono-ExtraBold.ttf on the ESP and
screendumping the console: visibly the bold face, against the Regular of a
boot with no file. Three boots each way, no traps and no panics, socktest and
memfdtest green in all six.

Nothing ships a console.ttf: the file would be the file already in the kernel,
and two copies of a font are two things that can disagree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-09-23 17:47:45 +02:00
commit d85274dbd9
5 changed files with 215 additions and 5 deletions

View file

@ -264,6 +264,43 @@ So the text layer fixes three things:
changing — glyphs carry 8-bit coverage, so an anti-aliased outline renderer
already has somewhere to put its output.
### The font is replaceable
The console draws with the TrueType face linked into the kernel image, and
will use a different one if the boot volume offers it: `\schos\console.ttf`,
or `\console.ttf`, on the ESP.
```sh
cp ports/JetBrains_Mono/static/JetBrainsMono-ExtraBold.ttf \
build/esp/schos/console.ttf
```
Nothing ships one, because the file that would be shipped is the file already
in the kernel, and two copies of a font are two things that can disagree. What
this buys is that the typeface is a property of the *installation* rather than
of the build.
**The loader reads it.** The kernel could open the file itself once the root
filesystem is mounted, and that is the obvious design and the wrong one: the
console has been drawing since the kernel's first line, so a font that arrives
later changes the cell size, which changes the number of columns, which
re-lays out every terminal — and blanks them. The kernel log would disappear
off the screen at the moment the root filesystem mounted, which is precisely
the log a machine being brought up for the first time is being read for.
So the choice is made before anything is drawn. The loader allocates the pages
as `EfiReservedMemoryType` — not `EfiLoaderData`, which the E820 handoff turns
into usable memory on the argument that the loader's own pages belong to the
kernel the moment it runs; true of the loader, not true of this — and hands
over a physical address and a length in `SCHBOOT_FONT_ADDR`. The kernel reads
the file where it lies. There is no copy, and no need for the contiguous
allocator the kernel does not have.
Every failure falls back to the built-in font and keeps going: no file, an
implausible size, no memory, a short read, or a file that will not parse. The
last of those says so in the log, because a font that silently did not load
looks exactly like a font that did.
### The TrueType rasteriser
`kernel/dev/ttf.c`: `glyf` outlines, composite glyphs with their component

View file

@ -256,9 +256,21 @@ These are the galling ones — the replacement exists, is tested, and is unused.
open. The console is a fixed grid of codepoints; `text.c`'s proportional
path has no caller. Nothing needs this before the compositor, which is
why it is separated from the row above rather than left blocking it.
- [ ] **Load a font from disk** and point the console at it. The built-in one
is linked in on purpose — an early panic has to be readable — so this is
about a *replaceable* font, not about having one.
- [x] ~~**Load a font from disk**~~ and point the console at it. The built-in
one is linked in on purpose — an early panic has to be readable — so this
is about a *replaceable* font, not about having one. Drop a TrueType file
at `\schos\console.ttf` on the ESP and the console is drawn with it.
**The loader reads it, not the kernel**, and that is the whole design
decision. The kernel could open the file once the root filesystem is
mounted — but the console has been drawing since long before that, so
switching then means re-measuring the cell and re-laying out every
terminal mid-boot, which wipes the kernel log off the screen at exactly
the moment a machine being brought up for the first time needs it. The
loader allocates the pages as `EfiReservedMemoryType`, so the E820
handoff marks them reserved, the page allocator never offers them, and
the kernel uses the file *in place* — no copy, and no need for a
contiguous allocator the kernel does not have.
- [x] ~~**Interrupt-driven receive.**~~ The e1000's handler calls
`netdev_rx_signal()`, which ends the network thread's wait early; the frame
is still *pulled* by the thread, because an interrupt handler can neither