"Ą Ę Ś Ł Ż" 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>
304 lines
15 KiB
Markdown
304 lines
15 KiB
Markdown
# Graphics, text and the console
|
||
|
||
Four things share this area and they are deliberately separate: the kernel's own
|
||
drawing surface, the compositor's display interface, the text-rendering layer,
|
||
and the crash screen.
|
||
|
||
---
|
||
|
||
## The two display interfaces, and why there are two
|
||
|
||
This distinction causes confusion every time somebody meets it, so it is the
|
||
first thing here.
|
||
|
||
| | [`gfx.h`](../../include/gfx.h) | [`display.h`](../../include/display.h) |
|
||
|---|---|---|
|
||
| **What** | The kernel's own drawing surface | The compositor's interface |
|
||
| **About** | Pixels and text | *Frames* |
|
||
| **Operations** | `draw_pixel`, `fill_rect`, `putc`, `text_at` | `acquire`, `present`, `wait_vblank` |
|
||
| **Buffering** | None. Direct writes to the scanout. | Double-buffered |
|
||
| **Users** | The console and PSOGM | Schcom |
|
||
| **Must work when** | **Nothing else in the system does** | Userspace is healthy |
|
||
|
||
That last row is the reason they are not one interface. The console and the
|
||
crash screen cannot depend on a back buffer being allocatable, on a driver
|
||
having a working present path, or on anything that could be the thing that just
|
||
broke.
|
||
|
||
### `gfx` — the scanout
|
||
|
||
A driver that owns a linear framebuffer publishes it with
|
||
`gfx_attach_scanout()`, and everything above draws through `gfx_*` calls.
|
||
|
||
This used to be `kernel/dev/vga.c`, and every consumer called it by that name:
|
||
`panic.c` drew the crash screen with `vga_fill_rect`, the console printed with
|
||
`vga_putc`, `/dev/fb0` handed out `vga_fb_phys()`. None of that has anything to
|
||
do with a VGA — it is bitmap drawing over a linear framebuffer, and the only
|
||
VGA-specific part was where the framebuffer came from. A second display driver
|
||
is now a second caller of one function rather than an edit to the crash screen.
|
||
|
||
`main()` names no display driver. Whichever one owns the framebuffer the loader
|
||
left running says so before any bus is enumerated, because the console draws
|
||
long before PCI is walked.
|
||
|
||
### `display` — the class
|
||
|
||
`struct display_ops`, found with `device_find_class(DEVCLASS_DISPLAY)`.
|
||
|
||
`acquire()` allocates the back buffer **on first call, never at boot** — it is
|
||
around two megabytes at 800×600, and a machine whose compositor never starts
|
||
should not pay that. A driver that cannot double-buffer returns 0, and the
|
||
caller draws directly and accepts tearing rather than not drawing at all.
|
||
`wait_vblank` is optional for the same reason.
|
||
|
||
`echo d > /proc/sysrq` drives the whole path and draws a test pattern.
|
||
|
||
## Drivers
|
||
|
||
| | |
|
||
|---|---|
|
||
| **VBE framebuffer** (`gpu/vga/`) | Whatever the loader set up. Always present; this is what the splash and the console are drawn on. |
|
||
| **virtio-gpu** (`gpu/virtio_gpu/`) | 2D, written from the OASIS specification. Full path: `GET_DISPLAY_INFO`, `RESOURCE_CREATE_2D`, `RESOURCE_ATTACH_BACKING`, `SET_SCANOUT`, `TRANSFER_TO_HOST_2D`, `RESOURCE_FLUSH`. |
|
||
|
||
virtio-gpu is offered the bus **before** the VBE driver, so on a machine with
|
||
both, the device with real mode setting and an explicit flush wins the display
|
||
class. `vga_pci` still claims the stdvga card underneath, which is what the boot
|
||
splash is drawn on — so `-vga std` must stay on the QEMU command line even when
|
||
adding `-device virtio-gpu-pci`, because the loader's splash goes through VBE
|
||
and there is no virtio path that early.
|
||
|
||
`/dev/fb0` speaks the Linux fbdev interface: geometry through the fbdev ioctls,
|
||
and `mmap` of the real framebuffer at its real physical address.
|
||
|
||
> `mmap` of `/dev/fb0` is also **the kernel's only chance to notice that the
|
||
> screen no longer belongs to the text console** — after that the drawing
|
||
> happens with no further syscalls. `fb_mapped()` is called there, and
|
||
> `fbclose()` puts it back.
|
||
|
||
## Text
|
||
|
||
[`include/text.h`](../../include/text.h) + `kernel/dev/text.c`.
|
||
|
||
**The font was never the problem — the character-cell grid was.** The console
|
||
draws text by indexing `font_data[256][16]` and stepping the cursor eight
|
||
pixels. A grid cannot do variable-width glyphs, cannot position text anywhere
|
||
except on a multiple of eight, cannot anti-alias, and cannot represent a
|
||
codepoint above 255. Swapping the bitmap for a nicer bitmap changes none of that.
|
||
|
||
So the text layer fixes three things:
|
||
|
||
- **Glyphs have metrics** — size, bearing, advance. That *is* variable-width
|
||
text; everything else follows from it.
|
||
- **It draws into a `struct surface`, not into the screen.** A compositor draws
|
||
off-screen and presents; rendering that writes straight to the VGA aperture is
|
||
unusable to it.
|
||
- **The font is behind `struct font_ops`.** The bitmap font is one
|
||
implementation and a rasteriser is another, arriving without any caller
|
||
changing — glyphs carry 8-bit coverage, so an anti-aliased outline renderer
|
||
already has somewhere to put its output.
|
||
|
||
### The TrueType rasteriser
|
||
|
||
`kernel/dev/ttf.c`: `glyf` outlines, composite glyphs with their component
|
||
transforms, `cmap` format 4, anti-aliased scanline fill, with a host harness at
|
||
`tools/ttftest.c`. A direct-mapped glyph cache in a 64 KB pool: one reserved
|
||
slot per printable ASCII character, plus 224 shared slots keyed by codepoint
|
||
for everything above it.
|
||
|
||
**It is the console font.** The regular weight is linked into the kernel image
|
||
by the `consfont` rule in the Makefile and opened at 16 px before `vt_init()`,
|
||
which is why `gfx_font_init()` prints nothing — there is no terminal to print
|
||
to yet. A failure there is not fatal; the 8x16 bitmap font is still the
|
||
fallback, and `gfx_font_ready()` says which one is in use.
|
||
|
||
**Two bugs here were the same bug.** `KSTACKSIZE` is 4096, and both the render
|
||
path and the composite-glyph path put multi-kilobyte outlines on it as locals.
|
||
They corrupted whatever was below the stack, which surfaced as user processes
|
||
faulting on addresses they never touched and a panic inside `freevm()` — never
|
||
anywhere near a font. Neither showed in the host harness, which has an 8 MB
|
||
stack. That is the standing hazard of a rasteriser built from one source for
|
||
both: the host is most forgiving about exactly the resource the kernel is
|
||
strictest about. The outlines are `static` now, one per recursion level, and
|
||
`load_simple()` is split out so the leaf's 512-byte flag array is not
|
||
multiplied by the recursion depth.
|
||
|
||
**Component transforms are applied, not skipped.** An earlier version read a
|
||
composite component's offset and stepped over its scale, reasoning that a wrong
|
||
transform is worse than none. The left single quote in JetBrains Mono is the
|
||
comma glyph with a scale of -1 — a 180-degree rotation — and its offset only
|
||
makes sense after the rotation. Dropping the scale put the mark twelve pixels
|
||
into a ten-pixel cell, where it was clipped away completely, so the character
|
||
read as missing from the font rather than as misplaced.
|
||
|
||
**The typefaces**, both licence-checked at source:
|
||
|
||
| Font | Where | Licence |
|
||
|---|---|---|
|
||
| **JetBrains Mono** | terminal and TTY | SIL OFL 1.1 |
|
||
| **Roboto** | desktop and compositor | Apache-2.0 |
|
||
|
||
JetBrains Mono declares **no Reserved Font Name**, which matters more than it
|
||
sounds: pre-rasterising a font into bitmaps at build time is a modification, and
|
||
an RFN would forbid keeping the name on the result.
|
||
|
||
## The console
|
||
|
||
`kernel/core/console.c`. A cursor, scrolling, and an ANSI escape parser
|
||
including SGR colours and the erase operations.
|
||
|
||
**`cons_serial_only` starts set.** The display is the product; the serial line
|
||
is the diagnostics. A machine that boots by printing `pci: 6 devices` and
|
||
`vga: display controller 1234:1111, aperture fd000000` to the screen the user is
|
||
looking at is showing them a developer's log, and that single fact is most of
|
||
what makes an OS look like a hobby project. Everything still goes out the UART.
|
||
`console_takeover()` clears the flag on the first `exec()`.
|
||
|
||
The console uses a cell grid whose size comes from the console font's measured
|
||
advance rather than a hardcoded 8x16. Porting it onto the text layer's
|
||
proportional path and deleting the grid is open work.
|
||
|
||
Two things the ANSI parser got wrong until a large program nobody here wrote
|
||
exercised it properly:
|
||
|
||
**`ESC[m` with no parameters is a reset**, and `sgr()` iterated its parameter
|
||
list — which is empty in that case, so the loop body never ran and the reset was
|
||
dropped. ECMA-48 says an omitted parameter takes its default and SGR's default
|
||
is 0. Programs emit the bare form constantly, so every colour set became
|
||
permanent; with a *background* colour that meant the whole console went
|
||
white-on-white and stayed there, which reads as a corrupted framebuffer rather
|
||
than a missing branch.
|
||
|
||
**OSC sequences must be consumed even when they are ignored.** `ESC ]` was not a
|
||
state, so the payload printed as text and a hyperlinked path arrived as
|
||
`Disk (]8;;file:///\]8;;\)`. There is no window title to set here and no
|
||
hyperlink to follow, but discarding the body is not optional — the alternative
|
||
is printing somebody's URL into the middle of their output.
|
||
|
||
### Input
|
||
|
||
Keys reach both `/dev/input/eventN` and the tty line discipline, which is
|
||
correct for two consumers of one keyboard and completely wrong for a full-screen
|
||
program — the shell behind it quietly accumulates every keystroke and runs the
|
||
lot at the next Enter.
|
||
|
||
`EVIOCGRAB` is the fix and it is a primitive rather than a patch. Three details
|
||
matter more than the feature:
|
||
|
||
- **The grab is released by the `close` hook, not by the program.** A program
|
||
that grabs the keyboard and then crashes would otherwise leave the console
|
||
permanently deaf, with no way to type the command that fixes it.
|
||
- **Modifier state is still tracked while grabbed**, or releasing the grab hands
|
||
the line discipline a stuck Shift.
|
||
- `/dev/input/eventN` **drops its backlog on open** and only queues while
|
||
something has it open. Without that, opening the device replayed the
|
||
keystrokes of the command line that had launched the program.
|
||
|
||
## PSOGM — the crash screen
|
||
|
||
`kernel/core/panic.c`. The Purple Screen of Guru Meditation. Built around three
|
||
decisions:
|
||
|
||
**It does not clear the screen.** Whatever was displayed stays, dimmed and
|
||
pushed back, with the report floating over it. A crash screen that wipes the
|
||
display tells you the machine is gone; one that leaves your work visible tells
|
||
you it stopped — which is both truer and much less alarming.
|
||
|
||
**It is not frightening.** No red, no capitals, no "FATAL". A calm sentence, a
|
||
face that is obviously a cartoon, then the facts. Whoever is reading this is
|
||
already having a bad time.
|
||
|
||
**It says considerably more than it needs to.** Everything cheap to collect is
|
||
collected: stop code, fault address, which CPU, which process, how far into the
|
||
boot, where the kernel is loaded, and a call chain — and since
|
||
[debugging.md](debugging.md)'s symbol table, the call chain is *names*:
|
||
`panic+0x5f`, `proc_sysrq_cmd+0xb3`, `sys_writev+0x12b`. All of it goes to the
|
||
serial line in the same order, which is what `bootcheck` reads and what you get
|
||
when the crash happens before there is a framebuffer at all.
|
||
|
||
Once painted, `panicked` is set and `consputc` spins rather than writing, so
|
||
nothing can scribble over the report.
|
||
|
||
### It is no longer the only copy
|
||
|
||
This screen used to be the only artefact of a panic — this file said so, and so
|
||
did `panic.c`'s own header. That stopped being true when the kernel log started
|
||
outliving the kernel, and the closing lines changed to match:
|
||
|
||
```
|
||
This report is in /proc/kmsg after a restart.
|
||
schdbg, or Ctrl-G on the console, asks more.
|
||
PSOGM -- Purple Screen of Guru Meditation
|
||
```
|
||
|
||
The first line replaced an `addr2line` command, which the call chain no longer
|
||
needs. It is the more useful instruction by a wide margin, and it is the one
|
||
nobody would guess: **restart and read it** beats photographing the screen.
|
||
|
||
The QR code below is still drawn, and still earns its place — it is the only
|
||
channel that works when the machine will not come back at all.
|
||
|
||
### The QR code
|
||
|
||
`kernel/core/qr.c`. **A panic report is worth nothing if it cannot leave a
|
||
machine whose network stack is dead**, and a phone camera is an out-of-band
|
||
channel needing no driver on our side.
|
||
|
||
It encodes the **report itself**, not a URL — it reaches nothing. Deliberately
|
||
small in three ways: byte mode only, EC level L only (the screen is a clean,
|
||
evenly lit, high-contrast source; the damage QR's stronger levels protect
|
||
against does not apply), and versions 1–6 only, stopping short of version 7's
|
||
extra version-information blocks and version 10's wider count field.
|
||
|
||
134 bytes is a large crash report. If a payload ever needs more, the honest fix
|
||
is to shorten the report.
|
||
|
||
It draws unconditionally rather than on a keypress, which is better: it costs
|
||
nothing, and requiring input would mean the one case where input is least
|
||
trustworthy is also the case where the report is hardest to retrieve. Verified
|
||
by `make crashshot && make crashqr`, which decodes the symbol out of the
|
||
captured framebuffer.
|
||
|
||
## One palette
|
||
|
||
[`include/theme.h`](../../include/theme.h). There were three unrelated designs
|
||
and the seams showed: the splash was deep purple with gold text deliberately
|
||
matching the *panic* screen, the console was cream with near-black text, and so
|
||
a healthy boot was dressed as a crash and then abruptly became an amber
|
||
terminal.
|
||
|
||
> **The rule: normal states are calm and dark; only failure is loud.**
|
||
|
||
Boot, console and shell share `SCH_BG`/`SCH_FG` — near-black with a slight blue
|
||
cast rather than pure `#000`, because pure black on an LCD reads as "the machine
|
||
is off". PSOGM keeps the full-bleed purple entirely to itself, which is what
|
||
makes it mean something.
|
||
|
||
---
|
||
|
||
## What it does not do
|
||
|
||
- **The console still has its cell grid.** Cells are now a Unicode codepoint
|
||
drawn by the TrueType rasteriser rather than a CP437 byte drawn from a bitmap,
|
||
and the cell size follows the font's measured advance — but it is still a
|
||
fixed grid, and the text layer's proportional path is unused by it.
|
||
- **No mode setting.** Resolution is whatever the loader asked for. There is no
|
||
`/dev/dri`-style device and no way to change it at runtime.
|
||
- **No double buffering in practice.** `display_ops.acquire` is specified and
|
||
the compositor needs it; virtio-gpu binds but exposes no modeset API, and
|
||
`wait_vblank` is unimplemented everywhere.
|
||
- **No hardware acceleration of any kind**, and none is planned before 2D blit.
|
||
- **No CFF/Type 2 charstrings**, so OTF fonts do not render — that is a separate
|
||
`font_ops` provider and a bytecode interpreter, not an extension of the `glyf`
|
||
path. TrueType hinting is deliberately out of scope.
|
||
- **No text layout library.** Advance widths exist per glyph; kerning, shaping
|
||
and line breaking do not.
|
||
- **The glyph cache has 224 slots above ASCII**, direct-mapped by codepoint
|
||
modulo that. One European language fits without collisions; mixing scripts
|
||
costs collisions, and a full pool is emptied and refilled rather than evicted
|
||
entry by entry, because a linear pool cannot reclaim one glyph's bytes.
|
||
- **No composite point-matching.** Components positioned by matching a point in
|
||
the base rather than by an offset are placed at the origin. Fonts that use it
|
||
are rare and none in the tree do.
|
||
- **PSOGM renders and stops.** The interactive crash mode described in
|
||
[../plan/roadmap.md](../plan/roadmap.md) — walk the stack, read another
|
||
process's memory, dump page tables — is designed and not built.
|