# 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. ### 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 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.