diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-02 01:59:45 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-02 01:59:45 +0200 |
| commit | 5185fe98d404c6c15a52f513313042ced16c350a (patch) | |
| tree | 0fd00f065d9d566e59da1af463befeace36b1fc9 | |
| parent | v0.5.5: visible boot progress + panic-on-screen for real hardware (diff) | |
| download | rs-dos-5185fe98d404c6c15a52f513313042ced16c350a.tar.gz rs-dos-5185fe98d404c6c15a52f513313042ced16c350a.zip | |
v0.5.5: self-diagnosing boot — vt refuses 0x0 geometry, panic band shows last stage
The user's real-hardware boot showed 'PANIC: index out of bounds (len 0,
index 0) at vt.rs:146' — the vt grid was created empty because the
framebuffer geometry was 0x0 at vt::init (GOP missing or the active
mode reporting zero resolution). The v0.5.4 blank screen was the same
panic, previously invisible (no screen dump in the panic handler).
- vt::init: rows/cols == 0 now halts with a visible white-on-red
'FATAL: no framebuffer geometry — GOP missing or active mode is 0x0'
band instead of building an empty grid that panics at the first
cell write.
- boot_marker records the last boot stage; the panic band appends it
as a 4th line so a crash shows exactly how far boot got.
- The gop+vt marker now carries the measured geometry ('rsnix: gop
800x600 vt 100x37 ok') so a good boot is self-confirming on the
screen.
- All 5 test suites pass; build/clippy clean.
| -rw-r--r-- | rsdos/src/main.rs | 16 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 21 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 20 |
3 files changed, 55 insertions, 2 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index b6393ee..aa116e4 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -5,6 +5,8 @@ | |||
| 5 | use core::panic::PanicInfo; | 5 | use core::panic::PanicInfo; |
| 6 | use rsnix::uefi::*; | 6 | use rsnix::uefi::*; |
| 7 | 7 | ||
| 8 | extern crate alloc; | ||
| 9 | |||
| 8 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
| 9 | // Panic handler | 11 | // Panic handler |
| 10 | // --------------------------------------------------------------------------- | 12 | // --------------------------------------------------------------------------- |
| @@ -47,9 +49,11 @@ fn panic(info: &PanicInfo) -> ! { | |||
| 47 | } | 49 | } |
| 48 | // Real hardware has no serial console — put the panic on the screen | 50 | // Real hardware has no serial console — put the panic on the screen |
| 49 | // too (white-on-red band), so a pre-shell panic is not a silent | 51 | // too (white-on-red band), so a pre-shell panic is not a silent |
| 50 | // blank screen. | 52 | // blank screen. The last boot stage is appended so the user can |
| 53 | // see how far boot got before the crash. | ||
| 51 | let mut loc_buf = [0u8; 96]; | 54 | let mut loc_buf = [0u8; 96]; |
| 52 | let mut msg_buf = [0u8; 128]; | 55 | let mut msg_buf = [0u8; 128]; |
| 56 | let mut stage_buf = [0u8; 96]; | ||
| 53 | let loc_n = { | 57 | let loc_n = { |
| 54 | let mut w = rsnix::strbuf::FmtBuf::new(&mut loc_buf); | 58 | let mut w = rsnix::strbuf::FmtBuf::new(&mut loc_buf); |
| 55 | if let Some(loc) = info.location() { | 59 | if let Some(loc) = info.location() { |
| @@ -64,10 +68,17 @@ fn panic(info: &PanicInfo) -> ! { | |||
| 64 | let _ = write!(&mut w, "{}", info.message()); | 68 | let _ = write!(&mut w, "{}", info.message()); |
| 65 | w.len() | 69 | w.len() |
| 66 | }; | 70 | }; |
| 71 | let stage_n = { | ||
| 72 | let s = rsnix::gop::last_boot_stage(); | ||
| 73 | let n = s.len().min(stage_buf.len()); | ||
| 74 | stage_buf[..n].copy_from_slice(&s.as_bytes()[..n]); | ||
| 75 | n | ||
| 76 | }; | ||
| 67 | let lines = [ | 77 | let lines = [ |
| 68 | "*** PANIC ***", | 78 | "*** PANIC ***", |
| 69 | core::str::from_utf8(&msg_buf[..msg_n]).unwrap_or("?"), | 79 | core::str::from_utf8(&msg_buf[..msg_n]).unwrap_or("?"), |
| 70 | core::str::from_utf8(&loc_buf[..loc_n]).unwrap_or("?"), | 80 | core::str::from_utf8(&loc_buf[..loc_n]).unwrap_or("?"), |
| 81 | core::str::from_utf8(&stage_buf[..stage_n]).unwrap_or(""), | ||
| 71 | ]; | 82 | ]; |
| 72 | rsnix::gop::dump_lines(&lines); | 83 | rsnix::gop::dump_lines(&lines); |
| 73 | loop { | 84 | loop { |
| @@ -150,7 +161,8 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 150 | rsnix::vt::init(); | 161 | rsnix::vt::init(); |
| 151 | let (rows, cols) = rsnix::vt::size(); | 162 | let (rows, cols) = rsnix::vt::size(); |
| 152 | rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); | 163 | rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); |
| 153 | rsnix::gop::boot_marker("rsnix: gop+vt ok"); | 164 | let (fw, fh) = rsnix::gop::pixel_dims(); |
| 165 | rsnix::gop::boot_marker(&alloc::format!("rsnix: gop {}x{} vt {}x{} ok", fw, fh, cols, rows)); | ||
| 154 | 166 | ||
| 155 | // ExitBootServices — we now own the machine | 167 | // ExitBootServices — we now own the machine |
| 156 | if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { | 168 | if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { |
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 7b97d14..90c24bf 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -71,6 +71,20 @@ pub fn framebuffer_region() -> Option<(usize, usize)> { | |||
| 71 | static mut SHADOW: *mut u32 = core::ptr::null_mut(); | 71 | static mut SHADOW: *mut u32 = core::ptr::null_mut(); |
| 72 | static mut SHADOW_WORDS: usize = 0; | 72 | static mut SHADOW_WORDS: usize = 0; |
| 73 | 73 | ||
| 74 | /// Last boot-progress stage (set by `boot_marker`); the panic handler | ||
| 75 | /// appends it to the on-screen band so a crash shows where boot died. | ||
| 76 | static mut LAST_BOOT_STAGE: [u8; 96] = [0; 96]; | ||
| 77 | static mut LAST_BOOT_STAGE_LEN: usize = 0; | ||
| 78 | const STAGE_MAX: usize = 96; | ||
| 79 | |||
| 80 | /// The last boot stage string (for the panic band). | ||
| 81 | pub fn last_boot_stage() -> alloc::string::String { | ||
| 82 | unsafe { | ||
| 83 | alloc::string::String::from_utf8_lossy(&LAST_BOOT_STAGE[..LAST_BOOT_STAGE_LEN]) | ||
| 84 | .into_owned() | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 74 | /// Diagnostic: when > 0, every display store busy-waits ~this many | 88 | /// Diagnostic: when > 0, every display store busy-waits ~this many |
| 75 | /// cycles first, simulating the per-write bus cost of an uncached real | 89 | /// cycles first, simulating the per-write bus cost of an uncached real |
| 76 | /// GOP framebuffer (QEMU's framebuffer is RAM — instant). Lets us | 90 | /// GOP framebuffer (QEMU's framebuffer is RAM — instant). Lets us |
| @@ -338,6 +352,13 @@ pub fn init_shadow() { | |||
| 338 | /// direct-display glyph path (the shadow may not exist yet in early | 352 | /// direct-display glyph path (the shadow may not exist yet in early |
| 339 | /// boot); the shell banner overwrites row 0 at first paint. | 353 | /// boot); the shell banner overwrites row 0 at first paint. |
| 340 | pub fn boot_marker(msg: &str) { | 354 | pub fn boot_marker(msg: &str) { |
| 355 | // Record the stage so a later panic band can show how far boot got. | ||
| 356 | unsafe { | ||
| 357 | let n = msg.len().min(STAGE_MAX); | ||
| 358 | let dst = core::ptr::addr_of_mut!(LAST_BOOT_STAGE) as *mut u8; | ||
| 359 | core::ptr::copy_nonoverlapping(msg.as_ptr(), dst, n); | ||
| 360 | LAST_BOOT_STAGE_LEN = n; | ||
| 361 | } | ||
| 341 | let Some(fb) = fb().as_ref() else { | 362 | let Some(fb) = fb().as_ref() else { |
| 342 | return; | 363 | return; |
| 343 | }; | 364 | }; |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 53821b7..4578f6c 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -96,6 +96,26 @@ static mut CUR: usize = 0; | |||
| 96 | /// Allocate the cell grid. Call once after `gop::init()`. | 96 | /// Allocate the cell grid. Call once after `gop::init()`. |
| 97 | pub fn init() { | 97 | pub fn init() { |
| 98 | let (cols, rows) = gop::dims(); | 98 | let (cols, rows) = gop::dims(); |
| 99 | if rows == 0 || cols == 0 { | ||
| 100 | // No usable framebuffer geometry: GOP is missing entirely or the | ||
| 101 | // active mode reports 0x0. Without a display there is nothing | ||
| 102 | // the shell can do, so stop with a visible explanation instead | ||
| 103 | // of building an empty grid and panicking at the first cell | ||
| 104 | // write (a blank screen on real hardware, which has no serial | ||
| 105 | // console). | ||
| 106 | let lines = [ | ||
| 107 | "FATAL: no framebuffer geometry", | ||
| 108 | "GOP missing or active mode is 0x0", | ||
| 109 | "see the 'gop:' boot marker / serial log", | ||
| 110 | ]; | ||
| 111 | crate::gop::dump_lines(&lines); | ||
| 112 | crate::serial_println!("FATAL: no framebuffer geometry (GOP missing or 0x0 mode)"); | ||
| 113 | loop { | ||
| 114 | unsafe { | ||
| 115 | core::arch::asm!("cli; hlt"); | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 99 | unsafe { | 119 | unsafe { |
| 100 | ROWS = rows; | 120 | ROWS = rows; |
| 101 | COLS = cols; | 121 | COLS = cols; |
