//! A framebuffer text console: draws glyphs from an embedded PSF2 font directly //! 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. //! //! 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 //! never assumes a display exists. const std = @import("std"); const boot_handoff = @import("boot-handoff"); /// The one framebuffer console, valid only when `con_present`. var con: Console = undefined; var con_present: bool = false; /// Set while a user-space display service owns the framebuffer: `write` falls silent so /// the kernel doesn't paint over the compositor. Driven by the display device's /// claim/release (system/kernel/process.zig). The terminal panic/exception paths clear /// it first (`setSuppressed(false)`) — a dying machine's message wins over any display. var suppressed: bool = false; /// Set up the console over `fb`, or mark it absent if there's no usable /// framebuffer. Clears the screen when present. pub fn init(fb: boot_handoff.Framebuffer) void { if (!fb.present()) { con_present = false; return; } con = Console.init(fb); if (con.cols == 0 or con.rows == 0) { con_present = false; return; } con.clear(); con_present = true; } /// Whether an on-screen console is available. pub fn present() bool { return con_present; } /// Output sink: draw `bytes` on screen. A no-op when no framebuffer is present, or /// while a display service owns the screen (`suppressed`), so it's always safe to call. pub fn write(bytes: []const u8) void { if (!con_present or suppressed) return; for (bytes) |c| con.putChar(c); } /// Quiesce (or resume) the bootstrap console. Set true when a display service claims the /// framebuffer; set false when that claim is released, or by the panic path to force a /// last message onto a screen a (now-irrelevant) service was holding. pub fn setSuppressed(value: bool) void { suppressed = value; } /// The console font, embedded at compile time. cp850-8x16, PSF2 format: /// a 32-byte header, then 256 glyphs of 16 bytes each (one byte per 8-pixel /// row). We index glyphs straight by byte value, so ASCII maps 1:1. const font = @embedFile("font.psf"); const glyph_w = 8; const glyph_h = 16; const glyph_bytes = glyph_h; // 8 pixels wide => 1 byte per row const glyph_data = 32; // PSF2 header size pub const Console = struct { fb: boot_handoff.Framebuffer, cols: u32, rows: u32, col: u32 = 0, row: u32 = 0, fg: u32 = 0x00c8_c8c8, // light grey bg: u32 = 0x0000_0000, // black pub fn init(fb: boot_handoff.Framebuffer) Console { // Reach the framebuffer through the physmap, so the pointer stays valid // once the low identity map is gone. The base is mapped by both the // loader's bootstrap tables and paging.init. var mapped = fb; if (fb.base != 0) mapped.base = boot_handoff.physicalToVirtual(fb.base); return .{ .fb = mapped, .cols = fb.width / glyph_w, .rows = fb.height / glyph_h, }; } /// Fill the whole screen with the background colour and home the cursor. pub fn clear(self: *Console) void { var y: u32 = 0; while (y < self.fb.height) : (y += 1) self.fillRow(y, self.bg); self.col = 0; self.row = 0; } pub fn putChar(self: *Console, ch: u8) void { switch (ch) { '\n' => self.newline(), '\r' => self.col = 0, else => { if (self.col >= self.cols) self.newline(); self.drawGlyph(ch, self.col * glyph_w, self.row * glyph_h); self.col += 1; }, } } fn newline(self: *Console) void { self.col = 0; if (self.row + 1 >= self.rows) { self.scroll(); } else { self.row += 1; } } fn drawGlyph(self: *Console, ch: u8, px: u32, py: u32) void { const rows = font[glyph_data + @as(usize, ch) * glyph_bytes ..][0..glyph_bytes]; var gy: u32 = 0; while (gy < glyph_h) : (gy += 1) { const bits = rows[gy]; var gx: u32 = 0; while (gx < glyph_w) : (gx += 1) { // Leftmost pixel is the high bit. const on = (bits >> @as(u3, @intCast(7 - gx))) & 1 != 0; self.pixel(px + gx, py + gy, if (on) self.fg else self.bg); } } } /// Shift the visible text up one glyph row and clear the freed bottom row, /// leaving the cursor on that now-blank last line. fn scroll(self: *Console) void { const visible = self.rows * glyph_h; var y: u32 = 0; while (y + glyph_h < visible) : (y += 1) self.copyRow(y, y + glyph_h); while (y < visible) : (y += 1) self.fillRow(y, self.bg); self.row = self.rows - 1; } inline fn rowPtr(self: *Console, y: u32) [*]volatile u32 { const base: [*]volatile u8 = @ptrFromInt(self.fb.base); return @ptrCast(@alignCast(base + y * self.fb.pitch)); } inline fn pixel(self: *Console, x: u32, y: u32, color: u32) void { self.rowPtr(y)[x] = color; } fn fillRow(self: *Console, y: u32, color: u32) void { const row = self.rowPtr(y); var x: u32 = 0; while (x < self.fb.width) : (x += 1) row[x] = color; } fn copyRow(self: *Console, destination_y: u32, source_y: u32) void { const destination = self.rowPtr(destination_y); const source = self.rowPtr(source_y); var x: u32 = 0; while (x < self.fb.width) : (x += 1) destination[x] = source[x]; } };