summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--CONTEXT.md28
-rw-r--r--Cargo.lock6
-rw-r--r--Cargo.toml2
-rw-r--r--rsdos/src/main.rs1
-rw-r--r--rsnix/src/gop.rs139
-rw-r--r--shell/src/lib.rs12
6 files changed, 158 insertions, 30 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index bf1e01f..1a1470c 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -113,17 +113,21 @@ Workspace version is `a.b.c` (single version for all crates):
113 113
114Bump the workspace version in the root `Cargo.toml` when a change of 114Bump the workspace version in the root `Cargo.toml` when a change of
115that class lands — do not leave the version to drift. Current: 115that class lands — do not leave the version to drift. Current:
116`0.5.3` — the GUI era landed with 0.5.0 (graphics layer + cursor 116`0.5.4` — the GUI era landed with 0.5.0 (graphics layer + cursor
117compositor); 0.5.1 fixed the canvas at 800×600; 0.5.2 added fault 117compositor); 0.5.1 fixed the canvas at 800×600; 0.5.2 added fault
118diagnostics (screen + serial dumps for every CPU exception) and 118diagnostics (screen + serial dumps for every CPU exception) and
119real-hardware hardening (framebuffer-size clamping, fb-region exclusion 119real-hardware hardening (framebuffer-size clamping, fb-region exclusion
120from the allocators, authoritative fb geometry logging); 0.5.3 fixed 120from the allocators, authoritative fb geometry logging); 0.5.3 changed
121the real-hardware scroll wedge — the scroll's full-frame push is now 121the scroll's full-frame push to per-word volatile stores like the
122per-word volatile stores like the boot-time `clear()` instead of one 122boot-time `clear()` instead of one 1.92 MB rep-movsb (a REP MOVS to an
1231.92 MB rep-movsb, which can stall forever on an uncached real-GPU 123uncached real-GPU framebuffer can stall forever — a stall, not an
124framebuffer (a stall, not an exception — hence no crash dump). A 124exception — hence no crash dump); 0.5.4 allocates the shadow
125future major GUI overhaul (graphics windows beyond the text surface) 125backbuffer from the fb-aware page allocator (it can no longer land on
126would be the `a` bump (1.0.0). 126the framebuffer — the heap free list spans the fb hole, so a 1.9 MB
127shadow could alias the display and turn every scroll into an MMIO→MMIO
128memcpy) and converts the scroll's shadow memmove to per-word stores as
129a belt-and-suspenders measure. A future major GUI overhaul (graphics
130windows beyond the text surface) would be the `a` bump (1.0.0).
127 131
128## Project layout 132## Project layout
129 133
@@ -562,7 +566,13 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target:
562 uncached real-GPU framebuffer wedged the CPU on some hardware (a 566 uncached real-GPU framebuffer wedged the CPU on some hardware (a
563 stall, not an exception — no dump fired), while the boot-time 567 stall, not an exception — no dump fired), while the boot-time
564 `clear()` — the same per-word pattern — always worked. QEMU's 568 `clear()` — the same per-word pattern — always worked. QEMU's
565 RAM-backed framebuffer never showed it. 569 RAM-backed framebuffer never showed it. **The shadow itself is
570 allocated from the fb-aware page allocator (0.5.4)** so it can
571 never land on the framebuffer: the heap's free list spans the fb
572 hole, so a 1.9 MB heap shadow could alias the display, making
573 every scroll an MMIO→MMIO memcpy that wedges real hardware. The
574 scroll's shadow memmove is also per-word stores (0.5.4) as a
575 belt-and-suspenders guard against any future fb-aliased shadow.
566 2. deferred scroll batching (`vt::PENDING_SCROLLS`): the logical grid 576 2. deferred scroll batching (`vt::PENDING_SCROLLS`): the logical grid
567 scrolls instantly in RAM while the framebuffer shift+push is 577 scrolls instantly in RAM while the framebuffer shift+push is
568 deferred; N pending scrolls cost one push. Bursts of streaming 578 deferred; N pending scrolls cost one push. Bursts of streaming
diff --git a/Cargo.lock b/Cargo.lock
index 65166d4..2796caa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 4
4 4
5[[package]] 5[[package]]
6name = "rsdos" 6name = "rsdos"
7version = "0.5.3" 7version = "0.5.4"
8dependencies = [ 8dependencies = [
9 "rsnix", 9 "rsnix",
10 "shell", 10 "shell",
@@ -19,11 +19,11 @@ dependencies = [
19 19
20[[package]] 20[[package]]
21name = "rsnix" 21name = "rsnix"
22version = "0.5.3" 22version = "0.5.4"
23 23
24[[package]] 24[[package]]
25name = "shell" 25name = "shell"
26version = "0.5.3" 26version = "0.5.4"
27dependencies = [ 27dependencies = [
28 "rsdosedit", 28 "rsdosedit",
29 "rsnix", 29 "rsnix",
diff --git a/Cargo.toml b/Cargo.toml
index 6c16784..b7abb0e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@ resolver = "2"
3members = ["rsnix", "shell", "rsdos", "rsdosedit"] 3members = ["rsnix", "shell", "rsdos", "rsdosedit"]
4 4
5[workspace.package] 5[workspace.package]
6version = "0.5.3" 6version = "0.5.4"
7edition = "2024" 7edition = "2024"
8 8
9# no_std UEFI kernel: no unwinder available, so panic must abort. 9# no_std UEFI kernel: no unwinder available, so panic must abort.
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs
index 6dbad5f..3153930 100644
--- a/rsdos/src/main.rs
+++ b/rsdos/src/main.rs
@@ -172,6 +172,7 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut
172 } else { 172 } else {
173 rsnix::serial_println!("pages: init FAILED"); 173 rsnix::serial_println!("pages: init FAILED");
174 } 174 }
175 rsnix::gop::init_shadow();
175 if rsnix::memory::page_self_test() { 176 if rsnix::memory::page_self_test() {
176 rsnix::serial_println!("pages: self-test OK (alloc/free/runs)"); 177 rsnix::serial_println!("pages: self-test OK (alloc/free/runs)");
177 } else { 178 } else {
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.
6use crate::font; 6use crate::font;
7use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; 7use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB};
8use alloc::boxed::Box;
9use alloc::vec;
10use alloc::vec::Vec;
11use core::fmt; 8use core::fmt;
12use core::ptr; 9use core::ptr;
13use core::sync::atomic::{AtomicU64, AtomicU8, Ordering}; 10use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
14 11
15/// Framebuffer state captured before ExitBootServices. 12/// Framebuffer state captured before ExitBootServices.
16pub struct Framebuffer { 13pub struct Framebuffer {
@@ -74,6 +71,27 @@ pub fn framebuffer_region() -> Option<(usize, usize)> {
74static mut SHADOW: *mut u32 = core::ptr::null_mut(); 71static mut SHADOW: *mut u32 = core::ptr::null_mut();
75static mut SHADOW_WORDS: usize = 0; 72static 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.
79pub static DISPLAY_STALL: AtomicU32 = AtomicU32::new(0);
80
81#[inline]
82fn 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.
254pub 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
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
index 3090776..b2c4a1b 100644
--- a/shell/src/lib.rs
+++ b/shell/src/lib.rs
@@ -722,6 +722,7 @@ write `{} <path> TIL <marker>`",
722 "kill" => cmd_kill(&parsed), 722 "kill" => cmd_kill(&parsed),
723 "demo" => cmd_demo(&parsed), 723 "demo" => cmd_demo(&parsed),
724 "fault" => cmd_fault(), 724 "fault" => cmd_fault(),
725 "stall" => cmd_stall(&parsed),
725 _ => term_err(&alloc::format!("unknown command: {}", cmd)), 726 _ => term_err(&alloc::format!("unknown command: {}", cmd)),
726 } 727 }
727} 728}
@@ -819,6 +820,17 @@ fn cmd_fault() {
819 } 820 }
820} 821}
821 822
823/// Set the display-store stall (cycles per word) — diagnostics knob that
824/// simulates the per-write cost of an uncached real-GPU framebuffer so
825/// the preemptive scheduler races long scroll pushes like on real
826/// hardware. `stall 0` disables (default; QEMU RAM framebuffers are
827/// instant).
828fn cmd_stall(p: &Parsed) {
829 let n: u32 = p.args.first().and_then(|a| a.parse().ok()).unwrap_or(0);
830 rsnix::gop::DISPLAY_STALL.store(n, core::sync::atomic::Ordering::Relaxed);
831 term_println(&alloc::format!("stall: display store stall = {}", n));
832}
833
822fn cmd_clear() { 834fn cmd_clear() {
823 rsnix::gop::clear_screen(); 835 rsnix::gop::clear_screen();
824} 836}