/// 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::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, 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::{AtomicU64, AtomicU8, 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; /// 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 fb = Framebuffer { base, width, height, pitch, bytes_per_pixel: bpp, framebuffer_size: mode.framebuffer_size, 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; } } // Re-stash the post-SetMode region (base can move) for the page // allocator, 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 ); 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; // 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; } ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, row_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); } /// 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; } } } } /// 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 { 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 (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. // // 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 { 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"); }}; }