From 74bc1ce1f0d3783578e89030955319c403db6e72 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 17:55:52 +0200 Subject: gop: RAM backbuffer — scrolling never reads the framebuffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-hardware GOP framebuffers are typically uncacheable or write-combining (firmware MTRR/PAT), so the scroll shift's full-screen memcpy *read* ~4-8 MB through uncached memory — a bus round-trip per cache line. That made scroll printing crawl on real hardware while plain printing (a few glyph writes) stayed instant. - gop keeps a RAM shadow copy of the whole framebuffer (same pitch layout), allocated at init. - Every raster write (glyph blits, clear) goes to both the display and the shadow. - shift_up becomes: cached shadow memcpy up one row + clear exposed bottom row in the shadow + one write-only push shadow→display via copy_nonoverlapping (64-byte bursts that coalesce on WC memory) — the display is never read. - Fallback to the old direct shift if the shadow allocation is null. Verified in QEMU: mmap output scrolls ~100 lines with coherent rows and a clean bottom row, clear wipes the screen, cursor tracks the prompt, editor paths untouched. Release + dev builds pass. --- rsnix/src/gop.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 7 deletions(-) (limited to 'rsnix/src') diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 9cb0d7d..fa5378e 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs @@ -5,6 +5,9 @@ /// 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; @@ -22,6 +25,18 @@ pub struct Framebuffer { 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 }; @@ -51,6 +66,17 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { 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 { @@ -105,7 +131,7 @@ pub fn rgb(r: u8, g: u8, b: u8) -> u32 { impl Framebuffer { fn clear(&self) { let size = self.pitch * self.height; - // Fill with background colour (32-bit writes) + // Fill with background colour (32-bit writes), display + shadow. let mut p = self.base; let end = unsafe { p.add(size) }; unsafe { @@ -114,22 +140,49 @@ impl Framebuffer { p = p.add(4); } } + if !unsafe { SHADOW.is_null() } { + for i in 0..unsafe { SHADOW_WORDS } { + unsafe { + *SHADOW.add(i) = self.bg; + } + } + } } /// Shift the whole framebuffer up one row (16 pixels) and clear the /// exposed bottom row. Called by `vt::scroll_up`; the vt then re-blits /// only the new bottom row. + /// + /// 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(&self) { - // Move everything up one row (16 pixels) let row_bytes = self.pitch * 16; let total = self.pitch * self.height; + let row_words = row_bytes / 4; + let words = total / 4; unsafe { - ptr::copy(self.base.add(row_bytes), self.base, total - row_bytes); - // Clear last row - let last_row = self.base.add(total - row_bytes); - for i in 0..(self.pitch * 16 / 4) { - ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); + if SHADOW.is_null() { + // No shadow (allocation failed): fall back to shifting the + // display directly. + ptr::copy(self.base.add(row_bytes), self.base, total - row_bytes); + let last_row = self.base.add(total - row_bytes); + for i in 0..row_words { + ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); + } + return; + } + // Shift the shadow up one row (cached RAM — fast). + ptr::copy(SHADOW.add(row_words), SHADOW, words - row_words); + // Clear the exposed bottom row in the shadow. + for i in (words - 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); } } } @@ -308,7 +361,11 @@ pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg: let offset = (y + cy) * fb.pitch + (x + cx) * fb.bytes_per_pixel; unsafe { let px = if row_bits & (0x80 >> cx) != 0 { fg } else { bg }; + // Display (volatile; the fb is MMIO-ish) + RAM shadow. ptr::write_volatile(fb.base.add(offset) as *mut u32, px); + if !SHADOW.is_null() { + *SHADOW.add(offset / 4) = px; + } } } } -- cgit v1.2.3