/// GOP framebuffer driver — renders text via a bitmap font. /// /// Text surface (cursor, scrolling, colours) lives in `crate::vt`; this /// module is the low-level rasteriser: glyph blits and pixel shifts on the /// linear framebuffer. use crate::font; use crate::uefi::{EFI_SUCCESS, GopModeInfo, GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB}; use core::fmt; use core::ptr; use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering}; /// Framebuffer state captured before ExitBootServices. pub struct Framebuffer { base: *mut u8, width: usize, height: usize, pitch: usize, // bytes per scan line bytes_per_pixel: usize, /// Size of the firmware's linear framebuffer mapping, in bytes. /// Everything written to the display is clamped to this, so a mode /// whose `pitch * height` exceeds the actual mapping (some real /// firmware pads the scan-line pitch but sizes the buffer for the /// resolution) degrades to a truncated view instead of faulting. framebuffer_size: usize, bg: u32, // packed BGRA or RGBA background colour bgr: bool, } static mut FB: Option = None; /// The GOP framebuffer region (base + mapping size), stashed before the /// memory map is captured so the heap and page allocator can exclude it. /// Some real-world firmware lists the framebuffer's memory as /// `EfiConventionalMemory`; handing those pages out would let kernel /// allocations (stacks, DMA buffers) overwrite the display. static mut FB_REGION_BASE: usize = 0; static mut FB_REGION_SIZE: usize = 0; /// Stash the current GOP framebuffer region *before* the memory map is /// captured (must run before `memory::capture_memory_map`). Updated /// again after `SetMode` in `init` — the base can move. pub fn peek_fb_region(gop: &mut GraphicsOutputProtocol) { unsafe { let mode = &*gop.mode; FB_REGION_BASE = mode.framebuffer_base as usize; FB_REGION_SIZE = mode.framebuffer_size; } } /// The framebuffer region currently excluded from the allocators /// (base, size in bytes), if any. pub fn framebuffer_region() -> Option<(usize, usize)> { unsafe { if FB_REGION_SIZE == 0 { None } else { Some((FB_REGION_BASE, FB_REGION_SIZE)) } } } /// Software backbuffer: a RAM copy of the whole framebuffer (same pitch /// layout, u32 pixels). /// /// Real-hardware GOP framebuffers are typically mapped uncacheable or /// write-combining (firmware MTRR/PAT), so *reading* them costs a bus /// round-trip per cache line — a full-screen scroll shift reads ~4-8 MB /// and crawls. Rasterisation therefore writes every pixel to the shadow /// too, and scrolling is a fast cached memcpy inside the shadow followed /// by a write-only push to the display: the display is never read. static mut SHADOW: *mut u32 = core::ptr::null_mut(); static mut SHADOW_WORDS: usize = 0; /// Last boot-progress stage (set by `boot_marker`); the panic handler /// appends it to the on-screen band so a crash shows where boot died. static mut LAST_BOOT_STAGE: [u8; 96] = [0; 96]; static mut LAST_BOOT_STAGE_LEN: usize = 0; const STAGE_MAX: usize = 96; /// The last boot stage string (for the panic band). pub fn last_boot_stage() -> alloc::string::String { unsafe { alloc::string::String::from_utf8_lossy(&LAST_BOOT_STAGE[..LAST_BOOT_STAGE_LEN]).into_owned() } } /// Diagnostic: when > 0, every display store busy-waits ~this many /// cycles first, simulating the per-write bus cost of an uncached real /// GOP framebuffer (QEMU's framebuffer is RAM — instant). Lets us /// reproduce real-hardware scroll timing headless and race it against /// the preemptive scheduler. Set via the shell `stall` command. pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0); /// Set once a diagnostic QR has been rendered on screen. While locked, the /// klog screen echo is suppressed so subsequent log lines cannot overwrite /// the QR; the serial console keeps the full stream. Never released (the /// QR is the "photograph this" surface for hardware with no serial pipe). pub static QR_LOCKED: AtomicBool = AtomicBool::new(false); pub fn qr_lock() { QR_LOCKED.store(true, Ordering::Relaxed); } pub fn qr_locked() -> bool { QR_LOCKED.load(Ordering::Relaxed) } #[inline] fn stall_display() { let n = DISPLAY_STALL.load(Ordering::Relaxed); if n > 0 { // ~n pause-cycles per store; interrupts stay enabled so the // timer can preempt mid-push exactly like on real hardware. let mut i = n; while i > 0 { core::hint::spin_loop(); i -= 1; } } } /// The GUI canvas. Every display surface assumes this resolution: /// `vt` derives its text grid from it, the compositor's cursor clamps /// to it, and the future window manager lays out windows on it. We /// force it through GOP `SetMode` at boot — QEMU's std-vga offers it, /// and real GPUs list it as a standard mode — falling back to the /// smallest mode that fits, then to whatever the firmware chose. const WANTED_W: usize = 800; const WANTED_H: usize = 600; /// Pick a GOP mode: prefer an exact 800×600 (BGR/RGB 32-bit), else the /// smallest mode that fits the canvas, else leave the firmware's choice /// alone. Call before reading the framebuffer — `SetMode` may move /// both the mode info pointer and the framebuffer base. fn select_gop_mode(gop: &mut GraphicsOutputProtocol) { let mode = unsafe { &*gop.mode }; let cur = mode.mode; let info = unsafe { &*mode.info }; let cur_w = info.horizontal_resolution as usize; let cur_h = info.vertical_resolution as usize; if cur_w == WANTED_W && cur_h == WANTED_H { crate::serial_println!("gop: mode {} — already {}x{}", cur, cur_w, cur_h); return; } let mut best: Option<(u32, usize, usize)> = None; // (mode#, w, h) for i in 0..mode.max_mode { let mut size: usize = 0; let mut info_ptr: *mut GopModeInfo = core::ptr::null_mut(); let st = unsafe { (gop.query_mode)(gop, i, &mut size, &mut info_ptr) }; if st != EFI_SUCCESS || info_ptr.is_null() { continue; } let mi = unsafe { &*info_ptr }; if mi.pixel_format != PIXEL_BGR && mi.pixel_format != PIXEL_RGB { continue; // 32-bit BGR/RGB only (the driver assumes 4 Bpp) } let w = mi.horizontal_resolution as usize; let h = mi.vertical_resolution as usize; if w == WANTED_W && h == WANTED_H { best = Some((i, w, h)); break; } if w >= WANTED_W && h >= WANTED_H { match best { Some((_, bw, bh)) if bw * bh <= w * h => {} _ => best = Some((i, w, h)), } } } match best { Some((n, w, h)) => { let st = unsafe { (gop.set_mode)(gop, n) }; if st != EFI_SUCCESS { crate::serial_println!( "gop: SetMode({}) failed 0x{:x} — keeping current {}x{}", n, st, cur_w, cur_h ); return; } crate::serial_println!( "gop: mode {} selected — {}x{} (was {}x{})", n, w, h, cur_w, cur_h ); } None => crate::serial_println!( "gop: no mode ≥ {}x{} — keeping current {}x{}", WANTED_W, WANTED_H, cur_w, cur_h ), } } /// Initialise the framebuffer from GOP. Call *before* ExitBootServices. pub fn init(gop: &mut GraphicsOutputProtocol) { // Force the 800×600 canvas first; SetMode may move the info // pointer and framebuffer base, so everything is re-read after. select_gop_mode(gop); let mode = unsafe { &*gop.mode }; let info = unsafe { &*mode.info }; let base = mode.framebuffer_base as *mut u8; let width = info.horizontal_resolution as usize; let height = info.vertical_resolution as usize; let pitch = info.pixels_per_scan_line as usize * 4; // 32-bit pixel let bpp = 4; // Pick colours based on pixel format let (fg, bg) = match info.pixel_format { PIXEL_BGR => (0x00_FF_FF_FFu32, 0x00_00_00_00u32), // white fg, black bg PIXEL_RGB => (0x00_FF_FF_FFu32, 0x00_00_00_00u32), _ => (0x00_FF_FF_FFu32, 0x00_00_00_00u32), }; let bgr = info.pixel_format == PIXEL_BGR; let _ = fg; let fbf = Framebuffer { base, width, height, pitch, bytes_per_pixel: bpp, framebuffer_size: mode.framebuffer_size, bg, bgr, }; // Re-stash the post-SetMode region (base can move) for the // allocators, and log the authoritative geometry for diagnostics. unsafe { FB_REGION_BASE = mode.framebuffer_base as usize; FB_REGION_SIZE = mode.framebuffer_size; } crate::serial_println!( "gop: fb base={:#x} size={:#x} pitch={:#x} {}x{} bpp={} bgr={}", base as usize, { mode.framebuffer_size }, pitch, width, height, bpp, bgr ); fbf.clear(); unsafe { FB = Some(fbf); } // The RAM backbuffer is the *first* heap allocation. It is allocated // pre-ExitBootServices (before vt::init builds its grids) so it sits at // the front of the free list; the heap free list already excludes the // fb region (init_heap split at the fb hole) and the range-overlap // check below cannot alias the display. An ExitBootServices retry on // real hardware no longer rebuilds the free list (see // memory::refresh_map_key), so this pre-EBS allocation is preserved. shadow_late_init(); } /// Allocate the RAM backbuffer as the FIRST heap allocation (called from /// `init`, pre-ExitBootServices). Degrades to no-shadow mode (never /// panics) when the range would overlap the framebuffer or the alloc fails. pub fn shadow_late_init() { unsafe { if let Some(fbinfo) = fb().as_ref() { let words = (fbinfo.pitch * fbinfo.height) / 4; if words > 0 { let bytes = words * 4; #[allow(clippy::slow_vector_initialization)] // deliberate: try_reserve must not panic let mut v: alloc::vec::Vec = alloc::vec::Vec::new(); if v.try_reserve_exact(words).is_ok() { v.resize(words, 0); // can't fail after the reserve let p = v.as_ptr() as usize; let overlaps = FB_REGION_SIZE > 0 && p < FB_REGION_BASE + FB_REGION_SIZE && p + bytes > FB_REGION_BASE; if !overlaps { SHADOW = v.as_ptr() as *mut u32; SHADOW_WORDS = words; for i in 0..words { *SHADOW.add(i) = fbinfo.bg; } core::mem::forget(v); // shadow owns the memory now } } let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE); let (shadow, swords) = (SHADOW as usize, SHADOW_WORDS); crate::serial_println!( "gop: shadow @ {:#x} words={} fb={:#x}..{:#x} origin=heap", shadow, swords, rbase, rbase + rsize, ); if SHADOW.is_null() { let msg = "gop: WARNING — shadow rejected (fb overlap); no-shadow fallback"; crate::serial_println!("{}", msg); let lines: alloc::vec::Vec<&str> = msg .as_bytes() .chunks(100) .map(|c| core::str::from_utf8(c).unwrap_or("?")) .collect(); dump_lines(&lines); } } } } } /// Draw a one-line boot-progress marker to the display (row 0, /// white-on-black). Real hardware has no serial console, so every /// boot stage is written straight to the framebuffer — the last visible /// marker before a hang identifies where the kernel died. Uses the /// direct-display glyph path (the shadow may not exist yet in early /// boot). Row 0 stays a persistent status line: the shell starts its /// output at row 1 so the marker survives a post-boot freeze. pub fn boot_marker(msg: &str) { boot_marker_at(msg, 0); } /// DEBUG (crash-family): read the shadow block's header words. /// Returns `(block_base, size, free_flag)` if a shadow is live. /// /// The shadow is the first heap allocation: payload at base+32, header /// at base. A live shadow shows `free == 0`; if its block ever leaked /// back into the free list (while `SHADOW` still points at it), later /// allocations carve into the zeroed span and the display writes keep /// zeroing them — the OptiPlex crash family signature. pub fn shadow_block_state() -> Option<(usize, usize, usize)> { unsafe { if SHADOW.is_null() { return None; } let base = (SHADOW as usize).wrapping_sub(32); let size = *(base as *const usize); let free = *((base + 8) as *const usize); Some((base, size, free)) } } /// boot_marker at an explicit text row. The shell's first paint /// occupies grid rows 0 (motd) and 1 (=== banner) and the prompt sits /// at row 2 — markers whose *values* matter after boot (shadow state, /// vt grid pointers) are anchored to the bottom of the screen via /// `bottom_row`, which also survives a stale crash.log replay (the /// fault band is ~20 lines starting at grid row 2). pub fn boot_marker_at(msg: &str, row: usize) { // Record the stage so a later panic band can show how far boot got. unsafe { let n = msg.len().min(STAGE_MAX); let dst = core::ptr::addr_of_mut!(LAST_BOOT_STAGE) as *mut u8; core::ptr::copy_nonoverlapping(msg.as_ptr(), dst, n); LAST_BOOT_STAGE_LEN = n; } let Some(fb) = fb().as_ref() else { return; }; if row >= fb.height / 16 { return; } let cols = fb.width / 8; let white = (0xFFu8, 0xFFu8, 0xFFu8); let black = (0x00u8, 0x00u8, 0x00u8); let mut c = 0usize; for &b in msg.as_bytes().iter().take(cols) { if b < 0x80 { blit_glyph(c, row, b as u16, white, black); c += 1; } } // Blank the rest of the row so a shorter message can't leave stale // glyphs behind. while c < cols { blit_glyph(c, row, b' ' as u16, white, black); c += 1; } } /// Text-row count of the active framebuffer (each row is 16 px tall). pub fn text_rows() -> usize { fb().as_ref().map(|f| f.height / 16).unwrap_or(0) } /// Row index of the `off`-th text row from the bottom (0 = last row). /// Bottom-anchored markers survive the shell banner + prompt at the top /// and a stale crash.log replay below it. pub fn bottom_row(off: usize) -> usize { text_rows().saturating_sub(1 + off) } // --------------------------------------------------------------------------- // Public text API — the text surface now lives in `crate::vt`; these wrappers // keep callers (shell, macros) unchanged. // --------------------------------------------------------------------------- /// Draw a character at the current terminal cursor position and advance. pub fn put_char(c: char) { let b = if (c as u32) < 0x100 { c as u8 } else { b'?' }; crate::vt::put_char(b); } /// Write a string to the terminal. pub fn write_str(s: &str) { crate::vt::write_str(s); } /// Clear the terminal and home the cursor. pub fn clear_screen() { crate::vt::clear_all(); } /// Raw-pointer accessor for the framebuffer static (single-threaded /// kernel; keeps `static_mut_refs` quiet). fn fb() -> &'static mut Option { unsafe { &mut *core::ptr::addr_of_mut!(FB) } } /// (cols, rows) of the text surface, from the current framebuffer mode. pub fn dims() -> (usize, usize) { match fb().as_ref() { Some(f) => (f.width / 8, f.height / 16), None => (0, 0), } } /// (width, height) of the framebuffer in pixels. pub fn pixel_dims() -> (usize, usize) { match fb().as_ref() { Some(f) => (f.width, f.height), None => (0, 0), } } // --------------------------------------------------------------------------- // Rect primitives + the push path // --------------------------------------------------------------------------- /// Copy a shadow rect to the display and composite the mouse cursor on /// top. Every display write goes through here (glyph blits, fills, /// scroll shifts), so the cursor — which lives *only* in this overlay, /// never in the shadow — stays on top of whatever the content is without /// any save/restore bookkeeping. The display is never read. pub(crate) fn push_rect(x: usize, y: usize, w: usize, h: usize) { let Some(fb) = fb().as_ref() else { return; }; if unsafe { SHADOW.is_null() } || x >= fb.width || y >= fb.height { return; } let w = w.min(fb.width - x); let h = h.min(fb.height - y); let row_words = fb.pitch / 4; // Words of the firmware's actual display mapping — per-row clamp so a // pitch-padded mode never writes past the buffer. let fb_words = fb.framebuffer_size / 4; unsafe { for dy in 0..h { let off = (y + dy) * row_words + x; let row_w = w.min(fb_words.saturating_sub(off)); if row_w == 0 { continue; } for k in 0..row_w { stall_display(); ptr::write_volatile(fb.base.add((off + k) * 4) as *mut u32, *SHADOW.add(off + k)); } } } overlay_cursor(x, y, w, h); } /// Fill a rectangle in the shadow and push it (with cursor composited). pub fn fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) { let Some(fb) = fb().as_ref() else { return; }; if x >= fb.width || y >= fb.height { return; } let w = w.min(fb.width - x); let h = h.min(fb.height - y); if unsafe { SHADOW.is_null() } { // Degraded: no backbuffer — write the display directly. let row_words = fb.pitch / 4; unsafe { for dy in 0..h { let off = (y + dy) * row_words + x; for dx in 0..w { ptr::write_volatile(fb.base.add((off + dx) * 4) as *mut u32, color); } } } return; } let row_words = fb.pitch / 4; unsafe { for dy in 0..h { let off = (y + dy) * row_words + x; for dx in 0..w { *SHADOW.add(off + dx) = color; } } } push_rect(x, y, w, h); } /// Fill a shadow rectangle WITHOUT pushing to the display. The window /// manager composites several surfaces into the shadow and pushes once at /// the end. pub(crate) fn shadow_fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) { let Some(fb) = fb().as_ref() else { return; }; if unsafe { SHADOW.is_null() } || x >= fb.width || y >= fb.height { return; } let w = w.min(fb.width - x); let h = h.min(fb.height - y); let row_words = fb.pitch / 4; unsafe { for dy in 0..h { let off = (y + dy) * row_words + x; for dx in 0..w { *SHADOW.add(off + dx) = color; } } } } /// Copy a row-major `u32` source buffer (stride in pixels) into the shadow /// WITHOUT pushing. Clipped to the framebuffer. pub(crate) fn shadow_blit( x: usize, y: usize, w: usize, h: usize, src: *const u32, src_stride: usize, ) { let Some(fb) = fb().as_ref() else { return; }; if unsafe { SHADOW.is_null() } || src.is_null() || x >= fb.width || y >= fb.height { return; } let w = w.min(fb.width - x); let h = h.min(fb.height - y); let row_words = fb.pitch / 4; unsafe { for dy in 0..h { let doff = (y + dy) * row_words + x; let soff = dy * src_stride; for dx in 0..w { *SHADOW.add(doff + dx) = *src.add(soff + dx); } } } } /// Render one 8×16 glyph into the shadow at a pixel origin WITHOUT pushing /// (for window chrome and console repaint). Same glyph lookup as /// `blit_glyph`, but caller-controlled push. pub(crate) fn shadow_glyph_px(x: usize, y: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) { let Some(fb) = fb().as_ref() else { return; }; if unsafe { SHADOW.is_null() } { return; } let glyph = font::GLYPH_8X16; let data = if ch < 256 { match glyph.get(ch as usize) { Some(d) => *d, None => return, } } else if let Some(bits) = unicode_glyph_bits(ch) { bits } else { match glyph.get(32) { Some(d) => *d, None => return, } }; let fgc = rgb(fg.0, fg.1, fg.2); let bgc = rgb(bg.0, bg.1, bg.2); let row_words = fb.pitch / 4; unsafe { for cy in 0..16 { let py = y + cy; if py >= fb.height { break; } let row_bits = data[cy]; for cx in 0..8 { let px = x + cx; if px >= fb.width { break; } let on = row_bits & (0x80 >> cx) != 0; *SHADOW.add(py * row_words + px) = if on { fgc } else { bgc }; } } } } /// Print crash/fault lines directly to the framebuffer, bypassing the vt /// entirely (the vt state may be corrupt when an exception fires). /// /// White-on-red band at the top of the screen, written through the same /// clamped glyph path — safe even if the fault was inside the vt/gop /// machinery, and visible on real hardware with no serial console. pub fn dump_lines(lines: &[&str]) { let Some(fb) = fb().as_ref() else { return; }; let cols = fb.width / 8; let rows = fb.height / 16; let red = (0xC0u8, 0x00u8, 0x00u8); let white = (0xFFu8, 0xFFu8, 0xFFu8); let band = lines.len().min(rows); // Red band behind the text. if cols > 0 && band > 0 { fill_rect(0, 0, cols * 8, band * 16, rgb(red.0, red.1, red.2)); } for (i, line) in lines.iter().enumerate().take(band) { let mut c = 0; for &b in line.as_bytes() { if c >= cols { break; } if b < 0x80 { blit_glyph(c, i, b as u16, white, red); c += 1; } } } // Scannable QR of the same text below the band (heap-free encoder; // real hardware has no serial console — photograph the screen). dump_qr(lines); } /// Render the crash/fault lines as a QR code below the dump band. /// /// The joined text is built in a `.bss` buffer (the fault path must not /// allocate) and encoded byte-mode EC-L by `crate::qr` (also heap-free, /// static matrix). Over-long text is truncated with a "…" marker. If /// the text does not fit a QR or there is no framebuffer, this is a no-op /// and the band text remains the only channel. static mut QR_TEXT: [u8; crate::qr::MAX_BYTES + 4] = [0; crate::qr::MAX_BYTES + 4]; pub fn dump_qr(lines: &[&str]) { let Some(fb) = fb().as_ref() else { return; }; unsafe { let text = &mut *core::ptr::addr_of_mut!(QR_TEXT); let mut n = 0usize; for line in lines { let b = line.as_bytes(); let take = b.len().min(text.len() - n); text[n..n + take].copy_from_slice(&b[..take]); n += take; if n < text.len() { text[n] = b'\n'; n += 1; } } while n > 0 && text[n - 1] == b'\n' { n -= 1; } if n == 0 { return; } let cap = crate::qr::MAX_BYTES.saturating_sub(3); if n > cap { n = cap; text[n..n + 3].copy_from_slice(b"..."); n += 3; } if crate::qr::encode(&text[..n]) == 0 { return; } } render_qr(fb, lines.len() * 16); } /// Draw the current QR matrix (black on white, quiet zone) centred below /// the band, scaled to the largest square that fits. fn render_qr(fb: &Framebuffer, band_px: usize) { let size = crate::qr::size(); if size == 0 { return; } let total = size + 8; // 4-module quiet zone each side let avail_w = fb.width.saturating_sub(24); let avail_h = fb.height.saturating_sub(band_px + 16); let scale = (avail_w / total).min(avail_h / total); if scale < 1 { return; } let px = total * scale; let x0 = (fb.width - px) / 2; let y0 = band_px + 8 + (avail_h.saturating_sub(px) / 2); let white = rgb(0xFF, 0xFF, 0xFF); let black = rgb(0, 0, 0); fill_rect(x0, y0, px, px, white); for y in 0..size { for x in 0..size { if crate::qr::module(x, y) { fill_rect( x0 + (x + 4) * scale, y0 + (y + 4) * scale, scale, scale, black, ); } } } } /// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. pub fn shadow_pixel(x: usize, y: usize) -> Option { let fb = fb().as_ref()?; if x >= fb.width || y >= fb.height || unsafe { SHADOW.is_null() } { return None; } Some(unsafe { *SHADOW.add(y * (fb.pitch / 4) + x) }) } /// Read one pixel back from the display (volatile; uncached on real /// hardware — test/demo use only, never on a hot path). pub fn display_pixel(x: usize, y: usize) -> Option { let fb = fb().as_ref()?; if x >= fb.width || y >= fb.height { return None; } Some(unsafe { ptr::read_volatile(fb.base.add((y * (fb.pitch / 4) + x) * 4) as *mut u32) }) } // --------------------------------------------------------------------------- // Mouse cursor — a display overlay, composited by `push_rect`. // --------------------------------------------------------------------------- /// Cursor bitmap: 12×16 classic pointer, 1 = opaque. The hotspot is the /// top-left corner of the bitmap; the arrow tip sits at (2, 0). Built by /// a const fn from explicit column lists (pixel column `dx` = bit `11-dx`) /// so the shape is auditable instead of hand-mangled bit literals. const CURSOR_W: usize = 12; const CURSOR_H: usize = 16; const fn bmp_row(cols: &[u8]) -> u16 { let mut v: u16 = 0; let mut i = 0; while i < cols.len() { v |= 1u16 << (11 - cols[i] as u16); i += 1; } v } /// Head: triangle from a 1-px tip at (2,0) widening right to col 11. /// Tail: 2-px diagonal dropping down-right from the head's base. const CURSOR_BMP: [u16; CURSOR_H] = [ bmp_row(&[2]), bmp_row(&[2, 3]), bmp_row(&[2, 3, 4]), bmp_row(&[2, 3, 4, 5]), bmp_row(&[2, 3, 4, 5, 6]), bmp_row(&[2, 3, 4, 5, 6, 7]), bmp_row(&[2, 3, 4, 5, 6, 7, 8]), bmp_row(&[2, 3, 4, 5, 6, 7, 8, 9]), bmp_row(&[2, 3, 4, 5, 6, 7, 8, 9, 10]), bmp_row(&[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), bmp_row(&[8, 9]), bmp_row(&[9, 10]), bmp_row(&[9, 10]), bmp_row(&[10, 11]), bmp_row(&[10, 11]), bmp_row(&[10, 11]), ]; /// Packed cursor position: x in the high 32 bits, y in the low 32, so a /// single relaxed load never sees a torn pair. Written by the cursor /// task, read by every push (any task). static CURSOR_POS: AtomicU64 = AtomicU64::new(0); static CURSOR_BUTTONS: AtomicU8 = AtomicU8::new(0); fn pack(x: usize, y: usize) -> u64 { ((x as u64) << 32) | (y as u64) } /// Current cursor position (top-left of the bitmap). pub fn cursor_pos() -> (usize, usize) { let v = CURSOR_POS.load(Ordering::Relaxed); ((v >> 32) as usize, (v & 0xFFFF_FFFF) as usize) } /// Current button state: bit 0 = left, bit 1 = right, bit 2 = middle. pub fn cursor_buttons() -> u8 { CURSOR_BUTTONS.load(Ordering::Relaxed) } pub fn cursor_set_buttons(b: u8) { CURSOR_BUTTONS.store(b, Ordering::Relaxed); } /// Place the cursor (used at init; centre of the screen). pub fn cursor_set(x: usize, y: usize) { CURSOR_POS.store(pack(x, y), Ordering::Relaxed); } /// Move the cursor by a relative delta, clamped to the screen, erasing /// the old position (a shadow push — the shadow never held the cursor) /// and drawing the new one. Returns the new position. pub fn cursor_move(dx: i16, dy: i16) -> (usize, usize) { let Some(fb) = fb().as_ref() else { return cursor_pos(); }; let (cx, cy) = cursor_pos(); let max_x = fb.width.saturating_sub(CURSOR_W); let max_y = fb.height.saturating_sub(CURSOR_H); let nx = (cx as i64 + dx as i64).clamp(0, max_x as i64) as usize; let ny = (cy as i64 + dy as i64).clamp(0, max_y as i64) as usize; if (nx, ny) == (cx, cy) { return (cx, cy); } CURSOR_POS.store(pack(nx, ny), Ordering::Relaxed); // Restore the old rect (overlay now composites at the new position). push_rect(cx, cy, CURSOR_W, CURSOR_H); // Draw at the new position. push_rect(nx, ny, CURSOR_W, CURSOR_H); (nx, ny) } /// If the cursor overlaps the given rect, composite it (black outline /// offset +1,+1, white body on top). Called by `push_rect`. fn overlay_cursor(x: usize, y: usize, w: usize, h: usize) { let (cx, cy) = cursor_pos(); if x >= cx + CURSOR_W || y >= cy + CURSOR_H { return; } if cx >= x + w || cy >= y + h { return; } draw_bmp(cx + 1, cy + 1, rgb(0, 0, 0)); draw_bmp(cx, cy, rgb(255, 255, 255)); } /// Re-composite the cursor where it is (after a scroll shift, which /// bypasses `push_rect`, and once at init so it is visible over idle /// cells the diff never re-blits). pub(crate) fn cursor_redraw() { let (cx, cy) = cursor_pos(); draw_bmp(cx + 1, cy + 1, rgb(0, 0, 0)); draw_bmp(cx, cy, rgb(255, 255, 255)); } /// Write the cursor bitmap at (x, y) in one colour — display only, the /// shadow is never touched (that is what makes the overlay race-free). fn draw_bmp(x: usize, y: usize, color: u32) { let Some(fb) = fb().as_ref() else { return; }; let row_words = fb.pitch / 4; unsafe { for dy in 0..CURSOR_H { let yy = y + dy; if yy >= fb.height { break; } let row = CURSOR_BMP[dy]; for dx in 0..CURSOR_W { let xx = x + dx; if xx >= fb.width { break; } if row & (1u16 << (11 - dx)) != 0 { let off = yy * row_words + xx; // Clamp to the display mapping (defensive). if off < fb.framebuffer_size / 4 { stall_display(); ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color); } } } } } } /// Pack an (r, g, b) colour for the framebuffer's pixel format. pub fn rgb(r: u8, g: u8, b: u8) -> u32 { match fb().as_ref() { Some(f) if f.bgr => ((r as u32) << 16) | ((g as u32) << 8) | b as u32, _ => ((b as u32) << 16) | ((g as u32) << 8) | r as u32, } } // --------------------------------------------------------------------------- // Framebuffer implementation // --------------------------------------------------------------------------- impl Framebuffer { fn clear(&self) { // Clamp the display write to the firmware's actual mapping — some // real GPUs size the buffer for the resolution while the pitch // (with scan-line padding) is larger. let size = (self.pitch * self.height).min(self.framebuffer_size); // Fill with background colour (32-bit writes), display + shadow. let mut p = self.base; let end = unsafe { p.add(size) }; unsafe { while p < end { ptr::write_volatile(p as *mut u32, self.bg); p = p.add(4); } } if !unsafe { SHADOW.is_null() } { for i in 0..unsafe { SHADOW_WORDS } { unsafe { *SHADOW.add(i) = self.bg; } } } } /// Shift the framebuffer content up by `n` text rows (16 pixels each) /// and clear the exposed bottom rows. Called by `vt` when deferred /// scrolls are pushed to the display; `n` can batch many scrolls into /// one write-only push. /// /// With the RAM backbuffer, this is a cached shadow memcpy + a /// write-only push to the display — the display itself is never read /// (reads are uncached on real hardware and dominate the old path). pub(crate) fn shift_up_n(&self, n: usize) { let n = n.max(1).min(self.height / 16); // sanity let row_bytes = self.pitch * 16; let total = self.pitch * self.height; let row_words = row_bytes / 4; let words = total / 4; unsafe { if SHADOW.is_null() { // No shadow (allocation failed): fall back to shifting the // display directly — per-word volatile stores, never a // bulk memcpy: a giant REP MOVS to an uncached MMIO // framebuffer can stall forever on real hardware (the // boot-time clear() uses the same per-word pattern and // provably works). let src = self.base.add(n * row_bytes) as *mut u32; let n_words = (total - n * row_bytes) / 4; for i in 0..n_words { ptr::write_volatile(self.base.add(i * 4) as *mut u32, ptr::read(src.add(i))); } let last_row = self.base.add(total - n * row_bytes); for i in 0..(n * row_words) { ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); } return; } // Shift the shadow up n rows. Per-word stores, never a // bulk REP MOVS: the shadow is cached RAM so this is fast, // and it stays safe even if a future allocation ever aliased // the shadow over an uncached framebuffer (MMIO→MMIO fast // string ops can wedge real GPUs). for i in 0..(words - n * row_words) { *SHADOW.add(i) = *SHADOW.add(n * row_words + i); } // Clear the exposed bottom rows in the shadow. for i in (words - n * row_words)..words { *SHADOW.add(i) = self.bg; } // Push the shadow to the display: write-only, no reads. // // Per-word volatile stores, NOT a bulk rep-movsb: the shadow // is cached RAM (reads are cheap) and each display store is // individually posted, exactly like the boot-time clear(). A // 1.92 MB REP MOVS to an uncached real-GPU framebuffer can // wedge the CPU — the "fast string" burst path never drains // on some hardware, the shell stalls mid-instruction, no // exception fires, and the machine looks hung with a torn // frame (the white bands). QEMU never shows this because its // framebuffer is RAM. // // Clamp to the firmware's mapping size; a pitch-padded mode // whose `pitch * height` exceeds the buffer degrades to a // truncated view instead of faulting past the mapping. let push_words = words.min(self.framebuffer_size / 4); let dst = self.base as *mut u32; for i in 0..push_words { stall_display(); ptr::write_volatile(dst.add(i), ptr::read(SHADOW.add(i))); } // The scroll copied the shadow (which never contains the // cursor); re-composite it on top of the pushed frame. crate::gop::cursor_redraw(); } } } /// Blit one glyph cell at (col, row) with explicit fg/bg colours. /// Procedurally-rendered bitmaps for Unicode glyphs missing from the /// 256-glyph Latin-1 font: box-drawing (U+2500–U+257F), arrows /// (U+2190–U+2193) and the em dash (U+2014). Box drawing is pure /// geometry, so these are computed, not stored. Same format as the font: /// one byte per row, bit 7 = leftmost column. fn unicode_glyph_bits(ch: u16) -> Option<[u8; 16]> { let mut g = [0u8; 16]; match ch { // ─ light horizontal, ━ heavy horizontal 0x2500 => { g[7] = 0xFF; g[8] = 0xFF; } 0x2501 => { g[7] = 0xFF; g[8] = 0xFF; g[9] = 0xFF; } // │ light vertical, ┃ heavy vertical 0x2502 => { for y in 0..16 { g[y] = 0x18; } } 0x2503 => { for y in 0..16 { g[y] = 0x3C; } } // corners ┌ ┐ └ ┘ 0x250C => { g[7] = 0xF0; g[8] = 0xF8; for y in 9..16 { g[y] = 0x18; } } 0x2510 => { g[7] = 0x0F; g[8] = 0x1F; for y in 9..16 { g[y] = 0x18; } } 0x2514 => { for y in 0..7 { g[y] = 0x18; } g[7] = 0xF8; g[8] = 0xF0; } 0x2518 => { for y in 0..7 { g[y] = 0x18; } g[7] = 0x1F; g[8] = 0x0F; } // T junctions ├ ┤ ┬ ┴ and cross ┼ 0x251C => { for y in 0..16 { g[y] = 0x18; } g[7] = 0xF8; g[8] = 0xF8; } 0x2524 => { for y in 0..16 { g[y] = 0x18; } g[7] = 0x1F; g[8] = 0x1F; } 0x252C => { g[7] = 0xFF; g[8] = 0xFF; for y in 9..16 { g[y] = 0x18; } } 0x2534 => { for y in 0..7 { g[y] = 0x18; } g[7] = 0xFF; g[8] = 0xFF; } 0x253C => { for y in 0..16 { g[y] = 0x18; } g[7] = 0xFF; g[8] = 0xFF; } // ═ double horizontal, ║ double vertical 0x2550 => { g[6] = 0xFF; g[9] = 0xFF; } 0x2551 => { for y in 0..16 { g[y] = 0x24; } } // — em dash 0x2014 => { g[7] = 0xFF; g[8] = 0xFF; } // ← → ↑ ↓ 0x2190 => { g[8] = 0xFF; g[6] = 0xC0; g[7] = 0xC0; g[9] = 0xC0; g[10] = 0xC0; } 0x2192 => { g[8] = 0xFF; g[6] = 0x03; g[7] = 0x03; g[9] = 0x03; g[10] = 0x03; } 0x2191 => { g[0] = 0x18; g[1] = 0x3C; g[2] = 0x7E; for y in 3..16 { g[y] = 0x18; } } 0x2193 => { for y in 0..13 { g[y] = 0x18; } g[13] = 0x7E; g[14] = 0x3C; g[15] = 0x18; } _ => return None, } Some(g) } pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) { if let Some(fb) = fb().as_ref() { let glyph = font::GLYPH_8X16; let data = if ch < 256 { match glyph.get(ch as usize) { Some(d) => *d, None => return, } } else if let Some(bits) = unicode_glyph_bits(ch) { bits } else { match glyph.get(32) { Some(d) => *d, None => return, } }; let fg = rgb(fg.0, fg.1, fg.2); let bg = rgb(bg.0, bg.1, bg.2); let x = col * 8; let y = row * 16; if unsafe { SHADOW.is_null() } { // Degraded: no backbuffer — write the display directly, no // cursor overlay. for cy in 0..16 { let row_bits = data[cy]; for cx in 0..8 { let offset = (y + cy) * fb.pitch + (x + cx) * fb.bytes_per_pixel; let px = if row_bits & (0x80 >> cx) != 0 { fg } else { bg }; unsafe { ptr::write_volatile(fb.base.add(offset) as *mut u32, px); } } } return; } // Normal path: rasterise into the RAM shadow, then one push // copies the cell to the display and composites the cursor. for cy in 0..16 { let row_bits = data[cy]; for cx in 0..8 { let offset = (y + cy) * fb.pitch + (x + cx) * fb.bytes_per_pixel; let px = if row_bits & (0x80 >> cx) != 0 { fg } else { bg }; unsafe { *SHADOW.add(offset / 4) = px; } } } push_rect(x, y, 8, 16); } } /// Shift the framebuffer content up `n` text rows and clear the exposed /// bottom rows. `n` batches deferred scrolls into one push. pub(crate) fn shift_up_n(n: usize) { if let Some(fb) = fb().as_ref() { fb.shift_up_n(n); } } // --------------------------------------------------------------------------- // fmt::Write wrapper // --------------------------------------------------------------------------- pub struct GopWriter; impl fmt::Write for GopWriter { fn write_str(&mut self, s: &str) -> fmt::Result { write_str(s); Ok(()) } } /// Write a formatted string to the framebuffer. #[macro_export] macro_rules! gop_print { ($($arg:tt)*) => {{ use core::fmt::Write; let _ = write!(&mut $crate::gop::GopWriter, $($arg)*); }}; } #[macro_export] macro_rules! gop_println { () => { $crate::gop::write_str("\n"); }; ($($arg:tt)*) => {{ $crate::gop_print!($($arg)*); $crate::gop::write_str("\n"); }}; }