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

@ -301,6 +301,10 @@ copy(void *dst, const void *src, UINTN n)
// Reading the kernel off the ESP // Reading the kernel off the ESP
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The volume this loader came off, kept so that anything else read at boot is
// read from the same place the kernel was. Set by open_kernel().
static EFI_FILE_PROTOCOL *boot_root;
static EFI_FILE_PROTOCOL * static EFI_FILE_PROTOCOL *
open_kernel(EFI_HANDLE image) open_kernel(EFI_HANDLE image)
{ {
@ -323,6 +327,8 @@ open_kernel(EFI_HANDLE image)
st = fs->OpenVolume(fs, &root); st = fs->OpenVolume(fs, &root);
if (EFI_ERROR(st)) die(L"cannot open the boot volume", st); if (EFI_ERROR(st)) die(L"cannot open the boot volume", st);
boot_root = root;
st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH, EFI_FILE_MODE_READ, 0); st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH, EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st)) if (EFI_ERROR(st))
st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH_ALT, st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH_ALT,
@ -332,6 +338,95 @@ open_kernel(EFI_HANDLE image)
return file; return file;
} }
// ---------------------------------------------------------------------------
// The console typeface
//
// See SCHBOOT_FONT_ADDR in include/schboot.h for why this is the loader's job
// and not the kernel's. Everything here is best-effort: a machine with no
// console.ttf on its ESP boots with the font in the kernel image, which is
// the same font, and says nothing about it.
// ---------------------------------------------------------------------------
#define FONT_PATH L"\\schos\\console.ttf"
#define FONT_PATH_ALT L"\\console.ttf"
// Four megabytes. Large enough for any single-weight TrueType face -- the one
// in the kernel image is 112 KB -- and small enough that a wrong file cannot
// eat the machine's memory before the kernel has any.
#define FONT_MAX (4u * 1024u * 1024u)
static void
load_console_font(void)
{
EFI_FILE_PROTOCOL *file = 0;
EFI_STATUS st;
UINT64 size = 0;
EFI_PHYSICAL_ADDRESS at;
UINTN read;
// Absent until proven present: the kernel reads these two fields with no
// other way to know whether the loader wrote them.
*(UINT64 *)SCHBOOT_FONT_ADDR = 0;
*(UINT32 *)SCHBOOT_FONT_LEN = 0;
if (!boot_root)
return;
st = boot_root->Open(boot_root, &file, (CHAR16 *)FONT_PATH,
EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st))
st = boot_root->Open(boot_root, &file, (CHAR16 *)FONT_PATH_ALT,
EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st) || !file)
return; /* no console.ttf; the built-in one stands */
// The size, without EFI_FILE_INFO and its GUID: seek past the end and ask
// where that landed. The specification requires SetPosition beyond EOF to
// clamp to the end, which is exactly the number wanted.
if (EFI_ERROR(file->SetPosition(file, 0xFFFFFFFFFFFFFFFFULL)) ||
EFI_ERROR(file->GetPosition(file, &size)) ||
EFI_ERROR(file->SetPosition(file, 0))) {
file->Close(file);
return;
}
if (size < 4096 || size > FONT_MAX) {
vprint(L"schos: console.ttf is an implausible size; ignoring it\r\n");
file->Close(file);
return;
}
// 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.
//
// Reserved rather than loader data: EfiLoaderData becomes E820_USABLE in
// the handoff, on the argument that the loader's own pages are the
// kernel's the moment it runs. True of the loader; not true of this.
at = 0xFFFFFFFFULL;
st = BS->AllocatePages(AllocateMaxAddress, EfiReservedMemoryType,
(UINTN)((size + 0xFFF) / 0x1000), &at);
if (EFI_ERROR(st)) {
vprint(L"schos: no memory for console.ttf; using the built-in font\r\n");
file->Close(file);
return;
}
read = (UINTN)size;
st = file->Read(file, &read, (void *)(UINTN)at);
file->Close(file);
if (EFI_ERROR(st) || read != size) {
vprint(L"schos: console.ttf would not read; using the built-in font\r\n");
return; /* the pages stay reserved; harmless */
}
*(UINT64 *)SCHBOOT_FONT_ADDR = (UINT64)at;
*(UINT32 *)SCHBOOT_FONT_LEN = (UINT32)size;
vprint(L"schos: console font "); vprint_hex((UINT32)size);
vprint(L" bytes at "); vprint_hex((UINT32)at); vprint(L"\r\n");
}
// Load PT_LOAD segments to p_paddr. Physical addresses, not virtual: the // Load PT_LOAD segments to p_paddr. Physical addresses, not virtual: the
// kernel is linked at 0xffff800000100000 but must be *placed* at 0x100000, and // kernel is linked at 0xffff800000100000 but must be *placed* at 0x100000, and
// p_paddr is where the linker script says each segment belongs. // p_paddr is where the linker script says each segment belongs.
@ -1346,6 +1441,12 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
ui_progress_end(); ui_progress_end();
file->Close(file); file->Close(file);
// After open_kernel, because that is what finds the volume, and after the
// kernel rather than before it: the font is optional and the kernel is
// not, so a machine that is going to fail should fail on the thing that
// matters.
load_console_font();
vprint(L"schos: kernel entry "); vprint_hex(entry); vprint(L"\r\n"); vprint(L"schos: kernel entry "); vprint_hex(entry); vprint(L"\r\n");
// Read while boot services still exist and the system table is guaranteed // Read while boot services still exist and the system table is guaranteed

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 changing — glyphs carry 8-bit coverage, so an anti-aliased outline renderer
already has somewhere to put its output. 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 ### The TrueType rasteriser
`kernel/dev/ttf.c`: `glyf` outlines, composite glyphs with their component `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 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 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. 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 - [x] ~~**Load a font from disk**~~ and point the console at it. The built-in
is linked in on purpose — an early panic has to be readable — so this is one is linked in on purpose — an early panic has to be readable — so this
about a *replaceable* font, not about having one. 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 - [x] ~~**Interrupt-driven receive.**~~ The e1000's handler calls
`netdev_rx_signal()`, which ends the network thread's wait early; the frame `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 is still *pulled* by the thread, because an interrupt handler can neither

View file

@ -89,6 +89,29 @@
// //
#define SCHBOOT_RSDP_ADDR (SCHBOOT_BASE + 0x600) #define SCHBOOT_RSDP_ADDR (SCHBOOT_BASE + 0x600)
// A console typeface, read off the boot volume by the loader.
//
// Zero length means "there was none" and the kernel uses the one linked into
// its own image. That fallback is not a degraded mode: an early panic has to
// be readable, and the only font guaranteed to exist before any filesystem
// does is the one in the image.
//
// **Loaded by the loader rather than by the kernel**, which is the whole
// design decision here. The kernel could read the file itself once the root
// filesystem is mounted -- but the console is drawing before that, so
// switching then means re-measuring the cell and re-laying out every terminal
// mid-boot. That wipes the kernel log off the screen at exactly the moment a
// machine being brought up for the first time needs it. Choosing the font
// before the first line is drawn costs one file read and changes nothing
// afterwards.
//
// The pages are allocated as EfiReservedMemoryType, so the E820 handoff marks
// them reserved and the page allocator never hands them out -- the font is
// used *in place*, and nothing in the kernel has to find room for 100 KB of
// contiguous memory.
#define SCHBOOT_FONT_ADDR (SCHBOOT_BASE + 0x608) /* uint64, physical */
#define SCHBOOT_FONT_LEN (SCHBOOT_BASE + 0x610) /* uint32, bytes */
#ifndef __ASSEMBLER__ #ifndef __ASSEMBLER__
struct sch64_header { struct sch64_header {

View file

@ -23,6 +23,9 @@
#include "gfx.h" #include "gfx.h"
#include "vt.h" #include "vt.h"
#include "text.h" #include "text.h"
#include "schboot.h"
#include "schboot.h"
#include "memlayout.h"
#include "ttf.h" #include "ttf.h"
#define BACKSPACE 0x100 #define BACKSPACE 0x100
@ -371,16 +374,50 @@ void
gfx_font_init(void) gfx_font_init(void)
{ {
extern unsigned char _binary_consfont_start[], _binary_consfont_end[]; extern unsigned char _binary_consfont_start[], _binary_consfont_end[];
const unsigned char *data = _binary_consfont_start;
uint32 len = (uint32)(_binary_consfont_end - _binary_consfont_start); uint32 len = (uint32)(_binary_consfont_end - _binary_consfont_start);
struct glyph gl; struct glyph gl;
if (cfont) if (cfont)
return; return;
// A typeface off the boot volume, if the loader found one.
//
// Tried first and fallen back from silently: the built-in font is not a
// degraded mode, it is the same font, and a machine with no console.ttf is
// the ordinary case rather than a fault. What this buys is that the font is
// *replaceable* -- drop a different TrueType file on the ESP and the console
// is drawn with it, with no kernel to rebuild.
//
// In place, out of memory the loader marked reserved. See SCHBOOT_FONT_ADDR
// for why the kernel does not read this file itself.
{
uint64 font_pa = *(volatile uint64 *)P2V(SCHBOOT_FONT_ADDR);
uint32 font_len = *(volatile uint32 *)P2V(SCHBOOT_FONT_LEN);
if (font_pa && font_len >= 4096 && font_pa + font_len <= 0x100000000ULL) {
data = (const unsigned char *)P2V(font_pa);
len = font_len;
}
}
// 16 pixels per em. The bitmap it replaces is 16 tall, so the terminal keeps // 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 // the number of rows it had and nothing that assumed a screenful has to
// change. // change.
if (ttf_open(&consttf, _binary_consfont_start, len, 16) != 0) if (ttf_open(&consttf, data, len, 16) != 0) {
// A bad file on the ESP must not cost the console its font. Nothing has
// been changed yet at this point, so the built-in one is still there to
// fall back to -- and it is worth saying, because a font that silently
// did not load looks exactly like a font that did.
if (data == _binary_consfont_start)
return; return;
kinfo("console: the font on the boot volume would not parse; "
"using the built-in one\n");
data = _binary_consfont_start;
len = (uint32)(_binary_consfont_end - _binary_consfont_start);
if (ttf_open(&consttf, data, len, 16) != 0)
return;
}
struct font *f = ttf_font(&consttf); struct font *f = ttf_font(&consttf);
if (!f) if (!f)