From 045c9ddf71b62dff538cd3ae1963166ab5c145a0 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 2 Aug 2026 01:52:10 +0200 Subject: v0.5.5: visible boot progress + panic-on-screen for real hardware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real hardware has no serial console: a pre-shell panic was a silent blank screen (the panic handler only wrote to serial, then cli;hlt). v0.5.4's shadow change made this window reachable — if the fb-aware page allocation or the heap fallback failed on hardware, the kernel halted before the shell banner with zero screen output. - gop::boot_marker(): every boot stage now draws straight to the framebuffer (row 0, white-on-black). The last marker visible before a hang identifies where the kernel died. - Panic handler: pre-shell panics now draw a white-on-red '*** PANIC *** at file:line' band via gop::dump_lines. - init_shadow(): heap fallback is panic-proof (try_reserve_exact + resize instead of vec![0; words] which aborts on OOM); page-alloc result defensively re-checked against the fb region; any failure degrades to no-shadow mode with a visible warning band. - Verified: full chain (rsboot -> kernel) boots with markers, shell banner renders, all 5 test suites pass. --- CONTEXT.md | 11 +++++++-- Cargo.lock | 6 ++--- Cargo.toml | 2 +- rsdos/src/main.rs | 30 +++++++++++++++++++++++ rsnix/src/gop.rs | 72 ++++++++++++++++++++++++++++++++++++++++++------------- 5 files changed, 99 insertions(+), 22 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 1a1470c..196565c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -126,8 +126,15 @@ backbuffer from the fb-aware page allocator (it can no longer land on the framebuffer — the heap free list spans the fb hole, so a 1.9 MB shadow could alias the display and turn every scroll into an MMIO→MMIO memcpy) and converts the scroll's shadow memmove to per-word stores as -a belt-and-suspenders measure. A future major GUI overhaul (graphics -windows beyond the text surface) would be the `a` bump (1.0.0). +a belt-and-suspenders measure; 0.5.5 makes every boot stage visible on +real hardware (no serial console): `gop::boot_marker` prints the boot +progress straight to the framebuffer (the last marker before a hang +identifies the death site) and pre-shell panics now draw a white-on-red +`*** PANIC ***` band instead of a silent blank screen, and the shadow +fallback allocation is panic-proof (try_reserve; a heap OOM degrades +to no-shadow mode instead of halting). A future major GUI overhaul +(graphics windows beyond the text surface) would be the `a` bump +(1.0.0). ## Project layout diff --git a/Cargo.lock b/Cargo.lock index 2796caa..431db32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "rsdos" -version = "0.5.4" +version = "0.5.5" dependencies = [ "rsnix", "shell", @@ -19,11 +19,11 @@ dependencies = [ [[package]] name = "rsnix" -version = "0.5.4" +version = "0.5.5" [[package]] name = "shell" -version = "0.5.4" +version = "0.5.5" dependencies = [ "rsdosedit", "rsnix", diff --git a/Cargo.toml b/Cargo.toml index b7abb0e..76139bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["rsnix", "shell", "rsdos", "rsdosedit"] [workspace.package] -version = "0.5.4" +version = "0.5.5" edition = "2024" # no_std UEFI kernel: no unwinder available, so panic must abort. diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 3153930..b6393ee 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs @@ -45,6 +45,31 @@ fn panic(info: &PanicInfo) -> ! { loc.line() ); } + // Real hardware has no serial console — put the panic on the screen + // too (white-on-red band), so a pre-shell panic is not a silent + // blank screen. + let mut loc_buf = [0u8; 96]; + let mut msg_buf = [0u8; 128]; + let loc_n = { + let mut w = rsnix::strbuf::FmtBuf::new(&mut loc_buf); + if let Some(loc) = info.location() { + let _ = write!(&mut w, "at {}:{}", loc.file(), loc.line()); + } else { + let _ = write!(&mut w, "(no location)"); + } + w.len() + }; + let msg_n = { + let mut w = rsnix::strbuf::FmtBuf::new(&mut msg_buf); + let _ = write!(&mut w, "{}", info.message()); + w.len() + }; + let lines = [ + "*** PANIC ***", + core::str::from_utf8(&msg_buf[..msg_n]).unwrap_or("?"), + core::str::from_utf8(&loc_buf[..loc_n]).unwrap_or("?"), + ]; + rsnix::gop::dump_lines(&lines); loop { unsafe { core::arch::asm!("cli; hlt"); @@ -121,9 +146,11 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut } else { rsnix::serial_println!("gop: not found (0x{:x})", gop_result); } + rsnix::gop::boot_marker("rsnix v0.5.5 booting.."); rsnix::vt::init(); let (rows, cols) = rsnix::vt::size(); rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); + rsnix::gop::boot_marker("rsnix: gop+vt ok"); // ExitBootServices — we now own the machine if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { @@ -158,6 +185,7 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut } rsnix::interrupts::init(); rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); + rsnix::gop::boot_marker("rsnix: serial+ps2+gdt+intr ok"); dump_memory_summary(); @@ -173,6 +201,7 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut rsnix::serial_println!("pages: init FAILED"); } rsnix::gop::init_shadow(); + rsnix::gop::boot_marker("rsnix: shadow ok"); if rsnix::memory::page_self_test() { rsnix::serial_println!("pages: self-test OK (alloc/free/runs)"); } else { @@ -228,6 +257,7 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut } rsnix::serial_println!("boot: entering shell"); + rsnix::gop::boot_marker("rsnix: boot done — shell"); if rsnix::ps2::mouse_present() { rsnix::cursor::spawn(); } else { diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 437bf9d..7b97d14 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs @@ -265,28 +265,40 @@ pub fn init_shadow() { let mut origin = "none"; // Primary: fb-aware page allocator (fb pages already marked - // used by init_page_allocator). + // used by init_page_allocator). Defensively refuse the range if + // it ever overlaps the fb region anyway. if let Some(p) = crate::memory::alloc_pages(pages) { - SHADOW = p as *mut u32; - SHADOW_WORDS = words; - origin = "page-alloc"; - for i in 0..words { - *SHADOW.add(i) = fb.bg; - } - } else { - // Fallback: heap, rejected if it overlaps the fb region. - let v: alloc::vec::Vec = alloc::vec![0; words]; - let p = v.as_ptr() as usize; let overlaps = FB_REGION_SIZE > 0 && p < FB_REGION_BASE + FB_REGION_SIZE && p + bytes > FB_REGION_BASE; if !overlaps { - SHADOW = v.as_ptr() as *mut u32; + SHADOW = p as *mut u32; SHADOW_WORDS = words; - origin = "heap"; - core::mem::forget(v); // shadow owns the memory now - } else { - drop(v); + origin = "page-alloc"; + for i in 0..words { + *SHADOW.add(i) = fb.bg; + } + } + } + if origin == "none" { + // Fallback: heap — must never panic here: an OOM would halt + // the kernel before the shell banner, a blank screen on real + // hardware (the old `vec![0; words]` could abort). On any + // failure we degrade to no-shadow mode instead. + #[allow(clippy::slow_vector_initialization)] // deliberate: try_reserve must not panic + let mut v: alloc::vec::Vec = alloc::vec::Vec::new(); + if v.try_reserve_exact(words).is_ok() { + v.resize(words, 0); // can't fail after the reserve + let p = v.as_ptr() as usize; + let overlaps = FB_REGION_SIZE > 0 + && p < FB_REGION_BASE + FB_REGION_SIZE + && p + bytes > FB_REGION_BASE; + if !overlaps { + SHADOW = v.as_ptr() as *mut u32; + SHADOW_WORDS = words; + origin = "heap"; + core::mem::forget(v); // shadow owns the memory now + } } } let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE); @@ -319,6 +331,34 @@ pub fn init_shadow() { } } +/// Draw a one-line boot-progress marker to the display (row 0, +/// white-on-black). Real hardware has no serial console, so every +/// boot stage is written straight to the framebuffer — the last visible +/// marker before a hang identifies where the kernel died. Uses the +/// direct-display glyph path (the shadow may not exist yet in early +/// boot); the shell banner overwrites row 0 at first paint. +pub fn boot_marker(msg: &str) { + let Some(fb) = fb().as_ref() else { + return; + }; + let cols = fb.width / 8; + let white = (0xFFu8, 0xFFu8, 0xFFu8); + let black = (0x00u8, 0x00u8, 0x00u8); + let mut c = 0usize; + for &b in msg.as_bytes().iter().take(cols) { + if b < 0x80 { + blit_glyph(c, 0, b as u16, white, black); + c += 1; + } + } + // Blank the rest of the row so a shorter message can't leave stale + // glyphs behind. + while c < cols { + blit_glyph(c, 0, b' ' as u16, white, black); + c += 1; + } +} + // --------------------------------------------------------------------------- // Public text API — the text surface now lives in `crate::vt`; these wrappers // keep callers (shell, macros) unchanged. -- cgit v1.2.3