Two things, both asked for. **The crash report no longer says which monitors it reached.** "The report is on the virtio-gpu display too" was a line about the crash screen printed on the crash screen. The report says what happened and where to read it again; that is not part of it. **And the second head now shows the F8 menu and the boot.** It used to be black until vgpud started several seconds into userspace -- through the splash, through the menu, through every boot message -- which from the outside is indistinguishable from a display that does not work. Three stages now cover the whole boot and hand over to each other without coordinating: *The loader.* LocateProtocol() answers with the first GOP and offers no way to ask for the rest, so the rest are found by handle and fed the primary's framebuffer as a Blt source at the points where a screen is finished: the menu, the resolution chooser, system information, the splash, each segment of the kernel load. Blt rather than a second framebuffer to draw into, because a virtio GOP does not have one -- the same fact that made a virtio-only machine reboot-loop two commits ago. *The kernel.* gfx remembers which rows were drawn, as one span rather than a list of rectangles, because console output is lines and a span is what lines make. display_mirror_console() copies that span across: a line of boot text is one cell-row transfer, not a screen. Primed once after pci_init(), because a display that has just appeared has a black scanout and the console it should be showing was drawn by the loader into a framebuffer it knows nothing about. *Then vgpud*, from the same console framebuffer, unchanged. **Two bugs found building it, both worth the retelling.** The mirror ran per character from inside vt_feed -- which is inside the console lock -- and vga_disp_acquire() announces its back buffer with a cprintf. A CPU taking a lock it already holds, with a call chain that said exactly that. It runs at the end of consolewrite() and cprintf() now, outside the lock, which is also the better batching: one transfer per write instead of one per character. And "is this the console's own screen" was asked of the pointer acquire() returned. vga_fb hands back a back buffer it copies onto the console's framebuffer afterwards, so on a one-screen machine the answer was "a different screen" -- and the console was mirrored onto itself through two megabytes of intermediate, allocated for the purpose. display_ops grew scanout_phys, which is a fact about the hardware rather than a policy a driver can get subtly wrong, and it is asked first: it has no side effects and acquire() has two. Verified with two heads under QEMU, screendumping the virtio head every 400 ms from power-on: splash, splash, splash, then the console, and it stays. The F8 menu appears on it; choosing 1024x768 there brings both heads up at 1024x768 byte-identical. A panic still reaches both. Both supported configurations: one boot each, no traps, no panics, socktest and memfdtest green. One gap remains and is documented rather than hidden: while the kernel's driver resets the device during probe, QEMU shows "Display output is not active" for a fraction of a second. A device being taken over has no scanout until the new driver sets one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
502 lines
26 KiB
Markdown
502 lines
26 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.
|
||
|
||
## What the second head shows
|
||
|
||
A machine with both a stdvga and a virtio-gpu has two displays, and `vgpud` —
|
||
the out-of-kernel driver, `cmd/vgpud/main.c` — owns the second one. For a long
|
||
time it drew a test card there: colour bands, gradients, a checkerboard, and
|
||
its own pid in binary along the bottom. That was scaffolding for
|
||
[../plan/hybrid.md](../plan/hybrid.md)'s H2 and H3, where a screenshot had to
|
||
answer *did a userspace process put this here* and *did the driver come back*.
|
||
|
||
Both of those are closed, so the test card became the Shift mark. Which was
|
||
honest about ownership and useless about everything else: **a second monitor
|
||
showing a logo for ever is indistinguishable from a second monitor that does
|
||
not work**, and that is exactly what it got called.
|
||
|
||
It shows the console now.
|
||
|
||
### Before vgpud there is, the kernel and the loader do it
|
||
|
||
`vgpud` starts several seconds into userspace. Until then the second head used
|
||
to be black — through the splash, through the F8 menu, through every boot
|
||
message — which from the outside is indistinguishable from a display that does
|
||
not work. Three stages now cover the whole boot, and they hand over to each
|
||
other without coordinating:
|
||
|
||
**The loader.** `LocateProtocol()` answers with the first GOP and offers no way
|
||
to ask for the rest, so the rest are found by handle (`LocateHandleBuffer`,
|
||
`ByProtocol`). Each is fed the *primary's framebuffer* as a `Blt` source, at
|
||
the points where a screen is finished: the F8 menu, the resolution chooser,
|
||
system information, the splash, each segment of the kernel load. Blt rather
|
||
than a second framebuffer to draw into, because a virtio GOP does not have one
|
||
— the same fact that made a virtio-only machine reboot-loop.
|
||
|
||
**The kernel.** `display_mirror_console()` copies the console's newly-drawn
|
||
rows to the display class whenever that is a *different screen*. Cheap because
|
||
`gfx` remembers which rows those were: a line of boot text is one cell-row
|
||
transfer, not a screen.
|
||
|
||
Two things about that were learned the hard way, and both are load-bearing:
|
||
|
||
- **It runs outside the console lock**, at the end of `consolewrite()` and
|
||
`cprintf()` rather than per character inside them. A display driver is
|
||
entitled to say something — `vga_disp_acquire()` announces its back buffer —
|
||
and `cprintf()` from inside that lock is a CPU taking a lock it already
|
||
holds. The panic's call chain said so plainly, which is the argument for
|
||
keeping the call chain in the report.
|
||
- **"Is this the console's own screen" is asked of the scanout**, not of the
|
||
buffer `acquire()` returns. `vga_fb` hands back a back buffer that it later
|
||
copies onto the console's framebuffer, so comparing pointers answered "a
|
||
different screen" about the only screen there is — and mirrored the console
|
||
onto itself through two megabytes of intermediate. `display_ops` grew
|
||
`scanout_phys` to answer it as a fact about the hardware.
|
||
|
||
**Then vgpud**, from the same console framebuffer, by the mechanism below. The
|
||
handover needs no coordination in either direction: the moment `vgpud` claims
|
||
the device the kernel's `display_acquire()` returns nothing, and if `vgpud`
|
||
exits the kernel's driver takes it back and the first line of output repaints
|
||
the screen.
|
||
|
||
There is one visible gap, and it is honest: while the kernel's driver resets
|
||
the device during probe, QEMU shows *"Display output is not active"* for a
|
||
fraction of a second. A device being taken over has no scanout until the new
|
||
driver sets one, and the same blink happens when `vgpud` claims it.
|
||
|
||
### A copy with a diff
|
||
|
||
The console framebuffer is mapped through `/dev/fb0`; the virtio backing store
|
||
is already mapped, because `vgpud` allocated it. So the backing holds, at all
|
||
times, exactly what was last sent to the device — which means it *is* the
|
||
previous frame, and answering "what changed" needs no third buffer. Ten times
|
||
a second: compare each row, skip the ones that match, copy the ones that do
|
||
not, and transfer the single rectangle spanning the rows that changed.
|
||
|
||
This is why `present()` became `present_rect()`. Moving one line of text with a
|
||
full-screen transfer of a 1280×800 resource is most of a megabyte of copy for a
|
||
few hundred bytes of difference, and the loop runs whether or not anything is
|
||
happening.
|
||
|
||
Mapping `/dev/fb0` does not take the screen away from the console — the
|
||
mapping is of the same physical pages the console draws into, and the kernel's
|
||
`fb_mapped()` only arranges a redraw when the fd is *closed*. `vgpud` never
|
||
closes it.
|
||
|
||
A head larger than the console centres it on the theme ground. A head smaller
|
||
shows the top-left corner, which is where a terminal puts everything worth
|
||
seeing. Scaling is the alternative and it is the wrong one: text resampled to a
|
||
non-integer ratio is text you cannot read.
|
||
|
||
**This is not the endpoint.** A compositor (Gate 3) will want this head for
|
||
itself, and when there is one, the mirror goes away. Until then the honest
|
||
thing for a working display to show is the system running on it.
|
||
|
||
### The F8 resolution is the head's resolution
|
||
|
||
`vgpud` used to size itself from `GET_DISPLAY_INFO`, which answers with the
|
||
size of the *host's window* — a number the person running QEMU sets by dragging
|
||
a corner, and which says nothing about the machine. The resolution the F8 menu
|
||
offered at boot is the one that was actually chosen, and it is the console's.
|
||
|
||
A head that mirrors the console and ignores that choice shows the right pixels
|
||
in the wrong mode: pick 1024×768 at the menu and the second display stayed at
|
||
whatever the window was, with the console letterboxed inside it. So
|
||
`console_open()` runs *before* `bring_up()`, and the mode comes from the
|
||
console when there is one.
|
||
|
||
That ordering is the whole of it, and it is not cosmetic: the DMA allocation is
|
||
sized from those numbers and fixed for the life of the process, so a mode
|
||
chosen after the fact could only ever be smaller than the window that was
|
||
already paid for.
|
||
|
||
### /run is not a tmpfs
|
||
|
||
Found while testing the above, and older than it: `schmode` reported *"cannot
|
||
reach vgpud on /run/vgpud — it is not running"* about a process that was
|
||
running perfectly well, on every boot after the first.
|
||
|
||
`bind()` creates a node at the path, and `/run` is a directory on the root
|
||
filesystem rather than a tmpfs. So the node outlived the process, and the boot.
|
||
The next boot's `vgpud` claimed the device, drew, held it — and failed `bind()`
|
||
on a name that already existed, leaving a driver nobody could talk to and an
|
||
error message that named the wrong cause.
|
||
|
||
`vgpud` unlinks the path before binding it, which is what a Unix daemon does.
|
||
The usual objection — that it lets a second instance steal a live socket — does
|
||
not apply: two `vgpud`s cannot coexist, because the first holds the PCI claim
|
||
and the second is refused the device long before it reaches `bind()`. The claim
|
||
is the mutual exclusion; the socket node never was.
|
||
|
||
## 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 reaches every display, including the ones the kernel gave away
|
||
|
||
The report used to go to the console framebuffer and stop there. On a machine
|
||
with two heads that meant one screen saying the kernel was dead and another
|
||
showing a shell prompt from a few seconds earlier — the most misleading thing
|
||
a screen can do.
|
||
|
||
`display_panic_present()` in `kernel/dev/device.c` walks the rest of them, and
|
||
there are two cases:
|
||
|
||
**The kernel still drives it.** `display_acquire()` hands back a buffer; the
|
||
report is copied in and presented. This is the ordinary path, and it does
|
||
nothing at all when the display class's buffer *is* the console's — a machine
|
||
with one screen does not get the work done twice.
|
||
|
||
**A process drives it.** Harder, and the one worth having. The kernel let go of
|
||
the device deliberately, and the process holding it will never run again —
|
||
there is no scheduling after a panic. So `vgpud` registers its scanout in
|
||
advance (`PCIIOC_PANICFB`), the kernel paints the report into that memory, and
|
||
the driver the device was taken from is asked to put it on screen one last
|
||
time: `panic_present` in `struct pci_driver`.
|
||
|
||
For virtio-gpu that means resetting the device out from under a driver that is
|
||
still nominally running — safe here and nowhere else, because nothing is going
|
||
to run again — and rebuilding one queue from 20 KB of static memory. No
|
||
allocation, no locks, no sleeping, and every spin bounded: a crash screen that
|
||
hangs has taken away the serial log's last chance to be read.
|
||
|
||
Neither announces itself on the serial line. The report says what happened and
|
||
where to read it again; which monitors it reached is not part of that, and a
|
||
crash screen is not the place to narrate the crash screen.
|
||
|
||
### 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.
|