kernel: route routine status to the log, not the framebuffer console

Now that the user-space display service owns the framebuffer, the bootstrap
console shrinks to fatal-only. status/statusPrint go to the diagnostic log
alone, so routine boot output no longer scribbles on a screen the compositor is
about to paint — and a driver's recoverable fault report stays off it too. A new
fatal/fatalPrint keeps panics and kernel-mode faults on screen, forcing the
console back on so a dying machine's last words show even over a live display.
console.zig now documents its early-boot + fatal-fallback role.
This commit is contained in:
Daniel Samson
2026-07-14 08:26:55 +01:00
parent 88ed3c5417
commit c3e9c59086
2 changed files with 44 additions and 27 deletions
+9 -6
View File
@@ -2,12 +2,15 @@
//! into the linear framebuffer the bootloader handed us. No firmware, no driver //! into the linear framebuffer the bootloader handed us. No firmware, no driver
//! — just pixels. //! — just pixels.
//! //!
//! This is a **bootstrap** console — a stop-gap so early boot has something on //! This is a **bootstrap / fatal-fallback** console. The driver machinery now exists — the
//! screen. The framebuffer is a general graphics surface, *not* inherently a text //! user-space **display service** ([../services/display](../services/display/display.zig),
//! terminal; once the driver machinery exists it becomes a proper graphics device //! docs/display.md) owns the framebuffer in normal operation — so this no longer paints
//! driver and this text-grid crutch goes away. It is therefore kept **separate //! routine status. It exists for the two cases the display service can't cover: **early
//! from the diagnostic [log](log.zig)** — the log fans out to serial/debugcon/file, //! boot**, before the service has claimed the framebuffer, and **fatal errors** (a kernel
//! while this only paints the handful of user-facing status lines and panics. //! panic or a kernel-mode fault), which force it back on (`setSuppressed`) so a dying
//! machine's last words reach the screen even over a live display. It is kept **separate
//! from the diagnostic [log](log.zig)** — the log fans out to serial/debugcon/file and
//! carries all routine kernel output; this only paints those fatal cases.
//! //!
//! The module owns a single console and a `present` flag; `write` is a no-op when //! The module owns a single console and a `present` flag; `write` is a no-op when
//! the firmware handed over no framebuffer (a headless machine), so the kernel //! the firmware handed over no framebuffer (a headless machine), so the kernel
+35 -21
View File
@@ -156,12 +156,13 @@ fn kmain(boot_information: *const BootInformation) noreturn {
log.print(" page tables: root = 0x{x:0>16}\n", .{architecture.activePageTable()}); log.print(" page tables: root = 0x{x:0>16}\n", .{architecture.activePageTable()});
log.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_information.kernel_segment_count}); log.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_information.kernel_segment_count});
// Now on our own tables, the framebuffer window is write-combining: bring up // Now on our own tables, the framebuffer window is write-combining: bring up the
// the on-screen console and clear it (a fast burst here, not the loader's // on-screen console and clear it to a blank canvas (a fast burst here, not the loader's
// uncached crawl). From here `status` reaches the screen as well as the log. // uncached crawl). Routine boot output goes only to the log; this console now exists for
// early-boot and fatal (`fatal`/panic) output, until the display service takes over.
console.init(fb); console.init(fb);
log.write(if (console.present()) log.write(if (console.present())
"/system/kernel: framebuffer console online (bootstrap; graphics driver later)\n" "/system/kernel: framebuffer ready (early-boot + fatal fallback; the display service drives it in normal operation)\n"
else else
"/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n"); "/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n");
@@ -414,11 +415,22 @@ fn bringUpSecondaries() void {
log.print("/system/kernel: {d}/{d} cores online\n", .{ scheduler.onlineCount(), cores.len }); log.print("/system/kernel: {d}/{d} cores online\n", .{ scheduler.onlineCount(), cores.len });
} }
/// A user-facing status line: to the diagnostic `log` *and* the on-screen console /// A user-facing status line. Now that the user-space **display service** owns the
/// (if a framebuffer is present). The verbose log uses `log.*` directly and never /// framebuffer in normal operation (docs/display.md), routine kernel output goes to the
/// touches the framebuffer. /// diagnostic `log` (serial/debugcon/RAM) *only* — never to the on-screen console, which
/// the compositor is about to paint over. For a message that must reach the screen even so
/// — a panic or a fatal fault, when the machine is going down — use `fatal`.
fn status(message: []const u8) void { fn status(message: []const u8) void {
log.write(message); log.write(message);
}
/// A fatal, user-facing message: to the diagnostic log *and* the on-screen console, forcing
/// the console back on (`setSuppressed(false)`) first — a dying machine's last words outrank
/// any display service holding the framebuffer. The console is otherwise silent in normal
/// operation (see `status`); it exists now only for early-boot and fatal output.
fn fatal(message: []const u8) void {
log.write(message);
console.setSuppressed(false);
console.write(message); console.write(message);
} }
@@ -427,6 +439,11 @@ fn statusPrint(comptime fmt: []const u8, args: anytype) void {
status(std.fmt.bufPrint(&buffer, fmt, args) catch return); status(std.fmt.bufPrint(&buffer, fmt, args) catch return);
} }
fn fatalPrint(comptime fmt: []const u8, args: anytype) void {
var buffer: [256]u8 = undefined;
fatal(std.fmt.bufPrint(&buffer, fmt, args) catch return);
}
/// Frames (4 KiB pages) to whole MiB. /// Frames (4 KiB pages) to whole MiB.
fn mib(pages: u64) u64 { fn mib(pages: u64) u64 {
return pages * abi.page_size / (1024 * 1024); return pages * abi.page_size / (1024 * 1024);
@@ -486,17 +503,15 @@ fn onException(state: *const architecture.CpuState) noreturn {
} }
log.checkpoint(cp_exception); log.checkpoint(cp_exception);
// The machine is going down: force the console back on even if a display service
// was holding the framebuffer, so the exception actually reaches the screen.
console.setSuppressed(false);
const core = scheduler.currentCpuIndex(); const core = scheduler.currentCpuIndex();
// A fault is user-facing enough to paint on screen too (via statusPrint), on // The machine is going down: paint the exception on screen too — `fatalPrint` forces the
// top of the diagnostic log. // console back on even if a display service was holding the framebuffer — on top of the
statusPrint("\nCPU EXCEPTION on core {d}: {s} (vector {d})\n", .{ core, architecture.exceptionName(state.vector), state.vector }); // diagnostic log.
statusPrint(" error code : 0x{x}\n", .{state.error_code}); fatalPrint("\nCPU EXCEPTION on core {d}: {s} (vector {d})\n", .{ core, architecture.exceptionName(state.vector), state.vector });
statusPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)}); fatalPrint(" error code : 0x{x}\n", .{state.error_code});
statusPrint(" SP : 0x{x:0>16}\n", .{architecture.stackPointer(state)}); fatalPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
if (architecture.faultAddress(state)) |address| statusPrint(" fault addr : 0x{x:0>16}\n", .{address}); fatalPrint(" SP : 0x{x:0>16}\n", .{architecture.stackPointer(state)});
if (architecture.faultAddress(state)) |address| fatalPrint(" fault addr : 0x{x:0>16}\n", .{address});
var buffer: [128]u8 = undefined; var buffer: [128]u8 = undefined;
log.recordPanic(std.fmt.bufPrint(&buffer, "CPU exception {s} (vector {d}) on core {d} at IP 0x{x}", .{ architecture.exceptionName(state.vector), state.vector, core, architecture.instructionPointer(state) }) catch "cpu exception"); log.recordPanic(std.fmt.bufPrint(&buffer, "CPU exception {s} (vector {d}) on core {d} at IP 0x{x}", .{ architecture.exceptionName(state.vector), state.vector, core, architecture.instructionPointer(state) }) catch "cpu exception");
@@ -511,10 +526,9 @@ pub const panic = std.debug.FullPanic(struct {
_ = first_trace_address; _ = first_trace_address;
log.checkpoint(cp_panic); log.checkpoint(cp_panic);
log.recordPanic(message); log.recordPanic(message);
console.setSuppressed(false); // a panic outranks any display service holding the screen fatal("\nKERNEL PANIC: "); // a panic outranks any display service holding the screen
status("\nKERNEL PANIC: "); fatal(message);
status(message); fatal("\n");
status("\n");
architecture.halt(); architecture.halt();
} }
}.panic); }.panic);