diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-02 01:52:10 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-02 01:52:10 +0200 |
| commit | 045c9ddf71b62dff538cd3ae1963166ab5c145a0 (patch) | |
| tree | fd99757329f7b770f067aa5f0204c87068fea681 | |
| parent | gop: rustfmt collapse in init_shadow warning band (diff) | |
| download | rs-dos-045c9ddf71b62dff538cd3ae1963166ab5c145a0.tar.gz rs-dos-045c9ddf71b62dff538cd3ae1963166ab5c145a0.zip | |
v0.5.5: visible boot progress + panic-on-screen for real hardware
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 *** <msg> 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.
| -rw-r--r-- | CONTEXT.md | 11 | ||||
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 30 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 72 |
5 files changed, 99 insertions, 22 deletions
| @@ -126,8 +126,15 @@ backbuffer from the fb-aware page allocator (it can no longer land on | |||
| 126 | the framebuffer — the heap free list spans the fb hole, so a 1.9 MB | 126 | the framebuffer — the heap free list spans the fb hole, so a 1.9 MB |
| 127 | shadow could alias the display and turn every scroll into an MMIO→MMIO | 127 | shadow could alias the display and turn every scroll into an MMIO→MMIO |
| 128 | memcpy) and converts the scroll's shadow memmove to per-word stores as | 128 | memcpy) and converts the scroll's shadow memmove to per-word stores as |
| 129 | a belt-and-suspenders measure. A future major GUI overhaul (graphics | 129 | a belt-and-suspenders measure; 0.5.5 makes every boot stage visible on |
| 130 | windows beyond the text surface) would be the `a` bump (1.0.0). | 130 | real hardware (no serial console): `gop::boot_marker` prints the boot |
| 131 | progress straight to the framebuffer (the last marker before a hang | ||
| 132 | identifies the death site) and pre-shell panics now draw a white-on-red | ||
| 133 | `*** PANIC ***` band instead of a silent blank screen, and the shadow | ||
| 134 | fallback allocation is panic-proof (try_reserve; a heap OOM degrades | ||
| 135 | to no-shadow mode instead of halting). A future major GUI overhaul | ||
| 136 | (graphics windows beyond the text surface) would be the `a` bump | ||
| 137 | (1.0.0). | ||
| 131 | 138 | ||
| 132 | ## Project layout | 139 | ## Project layout |
| 133 | 140 | ||
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.5.4" | 7 | version = "0.5.5" |
| 8 | dependencies = [ | 8 | dependencies = [ |
| 9 | "rsnix", | 9 | "rsnix", |
| 10 | "shell", | 10 | "shell", |
| @@ -19,11 +19,11 @@ dependencies = [ | |||
| 19 | 19 | ||
| 20 | [[package]] | 20 | [[package]] |
| 21 | name = "rsnix" | 21 | name = "rsnix" |
| 22 | version = "0.5.4" | 22 | version = "0.5.5" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.5.4" | 26 | version = "0.5.5" |
| 27 | dependencies = [ | 27 | dependencies = [ |
| 28 | "rsdosedit", | 28 | "rsdosedit", |
| 29 | "rsnix", | 29 | "rsnix", |
| @@ -3,7 +3,7 @@ resolver = "2" | |||
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.5.4" | 6 | version = "0.5.5" |
| 7 | edition = "2024" | 7 | edition = "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 3153930..b6393ee 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -45,6 +45,31 @@ fn panic(info: &PanicInfo) -> ! { | |||
| 45 | loc.line() | 45 | loc.line() |
| 46 | ); | 46 | ); |
| 47 | } | 47 | } |
| 48 | // 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 | ||
| 50 | // blank screen. | ||
| 51 | let mut loc_buf = [0u8; 96]; | ||
| 52 | let mut msg_buf = [0u8; 128]; | ||
| 53 | let loc_n = { | ||
| 54 | let mut w = rsnix::strbuf::FmtBuf::new(&mut loc_buf); | ||
| 55 | if let Some(loc) = info.location() { | ||
| 56 | let _ = write!(&mut w, "at {}:{}", loc.file(), loc.line()); | ||
| 57 | } else { | ||
| 58 | let _ = write!(&mut w, "(no location)"); | ||
| 59 | } | ||
| 60 | w.len() | ||
| 61 | }; | ||
| 62 | let msg_n = { | ||
| 63 | let mut w = rsnix::strbuf::FmtBuf::new(&mut msg_buf); | ||
| 64 | let _ = write!(&mut w, "{}", info.message()); | ||
| 65 | w.len() | ||
| 66 | }; | ||
| 67 | let lines = [ | ||
| 68 | "*** PANIC ***", | ||
| 69 | core::str::from_utf8(&msg_buf[..msg_n]).unwrap_or("?"), | ||
| 70 | core::str::from_utf8(&loc_buf[..loc_n]).unwrap_or("?"), | ||
| 71 | ]; | ||
| 72 | rsnix::gop::dump_lines(&lines); | ||
| 48 | loop { | 73 | loop { |
| 49 | unsafe { | 74 | unsafe { |
| 50 | core::arch::asm!("cli; hlt"); | 75 | core::arch::asm!("cli; hlt"); |
| @@ -121,9 +146,11 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 121 | } else { | 146 | } else { |
| 122 | rsnix::serial_println!("gop: not found (0x{:x})", gop_result); | 147 | rsnix::serial_println!("gop: not found (0x{:x})", gop_result); |
| 123 | } | 148 | } |
| 149 | rsnix::gop::boot_marker("rsnix v0.5.5 booting.."); | ||
| 124 | rsnix::vt::init(); | 150 | rsnix::vt::init(); |
| 125 | let (rows, cols) = rsnix::vt::size(); | 151 | let (rows, cols) = rsnix::vt::size(); |
| 126 | rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); | 152 | rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); |
| 153 | rsnix::gop::boot_marker("rsnix: gop+vt ok"); | ||
| 127 | 154 | ||
| 128 | // ExitBootServices — we now own the machine | 155 | // ExitBootServices — we now own the machine |
| 129 | if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { | 156 | 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 | |||
| 158 | } | 185 | } |
| 159 | rsnix::interrupts::init(); | 186 | rsnix::interrupts::init(); |
| 160 | rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); | 187 | rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); |
| 188 | rsnix::gop::boot_marker("rsnix: serial+ps2+gdt+intr ok"); | ||
| 161 | 189 | ||
| 162 | dump_memory_summary(); | 190 | dump_memory_summary(); |
| 163 | 191 | ||
| @@ -173,6 +201,7 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 173 | rsnix::serial_println!("pages: init FAILED"); | 201 | rsnix::serial_println!("pages: init FAILED"); |
| 174 | } | 202 | } |
| 175 | rsnix::gop::init_shadow(); | 203 | rsnix::gop::init_shadow(); |
| 204 | rsnix::gop::boot_marker("rsnix: shadow ok"); | ||
| 176 | if rsnix::memory::page_self_test() { | 205 | if rsnix::memory::page_self_test() { |
| 177 | rsnix::serial_println!("pages: self-test OK (alloc/free/runs)"); | 206 | rsnix::serial_println!("pages: self-test OK (alloc/free/runs)"); |
| 178 | } else { | 207 | } else { |
| @@ -228,6 +257,7 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 228 | } | 257 | } |
| 229 | 258 | ||
| 230 | rsnix::serial_println!("boot: entering shell"); | 259 | rsnix::serial_println!("boot: entering shell"); |
| 260 | rsnix::gop::boot_marker("rsnix: boot done — shell"); | ||
| 231 | if rsnix::ps2::mouse_present() { | 261 | if rsnix::ps2::mouse_present() { |
| 232 | rsnix::cursor::spawn(); | 262 | rsnix::cursor::spawn(); |
| 233 | } else { | 263 | } 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() { | |||
| 265 | let mut origin = "none"; | 265 | let mut origin = "none"; |
| 266 | 266 | ||
| 267 | // Primary: fb-aware page allocator (fb pages already marked | 267 | // Primary: fb-aware page allocator (fb pages already marked |
| 268 | // used by init_page_allocator). | 268 | // used by init_page_allocator). Defensively refuse the range if |
| 269 | // it ever overlaps the fb region anyway. | ||
| 269 | if let Some(p) = crate::memory::alloc_pages(pages) { | 270 | 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 | 271 | let overlaps = FB_REGION_SIZE > 0 |
| 281 | && p < FB_REGION_BASE + FB_REGION_SIZE | 272 | && p < FB_REGION_BASE + FB_REGION_SIZE |
| 282 | && p + bytes > FB_REGION_BASE; | 273 | && p + bytes > FB_REGION_BASE; |
| 283 | if !overlaps { | 274 | if !overlaps { |
| 284 | SHADOW = v.as_ptr() as *mut u32; | 275 | SHADOW = p as *mut u32; |
| 285 | SHADOW_WORDS = words; | 276 | SHADOW_WORDS = words; |
| 286 | origin = "heap"; | 277 | origin = "page-alloc"; |
| 287 | core::mem::forget(v); // shadow owns the memory now | 278 | for i in 0..words { |
| 288 | } else { | 279 | *SHADOW.add(i) = fb.bg; |
| 289 | drop(v); | 280 | } |
| 281 | } | ||
| 282 | } | ||
| 283 | if origin == "none" { | ||
| 284 | // Fallback: heap — must never panic here: an OOM would halt | ||
| 285 | // the kernel before the shell banner, a blank screen on real | ||
| 286 | // hardware (the old `vec![0; words]` could abort). On any | ||
| 287 | // failure we degrade to no-shadow mode instead. | ||
| 288 | #[allow(clippy::slow_vector_initialization)] // deliberate: try_reserve must not panic | ||
| 289 | let mut v: alloc::vec::Vec<u32> = alloc::vec::Vec::new(); | ||
| 290 | if v.try_reserve_exact(words).is_ok() { | ||
| 291 | v.resize(words, 0); // can't fail after the reserve | ||
| 292 | let p = v.as_ptr() as usize; | ||
| 293 | let overlaps = FB_REGION_SIZE > 0 | ||
| 294 | && p < FB_REGION_BASE + FB_REGION_SIZE | ||
| 295 | && p + bytes > FB_REGION_BASE; | ||
| 296 | if !overlaps { | ||
| 297 | SHADOW = v.as_ptr() as *mut u32; | ||
| 298 | SHADOW_WORDS = words; | ||
| 299 | origin = "heap"; | ||
| 300 | core::mem::forget(v); // shadow owns the memory now | ||
| 301 | } | ||
| 290 | } | 302 | } |
| 291 | } | 303 | } |
| 292 | let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE); | 304 | let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE); |
| @@ -319,6 +331,34 @@ pub fn init_shadow() { | |||
| 319 | } | 331 | } |
| 320 | } | 332 | } |
| 321 | 333 | ||
| 334 | /// Draw a one-line boot-progress marker to the display (row 0, | ||
| 335 | /// white-on-black). Real hardware has no serial console, so every | ||
| 336 | /// boot stage is written straight to the framebuffer — the last visible | ||
| 337 | /// marker before a hang identifies where the kernel died. Uses the | ||
| 338 | /// direct-display glyph path (the shadow may not exist yet in early | ||
| 339 | /// boot); the shell banner overwrites row 0 at first paint. | ||
| 340 | pub fn boot_marker(msg: &str) { | ||
| 341 | let Some(fb) = fb().as_ref() else { | ||
| 342 | return; | ||
| 343 | }; | ||
| 344 | let cols = fb.width / 8; | ||
| 345 | let white = (0xFFu8, 0xFFu8, 0xFFu8); | ||
| 346 | let black = (0x00u8, 0x00u8, 0x00u8); | ||
| 347 | let mut c = 0usize; | ||
| 348 | for &b in msg.as_bytes().iter().take(cols) { | ||
| 349 | if b < 0x80 { | ||
| 350 | blit_glyph(c, 0, b as u16, white, black); | ||
| 351 | c += 1; | ||
| 352 | } | ||
| 353 | } | ||
| 354 | // Blank the rest of the row so a shorter message can't leave stale | ||
| 355 | // glyphs behind. | ||
| 356 | while c < cols { | ||
| 357 | blit_glyph(c, 0, b' ' as u16, white, black); | ||
| 358 | c += 1; | ||
| 359 | } | ||
| 360 | } | ||
| 361 | |||
| 322 | // --------------------------------------------------------------------------- | 362 | // --------------------------------------------------------------------------- |
| 323 | // Public text API — the text surface now lives in `crate::vt`; these wrappers | 363 | // Public text API — the text surface now lives in `crate::vt`; these wrappers |
| 324 | // keep callers (shell, macros) unchanged. | 364 | // keep callers (shell, macros) unchanged. |
