docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.
A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.
So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.
schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.
Three corrections found on the way, all load-bearing:
* projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
* The root README said "No TCP/IP" months after the stack landed.
* history/kernel.md sec.7 was marked "in progress" on a problem that process
groups solved.
Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
233 lines
11 KiB
Markdown
233 lines
11 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, `cmap` format 4,
|
||
anti-aliased scanline fill, with a host harness at `tools/ttftest.c`. A
|
||
direct-mapped glyph cache over ASCII in a 48 KB pool.
|
||
|
||
**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 still uses the 8×16 cell grid. Porting it onto the text layer and
|
||
deleting the grid is open work.
|
||
|
||
### 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.** The screen is the only artefact
|
||
of a panic — no core dump, no log to go back to — so 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. Anything left out is
|
||
gone. 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.
|
||
|
||
### 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.** The text layer exists and the console
|
||
does not use it; the rasteriser exists and no font is loaded from disk.
|
||
- **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 covers ASCII only**, direct-mapped. A codepoint outside it
|
||
is rasterised every time.
|
||
- **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.
|