# Logging: the diagnostic log vs. the display danos separates two things that are easy to conflate: the **diagnostic log** — the machine-readable stream of *what the kernel is doing* — and the **display**, the framebuffer surface the OS draws on. They are different concerns with different lifetimes, so they're different code paths. The guiding rule: **output is a diagnostic convenience, never a correctness dependency.** The kernel must boot and run correctly with *zero* output channels — no serial, no screen. Logging that can take the kernel down isn't robust; it's a liability. This is the same [resilience](resilience.md) posture the rest of the kernel follows. ## The log is multi-sink `system/kernel/log.zig` is the diagnostic log. It fans a message out to a set of registered **sinks**, each best-effort and self-guarding: ```zig log.addSink(arch.serialWrite); // the serial UART if (arch.debugconPresent()) log.addSink(arch.debugconWrite); // 0xE9 debug console // later: log.addSink(fs.logWrite); // a file on a ramdisk / USB / SSD log.write("…"); log.print("x={d}\n", .{x}); ``` Properties that matter: - **No allocation.** The sink table is a fixed array, so the log works before the heap is up and inside a panic. - **Best-effort.** A sink whose device is absent is a no-op (e.g. writing to a missing UART just goes nowhere — the TX-wait is bounded so it can't hang). A message reaches whatever channels exist; if none do, the kernel runs on, silent. - **Order-independent.** Every registered sink gets every message. Adding the file logger later is one `addSink` call and **zero** changes to call sites. ## The framebuffer is *not* a log sink The framebuffer is a general graphics surface, **not inherently a text terminal**. Today `system/kernel/console.zig` paints a text grid on it as a *bootstrap* console, but that's a stop-gap: once the driver machinery exists the framebuffer becomes a proper **graphics device driver**, and the text crutch goes away. So the log must not assume it — routing the verbose log through a pixel console would bake in "the OS is text". Instead the two paths are explicit: ``` verbose diagnostics ──► log ──► serial, debugcon, (file later) user status / panics ──► status() ──► log (above) + framebuffer (if present) ``` A handful of user-facing lines (`kernel initialised`, a panic) go through `main.zig`'s `status()` / `statusPrint()`, which write to the log **and** paint the framebuffer when one is present. Everything else uses `log.*` and never touches the screen. `console.write` is a no-op when the firmware gave us no framebuffer. ## Optional framebuffer (headless machines) A framebuffer is not guaranteed — a headless server exposes no UEFI Graphics Output Protocol. That used to be *fatal* (the loader failed the boot). Now the loader hands over a "no framebuffer" descriptor (`base == 0`) rather than failing, and `Framebuffer.present()` (in `system/boot-handoff.zig`) gates every on-screen path. A headless, serial-less machine boots and runs correctly — it just goes quiet. ## Last-resort channels (no text output at all) Two signals bypass the sink list, because they must survive even a total output-channel failure: - **`log.checkpoint(code)`** — a one-byte **POST code** to I/O port `0x80` (a POST card or BMC shows it). `main.zig` emits one at each boot milestone (`cp_paging`, `cp_heap`, …) and on a fault/panic, so "where did it hang?" is answerable with no text output whatsoever. Writing `0x80` is universally safe. - **`log.recordPanic(msg)`** — stamps the panic message into a fixed record (`log.panic_record`, with a `magic` written last). A post-mortem — an attached debugger, a RAM dump, or a future file/pstore reader — recovers *what killed it* even though nothing was on screen. The panic and CPU-exception handlers fan out to every sink, emit a POST code, and drop the breadcrumb — they never assume a console. ## The 0xE9 debug console Port `0xE9` is the Bochs/QEMU debug console. It's detected safely: the port returns `0xE9` when read if present, and `0xFF` on real hardware, so `debugconPresent()` only enables the sink when it's really there. Under QEMU it's captured with `-debugcon file:…`, giving CI a log channel independent of `-serial`. ## The robustness spectrum The result handles every combination — framebuffer only, serial only, both, or **neither**. With no channels at all the kernel still boots and runs; port-`0x80` checkpoints track progress and the panic breadcrumb captures failures. *Runs blind but correct* is the goal, not *always has output*. ## Related - [framebuffer.md](framebuffer.md) — the display surface itself (pitch, format), the thing that becomes a graphics device driver. - [efi.md](efi.md) — where the loader captures (or, headless, doesn't capture) the framebuffer before `ExitBootServices`. - [device-interrupts.md](device-interrupts.md) — the serial UART bring-up the log's primary sink rides on. - [resilience.md](resilience.md) — why "never let a missing peripheral take the kernel down" is a core design stance.