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
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
2 changed files with 44 additions and 27 deletions

View File

@ -2,12 +2,15 @@
//! into the linear framebuffer the bootloader handed us. No firmware, no driver
//! just pixels.
//!
//! This is a **bootstrap** console a stop-gap so early boot has something on
//! screen. The framebuffer is a general graphics surface, *not* inherently a text
//! terminal; once the driver machinery exists it becomes a proper graphics device
//! driver and this text-grid crutch goes away. It is therefore kept **separate
//! from the diagnostic [log](log.zig)** the log fans out to serial/debugcon/file,
//! while this only paints the handful of user-facing status lines and panics.
//! This is a **bootstrap / fatal-fallback** console. The driver machinery now exists the
//! user-space **display service** ([../services/display](../services/display/display.zig),
//! docs/display.md) owns the framebuffer in normal operation so this no longer paints
//! routine status. It exists for the two cases the display service can't cover: **early
//! boot**, before the service has claimed the framebuffer, and **fatal errors** (a kernel
//! 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 firmware handed over no framebuffer (a headless machine), so the kernel

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(" 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
// the on-screen console and clear it (a fast burst here, not the loader's
// uncached crawl). From here `status` reaches the screen as well as the log.
// Now on our own tables, the framebuffer window is write-combining: bring up the
// on-screen console and clear it to a blank canvas (a fast burst here, not the loader's
// 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);
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
"/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 });
}
/// A user-facing status line: to the diagnostic `log` *and* the on-screen console
/// (if a framebuffer is present). The verbose log uses `log.*` directly and never
/// touches the framebuffer.
/// A user-facing status line. Now that the user-space **display service** owns the
/// framebuffer in normal operation (docs/display.md), routine kernel output goes to the
/// 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 {
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);
}
@ -427,6 +439,11 @@ fn statusPrint(comptime fmt: []const u8, args: anytype) void {
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.
fn mib(pages: u64) u64 {
return pages * abi.page_size / (1024 * 1024);
@ -486,17 +503,15 @@ fn onException(state: *const architecture.CpuState) noreturn {
}
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();
// A fault is user-facing enough to paint on screen too (via statusPrint), on
// top of the diagnostic log.
statusPrint("\nCPU EXCEPTION on core {d}: {s} (vector {d})\n", .{ core, architecture.exceptionName(state.vector), state.vector });
statusPrint(" error code : 0x{x}\n", .{state.error_code});
statusPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
statusPrint(" SP : 0x{x:0>16}\n", .{architecture.stackPointer(state)});
if (architecture.faultAddress(state)) |address| statusPrint(" fault addr : 0x{x:0>16}\n", .{address});
// The machine is going down: paint the exception on screen too `fatalPrint` forces the
// console back on even if a display service was holding the framebuffer on top of the
// diagnostic log.
fatalPrint("\nCPU EXCEPTION on core {d}: {s} (vector {d})\n", .{ core, architecture.exceptionName(state.vector), state.vector });
fatalPrint(" error code : 0x{x}\n", .{state.error_code});
fatalPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
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;
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;
log.checkpoint(cp_panic);
log.recordPanic(message);
console.setSuppressed(false); // a panic outranks any display service holding the screen
status("\nKERNEL PANIC: ");
status(message);
status("\n");
fatal("\nKERNEL PANIC: "); // a panic outranks any display service holding the screen
fatal(message);
fatal("\n");
architecture.halt();
}
}.panic);