/// 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::{GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB}; use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; use core::fmt; use core::ptr; use core::sync::atomic::{AtomicU8, 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, bg: u32, // packed BGRA or RGBA background colour bgr: bool, } static mut FB: Option = None; /// 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; /// Initialise the framebuffer from GOP. Call *before* ExitBootServices. pub fn init(gop: &mut GraphicsOutputProtocol) { 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 fb = Framebuffer { base, width, height, pitch, bytes_per_pixel: bpp, bg, bgr, }; // Allocate the RAM backbuffer before first paint so every write goes // to both the display and the shadow. let words = (pitch * height) / 4; unsafe { if words > 0 { let v: Vec = vec![0; words]; SHADOW = Box::into_raw(v.into_boxed_slice()) as *mut u32; SHADOW_WORDS = words; } } fb.clear(); unsafe { FB = Some(fb); } } // --------------------------------------------------------------------------- // 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; unsafe { for dy in 0..h { let off = (y + dy) * row_words + x; ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, w); } } 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); } /// 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; 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) { let size = self.pitch * self.height; // 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. ptr::copy( self.base.add(n * row_bytes), self.base, total - n * row_bytes, ); 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 (cached RAM — fast). ptr::copy(SHADOW.add(n * row_words), SHADOW, words - n * row_words); // 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. // A bulk copy (rep movsb) writes in 64-byte bursts that // coalesce well on WC framebuffers — far fewer bus // transactions than per-pixel stores. ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, words); // 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"); }}; }