diff options
Diffstat (limited to 'rsnix/src')
| -rw-r--r-- | rsnix/src/gop.rs | 139 |
1 files changed, 122 insertions, 17 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 439dfb1..8946f51 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -5,12 +5,9 @@ | |||
| 5 | /// linear framebuffer. | 5 | /// linear framebuffer. |
| 6 | use crate::font; | 6 | use crate::font; |
| 7 | use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; | 7 | use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; |
| 8 | use alloc::boxed::Box; | ||
| 9 | use alloc::vec; | ||
| 10 | use alloc::vec::Vec; | ||
| 11 | use core::fmt; | 8 | use core::fmt; |
| 12 | use core::ptr; | 9 | use core::ptr; |
| 13 | use core::sync::atomic::{AtomicU64, AtomicU8, Ordering}; | 10 | use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering}; |
| 14 | 11 | ||
| 15 | /// Framebuffer state captured before ExitBootServices. | 12 | /// Framebuffer state captured before ExitBootServices. |
| 16 | pub struct Framebuffer { | 13 | pub struct Framebuffer { |
| @@ -74,6 +71,27 @@ pub fn framebuffer_region() -> Option<(usize, usize)> { | |||
| 74 | static mut SHADOW: *mut u32 = core::ptr::null_mut(); | 71 | static mut SHADOW: *mut u32 = core::ptr::null_mut(); |
| 75 | static mut SHADOW_WORDS: usize = 0; | 72 | static mut SHADOW_WORDS: usize = 0; |
| 76 | 73 | ||
| 74 | /// Diagnostic: when > 0, every display store busy-waits ~this many | ||
| 75 | /// cycles first, simulating the per-write bus cost of an uncached real | ||
| 76 | /// GOP framebuffer (QEMU's framebuffer is RAM — instant). Lets us | ||
| 77 | /// reproduce real-hardware scroll timing headless and race it against | ||
| 78 | /// the preemptive scheduler. Set via the shell `stall` command. | ||
| 79 | pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0); | ||
| 80 | |||
| 81 | #[inline] | ||
| 82 | fn stall_display() { | ||
| 83 | let n = DISPLAY_STALL.load(Ordering::Relaxed); | ||
| 84 | if n > 0 { | ||
| 85 | // ~n pause-cycles per store; interrupts stay enabled so the | ||
| 86 | // timer can preempt mid-push exactly like on real hardware. | ||
| 87 | let mut i = n; | ||
| 88 | while i > 0 { | ||
| 89 | core::hint::spin_loop(); | ||
| 90 | i -= 1; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 77 | /// The GUI canvas. Every display surface assumes this resolution: | 95 | /// The GUI canvas. Every display surface assumes this resolution: |
| 78 | /// `vt` derives its text grid from it, the compositor's cursor clamps | 96 | /// `vt` derives its text grid from it, the compositor's cursor clamps |
| 79 | /// to it, and the future window manager lays out windows on it. We | 97 | /// to it, and the future window manager lays out windows on it. We |
| @@ -189,16 +207,11 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 189 | bgr, | 207 | bgr, |
| 190 | }; | 208 | }; |
| 191 | 209 | ||
| 192 | // Allocate the RAM backbuffer before first paint so every write goes | 210 | // The RAM backbuffer is allocated *after* ExitBootServices by |
| 193 | // to both the display and the shadow. | 211 | // `init_shadow()` (main.rs) — via the fb-aware page allocator so it |
| 194 | let words = (pitch * height) / 4; | 212 | // can never land on the framebuffer (see init_shadow). Nothing is |
| 195 | unsafe { | 213 | // drawn between fb.clear() here and init_shadow(), so the blank |
| 196 | if words > 0 { | 214 | // shadow is in sync with the cleared display. |
| 197 | let v: Vec<u32> = vec![0; words]; | ||
| 198 | SHADOW = Box::into_raw(v.into_boxed_slice()) as *mut u32; | ||
| 199 | SHADOW_WORDS = words; | ||
| 200 | } | ||
| 201 | } | ||
| 202 | 215 | ||
| 203 | // Re-stash the post-SetMode region (base can move) for the page | 216 | // Re-stash the post-SetMode region (base can move) for the page |
| 204 | // allocator, and log the authoritative geometry for diagnostics. | 217 | // allocator, and log the authoritative geometry for diagnostics. |
| @@ -224,6 +237,87 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 224 | } | 237 | } |
| 225 | } | 238 | } |
| 226 | 239 | ||
| 240 | /// Allocate the RAM backbuffer. Must run *after* | ||
| 241 | /// `memory::init_page_allocator` (which excludes the GOP framebuffer | ||
| 242 | /// pages per-page), and before any text is painted. | ||
| 243 | /// | ||
| 244 | /// The heap (pre-ExitBootServices) is excluded from the fb region only | ||
| 245 | /// by a coarse overlap subtraction that still leaves one contiguous | ||
| 246 | /// free block *spanning* the fb hole — a 1.9 MB shadow allocation could | ||
| 247 | /// therefore land *on* the framebuffer on machines where firmware | ||
| 248 | /// reports it as ConventionalMemory. A shadow aliased over the display | ||
| 249 | /// makes every scroll a 1.8 MB MMIO→MMIO memcpy, which can wedge a | ||
| 250 | /// real uncached framebuffer (a stall, not a fault — hence no dump). | ||
| 251 | /// The page allocator marks those pages used, so the shadow can never | ||
| 252 | /// land there. Heap allocation is kept only as a fallback, with an | ||
| 253 | /// explicit overlap rejection. | ||
| 254 | pub fn init_shadow() { | ||
| 255 | unsafe { | ||
| 256 | let Some(fb) = fb().as_ref() else { | ||
| 257 | return; | ||
| 258 | }; | ||
| 259 | let words = (fb.pitch * fb.height) / 4; | ||
| 260 | if words == 0 { | ||
| 261 | return; | ||
| 262 | } | ||
| 263 | let bytes = words * 4; | ||
| 264 | let pages = bytes.div_ceil(crate::memory::PAGE_SIZE); | ||
| 265 | let mut origin = "none"; | ||
| 266 | |||
| 267 | // Primary: fb-aware page allocator (fb pages already marked | ||
| 268 | // used by init_page_allocator). | ||
| 269 | if let Some(p) = crate::memory::alloc_pages(pages) { | ||
| 270 | SHADOW = p as *mut u32; | ||
| 271 | SHADOW_WORDS = words; | ||
| 272 | origin = "page-alloc"; | ||
| 273 | for i in 0..words { | ||
| 274 | *SHADOW.add(i) = fb.bg; | ||
| 275 | } | ||
| 276 | } else { | ||
| 277 | // Fallback: heap, rejected if it overlaps the fb region. | ||
| 278 | let v: alloc::vec::Vec<u32> = alloc::vec![0; words]; | ||
| 279 | let p = v.as_ptr() as usize; | ||
| 280 | let overlaps = FB_REGION_SIZE > 0 | ||
| 281 | && p < FB_REGION_BASE + FB_REGION_SIZE | ||
| 282 | && p + bytes > FB_REGION_BASE; | ||
| 283 | if !overlaps { | ||
| 284 | SHADOW = v.as_ptr() as *mut u32; | ||
| 285 | SHADOW_WORDS = words; | ||
| 286 | origin = "heap"; | ||
| 287 | core::mem::forget(v); // shadow owns the memory now | ||
| 288 | } else { | ||
| 289 | drop(v); | ||
| 290 | } | ||
| 291 | } | ||
| 292 | let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE); | ||
| 293 | let (shadow, swords) = (SHADOW as usize, SHADOW_WORDS); | ||
| 294 | crate::serial_println!( | ||
| 295 | "gop: shadow @ {:#x} words={} fb={:#x}..{:#x} origin={}", | ||
| 296 | shadow, | ||
| 297 | swords, | ||
| 298 | rbase, | ||
| 299 | rbase + rsize, | ||
| 300 | origin | ||
| 301 | ); | ||
| 302 | // Real hardware has no serial console: surface a degraded shadow | ||
| 303 | // (heap fallback or rejected allocation) as a visible warning | ||
| 304 | // band so a boot on hardware shows it on the screen. | ||
| 305 | if origin != "page-alloc" { | ||
| 306 | let msg = if SHADOW.is_null() { | ||
| 307 | "gop: WARNING — shadow allocation rejected (fb overlap); no-shadow fallback" | ||
| 308 | } else { | ||
| 309 | "gop: WARNING — shadow from heap (page allocator unavailable)" | ||
| 310 | }; | ||
| 311 | crate::serial_println!("{}", msg); | ||
| 312 | let lines: alloc::vec::Vec<&str> = msg.as_bytes() | ||
| 313 | .chunks(100) | ||
| 314 | .map(|c| core::str::from_utf8(c).unwrap_or("?")) | ||
| 315 | .collect(); | ||
| 316 | dump_lines(&lines); | ||
| 317 | } | ||
| 318 | } | ||
| 319 | } | ||
| 320 | |||
| 227 | // --------------------------------------------------------------------------- | 321 | // --------------------------------------------------------------------------- |
| 228 | // Public text API — the text surface now lives in `crate::vt`; these wrappers | 322 | // Public text API — the text surface now lives in `crate::vt`; these wrappers |
| 229 | // keep callers (shell, macros) unchanged. | 323 | // keep callers (shell, macros) unchanged. |
| @@ -296,7 +390,10 @@ pub(crate) fn push_rect(x: usize, y: usize, w: usize, h: usize) { | |||
| 296 | if row_w == 0 { | 390 | if row_w == 0 { |
| 297 | continue; | 391 | continue; |
| 298 | } | 392 | } |
| 299 | ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, row_w); | 393 | for k in 0..row_w { |
| 394 | stall_display(); | ||
| 395 | ptr::write_volatile(fb.base.add((off + k) * 4) as *mut u32, *SHADOW.add(off + k)); | ||
| 396 | } | ||
| 300 | } | 397 | } |
| 301 | } | 398 | } |
| 302 | overlay_cursor(x, y, w, h); | 399 | overlay_cursor(x, y, w, h); |
| @@ -530,6 +627,7 @@ fn draw_bmp(x: usize, y: usize, color: u32) { | |||
| 530 | let off = yy * row_words + xx; | 627 | let off = yy * row_words + xx; |
| 531 | // Clamp to the display mapping (defensive). | 628 | // Clamp to the display mapping (defensive). |
| 532 | if off < fb.framebuffer_size / 4 { | 629 | if off < fb.framebuffer_size / 4 { |
| 630 | stall_display(); | ||
| 533 | ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color); | 631 | ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color); |
| 534 | } | 632 | } |
| 535 | } | 633 | } |
| @@ -606,8 +704,14 @@ impl Framebuffer { | |||
| 606 | } | 704 | } |
| 607 | return; | 705 | return; |
| 608 | } | 706 | } |
| 609 | // Shift the shadow up n rows (cached RAM — fast). | 707 | // Shift the shadow up n rows. Per-word stores, never a |
| 610 | ptr::copy(SHADOW.add(n * row_words), SHADOW, words - n * row_words); | 708 | // bulk REP MOVS: the shadow is cached RAM so this is fast, |
| 709 | // and it stays safe even if a future allocation ever aliased | ||
| 710 | // the shadow over an uncached framebuffer (MMIO→MMIO fast | ||
| 711 | // string ops can wedge real GPUs). | ||
| 712 | for i in 0..(words - n * row_words) { | ||
| 713 | *SHADOW.add(i) = *SHADOW.add(n * row_words + i); | ||
| 714 | } | ||
| 611 | // Clear the exposed bottom rows in the shadow. | 715 | // Clear the exposed bottom rows in the shadow. |
| 612 | for i in (words - n * row_words)..words { | 716 | for i in (words - n * row_words)..words { |
| 613 | *SHADOW.add(i) = self.bg; | 717 | *SHADOW.add(i) = self.bg; |
| @@ -630,6 +734,7 @@ impl Framebuffer { | |||
| 630 | let push_words = words.min(self.framebuffer_size / 4); | 734 | let push_words = words.min(self.framebuffer_size / 4); |
| 631 | let dst = self.base as *mut u32; | 735 | let dst = self.base as *mut u32; |
| 632 | for i in 0..push_words { | 736 | for i in 0..push_words { |
| 737 | stall_display(); | ||
| 633 | ptr::write_volatile(dst.add(i), ptr::read(SHADOW.add(i))); | 738 | ptr::write_volatile(dst.add(i), ptr::read(SHADOW.add(i))); |
| 634 | } | 739 | } |
| 635 | // The scroll copied the shadow (which never contains the | 740 | // The scroll copied the shadow (which never contains the |
