summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-02 01:52:10 +0200
committersalmonik <salmonik@tuta.io>2026-08-02 01:52:10 +0200
commit045c9ddf71b62dff538cd3ae1963166ab5c145a0 (patch)
treefd99757329f7b770f067aa5f0204c87068fea681
parentgop: rustfmt collapse in init_shadow warning band (diff)
downloadrs-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.
Diffstat (limited to '')
-rw-r--r--CONTEXT.md11
-rw-r--r--Cargo.lock6
-rw-r--r--Cargo.toml2
-rw-r--r--rsdos/src/main.rs30
-rw-r--r--rsnix/src/gop.rs72
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
126the framebuffer — the heap free list spans the fb hole, so a 1.9 MB 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 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 128memcpy) and converts the scroll's shadow memmove to per-word stores as
129a belt-and-suspenders measure. A future major GUI overhaul (graphics 129a belt-and-suspenders measure; 0.5.5 makes every boot stage visible on
130windows beyond the text surface) would be the `a` bump (1.0.0). 130real hardware (no serial console): `gop::boot_marker` prints the boot
131progress straight to the framebuffer (the last marker before a hang
132identifies 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
134fallback allocation is panic-proof (try_reserve; a heap OOM degrades
135to 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
diff --git a/Cargo.lock b/Cargo.lock
index 2796caa..431db32 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.4" 7version = "0.5.5"
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.4" 22version = "0.5.5"
23 23
24[[package]] 24[[package]]
25name = "shell" 25name = "shell"
26version = "0.5.4" 26version = "0.5.5"
27dependencies = [ 27dependencies = [
28 "rsdosedit", 28 "rsdosedit",
29 "rsnix", 29 "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"
3members = ["rsnix", "shell", "rsdos", "rsdosedit"] 3members = ["rsnix", "shell", "rsdos", "rsdosedit"]
4 4
5[workspace.package] 5[workspace.package]
6version = "0.5.4" 6version = "0.5.5"
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 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.
340pub 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.