From d85274dbd97e7959cf50216253e0ba1aae21318d Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Wed, 23 Sep 2026 17:47:45 +0200 Subject: [PATCH] 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 --- boot/uefi/main.c | 101 ++++++++++++++++++++++++++++++++++ docs/architecture/graphics.md | 37 +++++++++++++ docs/plan/1.0.md | 18 +++++- include/schboot.h | 23 ++++++++ kernel/dev/gfx.c | 41 +++++++++++++- 5 files changed, 215 insertions(+), 5 deletions(-) diff --git a/boot/uefi/main.c b/boot/uefi/main.c index d21f94d..c6b4a67 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -301,6 +301,10 @@ copy(void *dst, const void *src, UINTN n) // 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 * open_kernel(EFI_HANDLE image) { @@ -323,6 +327,8 @@ open_kernel(EFI_HANDLE image) st = fs->OpenVolume(fs, &root); 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); if (EFI_ERROR(st)) st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH_ALT, @@ -332,6 +338,95 @@ open_kernel(EFI_HANDLE image) 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 // kernel is linked at 0xffff800000100000 but must be *placed* at 0x100000, and // 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(); 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"); // Read while boot services still exist and the system table is guaranteed diff --git a/docs/architecture/graphics.md b/docs/architecture/graphics.md index a1aa51c..3f5c6e4 100644 --- a/docs/architecture/graphics.md +++ b/docs/architecture/graphics.md @@ -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 diff --git a/docs/plan/1.0.md b/docs/plan/1.0.md index 87bfebf..0133f25 100644 --- a/docs/plan/1.0.md +++ b/docs/plan/1.0.md @@ -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 diff --git a/include/schboot.h b/include/schboot.h index ba0aa9b..8baeee6 100644 --- a/include/schboot.h +++ b/include/schboot.h @@ -89,6 +89,29 @@ // #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__ struct sch64_header { diff --git a/kernel/dev/gfx.c b/kernel/dev/gfx.c index bd70234..f912f31 100644 --- a/kernel/dev/gfx.c +++ b/kernel/dev/gfx.c @@ -23,6 +23,9 @@ #include "gfx.h" #include "vt.h" #include "text.h" +#include "schboot.h" +#include "schboot.h" +#include "memlayout.h" #include "ttf.h" #define BACKSPACE 0x100 @@ -371,16 +374,50 @@ void gfx_font_init(void) { 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); struct glyph gl; if (cfont) 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 // 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; + 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; + 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); if (!f)