Booting with a virtio-gpu as the *only* display reboot-looped. Not a hang, not
a panic: the firmware came back round to the boot menu, started the loader
again, and did it again, for ever, with nothing to say why.
virtio-gpu has no linear aperture, so OVMF's virtio GOP implements Blt and
nothing else. On a machine with one and nothing else, the firmware reports a
width and a height and a FrameBufferBase of **zero**:
schos: framebuffer 0x0000000000000000 0x500x0x320
Two places took that at face value.
**kmain() tested `if (bi.ptr && ...)` with bi.ptr already run through P2V()**,
and P2V(0) is KERNBASE, which is not zero. So the guard could not fail -- and
its own comment, three lines above, said "drawing into address zero is a worse
failure than staying quiet". The intent was exactly right and the test could
never enforce it. It reads the physical value first now and tests that.
**vga_fb_init() attached the scanout regardless**, one layer down, so fixing
kmain alone just moved the same write.
Either one had gfx_clear() write four megabytes over physical 0: the real-mode
IVT, the BIOS data area, the AP trampoline at 0x7000, the early page tables.
Hence a reset with no output -- there was nothing left to run.
Both refuse now, and the refusal says what it is:
vga: firmware reported a 1280x800 display with no framebuffer address;
no graphical console
A machine with no usable linear framebuffer gets no graphical console. That is
the truth about it, and the serial line still has everything.
**It still does not reach a shell on such a machine**, and that is recorded
rather than implied: the console path assumes a framebuffer -- vt_init()
reports a terminal of `0 by 0` cells -- and that path has never been exercised.
What this fixes is a machine that destroyed low memory and reset; what it does
not do is make a headless boot work. Supported remains stdvga, with virtio-gpu
optional beside it, which is what docs/architecture/graphics.md already said.
Both supported configurations verified: stdvga alone and stdvga with
virtio-gpu, one boot each, no traps, socktest and memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
343 lines
17 KiB
Markdown
343 lines
17 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.
|
||
|
||
### A framebuffer at physical zero is not a framebuffer
|
||
|
||
That last sentence used to be advice. It is now enforced, because ignoring it
|
||
did not produce a machine with no splash — it produced a **reboot loop**.
|
||
|
||
virtio-gpu has no linear aperture, so OVMF's virtio GOP implements `Blt` and
|
||
nothing else. On a machine whose only display is one, the firmware reports a
|
||
width and a height and a `FrameBufferBase` of **zero**. Two places took that at
|
||
face value:
|
||
|
||
- `kmain()` tested `if (bi.ptr && …)` — with `bi.ptr` already run through
|
||
`P2V()`, and `P2V(0)` is `KERNBASE`, which is not zero. The guard could not
|
||
fail, and its own comment said *"drawing into address zero is a worse failure
|
||
than staying quiet"*. It now tests the physical value before translating it.
|
||
- `vga_fb_init()` attached the scanout regardless, one layer down.
|
||
|
||
Either one wrote four megabytes over physical 0 — the real-mode IVT, the BIOS
|
||
data area, the AP trampoline at `0x7000`, the early page tables — and the
|
||
machine reset before it could print anything, came back to the firmware, and
|
||
did it again.
|
||
|
||
Both refuse now and say so. A machine with no usable linear framebuffer gets no
|
||
graphical console, which is the truth about it; the serial line still has
|
||
everything.
|
||
|
||
**A virtio-gpu-only machine still does not reach a shell.** It no longer
|
||
corrupts memory and it explains itself, but the console path assumes a
|
||
framebuffer — `vt_init()` reports a terminal of `0 by 0` cells — and that path
|
||
has never been exercised. Supported today is stdvga, with virtio-gpu optional
|
||
beside it.
|
||
|
||
`/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 takes the whole screen.** It used to dim what was there and float a panel
|
||
over it, on the argument that a crash screen which wipes the display tells you
|
||
the machine is gone while one that leaves your work visible tells you it
|
||
stopped. That looked good and was wrong: the thing is called a Purple Screen,
|
||
and half a screen of abandoned work behind the report is a distraction at the
|
||
worst possible moment.
|
||
|
||
**It is not frightening.** No red, no capitals, no "FATAL". A calm sentence,
|
||
the Shift mark — the same one the machine drew on the splash it started with —
|
||
then the facts. Whoever is reading this is already having a bad time.
|
||
|
||
The mark replaced **Guru**, a chamfered face drawn from primitives that lived
|
||
only here. Guru's argument was identity: `:(` is recognisably Microsoft's, and
|
||
a crash screen is the screenshot most likely to leave the machine. That holds.
|
||
What does not follow is that the answer is a *second* mark — see
|
||
[../branding/README.md](../branding/README.md).
|
||
|
||
**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.
|