summaryrefslogtreecommitdiffstats
path: root/rsnix/src/gop.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 22:28:34 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 22:28:34 +0200
commitab7d87bcde31c1681ea09c30534dde840a58d6fa (patch)
tree42eed2c86663cf6ecfa3507ca46399e31b441c99 /rsnix/src/gop.rs
parentrsboot: track latest upstream HEAD (92367a8) instead of pinned ee1c85b (diff)
downloadrs-dos-ab7d87bcde31c1681ea09c30534dde840a58d6fa.tar.gz
rs-dos-ab7d87bcde31c1681ea09c30534dde840a58d6fa.zip
rsnix v0.5.2: fault diagnostics + real-hardware scroll-crash hardening
The real-hardware scroll crash (white bands + instant freeze, QEMU-clean) was invisible because the only trace was a single serial line on machines with no serial console. This makes every crash self-identifying and hardens the real-hardware failure surfaces: - Every CPU exception vector 0-31 now has an IDT entry (macro-generated stubs; 8/14 keep their IST stacks). A fault in the shell prints a white-on-red band to the framebuffer via gop::dump_lines (raw fb writes bypassing the vt, fully clamped) plus a parseable serial line: '!!! <kind> vec=N err=… rip=… cr2=… rsp=… rflags=… task=N "name"', then halts — instead of converging to a cryptic double fault. Background-task faults abort the task as before. - Boot log now reports the authoritative post-SetMode geometry: 'gop: fb base=… size=… pitch=… WxH bpp=… bgr=…', so the next real machine's report identifies whether pitch*height exceeds the mapping. - Framebuffer writes (clear, push_rect, shift_up_n bulk push, cursor overlay) are clamped to the firmware's framebuffer_size: a pitch- padded mode degrades to a truncated view instead of faulting past the mapping. - The GOP framebuffer region is excluded from the page allocator (and subtracted from the heap's conventional-region pick) even when the firmware lists it as ConventionalMemory — kernel allocations can never overwrite the display again. The GOP protocol is located and peeked before the memory map is captured so the exclusion is known at heap-init time. - Shell gains a hidden 'fault' command (deliberate unmapped write) and 'make test-fault' verifies the dump: vec=14, cr2=0x400000000000, task=0, halt — no triple fault. All other suites re-pass. - CONTEXT.md: fault-dump format documented for real-hardware reports.
Diffstat (limited to '')
-rw-r--r--rsnix/src/gop.rs114
1 files changed, 110 insertions, 4 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index dad4505..47a2208 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -19,12 +19,49 @@ pub struct Framebuffer {
19 height: usize, 19 height: usize,
20 pitch: usize, // bytes per scan line 20 pitch: usize, // bytes per scan line
21 bytes_per_pixel: usize, 21 bytes_per_pixel: usize,
22 /// Size of the firmware's linear framebuffer mapping, in bytes.
23 /// Everything written to the display is clamped to this, so a mode
24 /// whose `pitch * height` exceeds the actual mapping (some real
25 /// firmware pads the scan-line pitch but sizes the buffer for the
26 /// resolution) degrades to a truncated view instead of faulting.
27 framebuffer_size: usize,
22 bg: u32, // packed BGRA or RGBA background colour 28 bg: u32, // packed BGRA or RGBA background colour
23 bgr: bool, 29 bgr: bool,
24} 30}
25 31
26static mut FB: Option<Framebuffer> = None; 32static mut FB: Option<Framebuffer> = None;
27 33
34/// The GOP framebuffer region (base + mapping size), stashed before the
35/// memory map is captured so the heap and page allocator can exclude it.
36/// Some real-world firmware lists the framebuffer's memory as
37/// `EfiConventionalMemory`; handing those pages out would let kernel
38/// allocations (stacks, DMA buffers) overwrite the display.
39static mut FB_REGION_BASE: usize = 0;
40static mut FB_REGION_SIZE: usize = 0;
41
42/// Stash the current GOP framebuffer region *before* the memory map is
43/// captured (must run before `memory::capture_memory_map`). Updated
44/// again after `SetMode` in `init` — the base can move.
45pub fn peek_fb_region(gop: &mut GraphicsOutputProtocol) {
46 unsafe {
47 let mode = &*gop.mode;
48 FB_REGION_BASE = mode.framebuffer_base as usize;
49 FB_REGION_SIZE = mode.framebuffer_size;
50 }
51}
52
53/// The framebuffer region currently excluded from the allocators
54/// (base, size in bytes), if any.
55pub fn framebuffer_region() -> Option<(usize, usize)> {
56 unsafe {
57 if FB_REGION_SIZE == 0 {
58 None
59 } else {
60 Some((FB_REGION_BASE, FB_REGION_SIZE))
61 }
62 }
63}
64
28/// Software backbuffer: a RAM copy of the whole framebuffer (same pitch 65/// Software backbuffer: a RAM copy of the whole framebuffer (same pitch
29/// layout, u32 pixels). 66/// layout, u32 pixels).
30/// 67///
@@ -147,6 +184,7 @@ pub fn init(gop: &mut GraphicsOutputProtocol) {
147 height, 184 height,
148 pitch, 185 pitch,
149 bytes_per_pixel: bpp, 186 bytes_per_pixel: bpp,
187 framebuffer_size: mode.framebuffer_size,
150 bg, 188 bg,
151 bgr, 189 bgr,
152 }; 190 };
@@ -162,6 +200,23 @@ pub fn init(gop: &mut GraphicsOutputProtocol) {
162 } 200 }
163 } 201 }
164 202
203 // Re-stash the post-SetMode region (base can move) for the page
204 // allocator, and log the authoritative geometry for diagnostics.
205 unsafe {
206 FB_REGION_BASE = mode.framebuffer_base as usize;
207 FB_REGION_SIZE = mode.framebuffer_size;
208 }
209 crate::serial_println!(
210 "gop: fb base={:#x} size={:#x} pitch={:#x} {}x{} bpp={} bgr={}",
211 base as usize,
212 { mode.framebuffer_size },
213 pitch,
214 width,
215 height,
216 bpp,
217 bgr
218 );
219
165 fb.clear(); 220 fb.clear();
166 221
167 unsafe { 222 unsafe {
@@ -231,10 +286,17 @@ pub(crate) fn push_rect(x: usize, y: usize, w: usize, h: usize) {
231 let w = w.min(fb.width - x); 286 let w = w.min(fb.width - x);
232 let h = h.min(fb.height - y); 287 let h = h.min(fb.height - y);
233 let row_words = fb.pitch / 4; 288 let row_words = fb.pitch / 4;
289 // Words of the firmware's actual display mapping — per-row clamp so a
290 // pitch-padded mode never writes past the buffer.
291 let fb_words = fb.framebuffer_size / 4;
234 unsafe { 292 unsafe {
235 for dy in 0..h { 293 for dy in 0..h {
236 let off = (y + dy) * row_words + x; 294 let off = (y + dy) * row_words + x;
237 ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, w); 295 let row_w = w.min(fb_words.saturating_sub(off));
296 if row_w == 0 {
297 continue;
298 }
299 ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, row_w);
238 } 300 }
239 } 301 }
240 overlay_cursor(x, y, w, h); 302 overlay_cursor(x, y, w, h);
@@ -275,6 +337,39 @@ pub fn fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) {
275 push_rect(x, y, w, h); 337 push_rect(x, y, w, h);
276} 338}
277 339
340/// Print crash/fault lines directly to the framebuffer, bypassing the vt
341/// entirely (the vt state may be corrupt when an exception fires).
342///
343/// White-on-red band at the top of the screen, written through the same
344/// clamped glyph path — safe even if the fault was inside the vt/gop
345/// machinery, and visible on real hardware with no serial console.
346pub fn dump_lines(lines: &[&str]) {
347 let Some(fb) = fb().as_ref() else {
348 return;
349 };
350 let cols = fb.width / 8;
351 let rows = fb.height / 16;
352 let red = (0xC0u8, 0x00u8, 0x00u8);
353 let white = (0xFFu8, 0xFFu8, 0xFFu8);
354 let band = lines.len().min(rows);
355 // Red band behind the text.
356 if cols > 0 && band > 0 {
357 fill_rect(0, 0, cols * 8, band * 16, rgb(red.0, red.1, red.2));
358 }
359 for (i, line) in lines.iter().enumerate().take(band) {
360 let mut c = 0;
361 for &b in line.as_bytes() {
362 if c >= cols {
363 break;
364 }
365 if b < 0x80 {
366 blit_glyph(c, i, b as u16, white, red);
367 c += 1;
368 }
369 }
370 }
371}
372
278/// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. 373/// Read one pixel back from the shadow (RAM; cheap). Test/demo use only.
279pub fn shadow_pixel(x: usize, y: usize) -> Option<u32> { 374pub fn shadow_pixel(x: usize, y: usize) -> Option<u32> {
280 let fb = fb().as_ref()?; 375 let fb = fb().as_ref()?;
@@ -433,7 +528,10 @@ fn draw_bmp(x: usize, y: usize, color: u32) {
433 } 528 }
434 if row & (1u16 << (11 - dx)) != 0 { 529 if row & (1u16 << (11 - dx)) != 0 {
435 let off = yy * row_words + xx; 530 let off = yy * row_words + xx;
436 ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color); 531 // Clamp to the display mapping (defensive).
532 if off < fb.framebuffer_size / 4 {
533 ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color);
534 }
437 } 535 }
438 } 536 }
439 } 537 }
@@ -453,7 +551,10 @@ pub fn rgb(r: u8, g: u8, b: u8) -> u32 {
453// --------------------------------------------------------------------------- 551// ---------------------------------------------------------------------------
454impl Framebuffer { 552impl Framebuffer {
455 fn clear(&self) { 553 fn clear(&self) {
456 let size = self.pitch * self.height; 554 // Clamp the display write to the firmware's actual mapping — some
555 // real GPUs size the buffer for the resolution while the pitch
556 // (with scan-line padding) is larger.
557 let size = (self.pitch * self.height).min(self.framebuffer_size);
457 // Fill with background colour (32-bit writes), display + shadow. 558 // Fill with background colour (32-bit writes), display + shadow.
458 let mut p = self.base; 559 let mut p = self.base;
459 let end = unsafe { p.add(size) }; 560 let end = unsafe { p.add(size) };
@@ -511,7 +612,12 @@ impl Framebuffer {
511 // A bulk copy (rep movsb) writes in 64-byte bursts that 612 // A bulk copy (rep movsb) writes in 64-byte bursts that
512 // coalesce well on WC framebuffers — far fewer bus 613 // coalesce well on WC framebuffers — far fewer bus
513 // transactions than per-pixel stores. 614 // transactions than per-pixel stores.
514 ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, words); 615 //
616 // Clamp to the firmware's mapping size; a pitch-padded mode
617 // whose `pitch * height` exceeds the buffer degrades to a
618 // truncated view instead of faulting past the mapping.
619 let push_words = words.min(self.framebuffer_size / 4);
620 ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, push_words);
515 // The scroll copied the shadow (which never contains the 621 // The scroll copied the shadow (which never contains the
516 // cursor); re-composite it on top of the pushed frame. 622 // cursor); re-composite it on top of the pushed frame.
517 crate::gop::cursor_redraw(); 623 crate::gop::cursor_redraw();