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:
parent
df304918f8
commit
d85274dbd9
5 changed files with 215 additions and 5 deletions
101
boot/uefi/main.c
101
boot/uefi/main.c
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue