SchOS/docs/architecture/graphics.md
Nexoniarz 415df0fd4d debugger: reachable from the console, and named on the crash screen
Two things, both about the debugger being available at the moment you
actually need it rather than once a shell exists.

**Ctrl-G on the console runs every view.** No filesystem, no /dev, no
shell -- it is the last thing main() sets up before entering the scheduler,
so it answers from early in boot, which is exactly when boot problems
happen. It sits beside the older Ctrl-P, and the difference between them is
the interesting part: procdump() takes no lock at all, deliberately,
because a lock is a way to hang while reporting a hang. The views cannot
follow that rule -- a free list read torn is worse than not read -- so they
take locks, and a lock must never be taken from an interrupt handler that
may have interrupted its holder. The key therefore *requests* and a kernel
thread *performs*. A scheduler too wedged to run a thread will not answer
Ctrl-G; Ctrl-P still will, and that division is the honest one.

**PSOGM says where the report went.** Its closing line used to be an
addr2line command, which the call chain no longer needs -- it prints names
now. What replaced it is the thing nobody would guess:

    This report is in /proc/kmsg after a restart.
    schdbg, or Ctrl-G on the console, asks more.
    PSOGM  --  Purple Screen of Guru Meditation

"Restart and read it" beats "photograph this screen" by a wide margin, and
until the log outlived the kernel it was not true. Both panic.c and
graphics.md had claimed in writing that the screen was the only artefact of
a crash; neither says so now. The QR code stays, because it is still the
only channel that works when the machine will not come back at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 01:29:30 +02:00

269 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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