diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-03 01:34:31 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-03 01:34:31 +0200 |
| commit | d5f41d6e057300c82b83653b4afadeffb45a9f19 (patch) | |
| tree | 4386a91f9bf89e87b8a1b708ef2b1836412fb2f4 | |
| parent | v0.5.7: guard report else-branch initializes all mem lines (fix broken build) (diff) | |
| download | rs-dos-d5f41d6e057300c82b83653b4afadeffb45a9f19.tar.gz rs-dos-d5f41d6e057300c82b83653b4afadeffb45a9f19.zip | |
v0.5.7: fix vt verify content check — expect BLANK (0x70020), not zero
The pre-verify 0x70020 'mystery' was a debug artifact, not corruption:
BLANK = {ch:32, fg:7, bg:0} = raw 0x70020, and vec![BLANK; n] fills both
grids with it at init. The old check compared cells against 0 and its
probe loop break-ed on the first non-zero cell, so probes 1-3 stayed at
their zero-initialized slots — c=[0x70020,0,0,0] was bogus on every
healthy boot. A full-grid dump proves QEMU grids are 100% BLANK at
shell entry (nz=3700 blank=3700).
Fix: probes compare against blank_raw (0x70020), record all four values
(no early break), probe both cells and rendered, and add a full-grid
nz/blank summary line. The OptiPlex firmware 0x00AAAAAA scrub remains
cleanly distinguishable (scrub != blank).
Markers: the shell banner occupies grid rows 0-1 and the prompt row 2
(proven by screendump), and a stale crash.log replays at rows 2+, so the
shadow and vt markers moved to the bottom of the screen via the new
gop::text_rows()/bottom_row(off) helpers instead of rows 1-2.
QEMU verified: verify: ok every boot; test-msg + test-fault PASS; debug
and release builds 0 warnings.
| -rw-r--r-- | DEBUG_NOTES.md | 204 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 44 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 132 |
3 files changed, 344 insertions, 36 deletions
diff --git a/DEBUG_NOTES.md b/DEBUG_NOTES.md new file mode 100644 index 0000000..d44c8bf --- /dev/null +++ b/DEBUG_NOTES.md | |||
| @@ -0,0 +1,204 @@ | |||
| 1 | # rsdos Debug Notes — heap/cells corruption family (v0.5.7) | ||
| 2 | |||
| 3 | Handoff state: **HEAD = `ab633ce`**; the mystery below is SOLVED and the | ||
| 4 | verify rework is committed on top (see git log). | ||
| 5 | |||
| 6 | ## TL;DR | ||
| 7 | |||
| 8 | - Real-hardware (OptiPlex, no serial) boots crash in the shell with heap | ||
| 9 | corruption. Two **distinct** bug families: | ||
| 10 | - **(A) scroll-crash family**: in-heap `ptr=0x404443e0` with zeroed header. | ||
| 11 | - **(B) 0xAA family**: `0xaaaaaa00aaaaaa` = two `0x00AAAAAA` u32s = UEFI | ||
| 12 | firmware memory-scrub fill. A dropped object's memory was never | ||
| 13 | allocated/freed by us — it's firmware-fresh scrub. | ||
| 14 | - Everything converges on **the `vt` static `Vec<Cell>`s' VecData (ptr/cap/len) | ||
| 15 | or their heap blocks being corrupted/stale** — the cells VecData ptr at shell | ||
| 16 | entry was `heap+0x1D8630` (= cells region) in the last crash, and the dropped | ||
| 17 | 126-byte String (`cap 0x7e`) is the vt verify klog line itself. | ||
| 18 | - **SOLVED — the pre-verify `0x70020` "mystery" was a debug artifact, not | ||
| 19 | corruption.** `BLANK = {ch:32, fg:7, bg:0}` = raw `0x70020`, and | ||
| 20 | `vec![BLANK; rows*cols]` fills the whole grid with it at init. The old | ||
| 21 | content check compared cells against **0** (wrong expectation — BLANK is not | ||
| 22 | zero), and its probe loop `break`-ed on the first non-zero cell, so probes | ||
| 23 | 1–3 were never written and stayed at their zero-initialized slots. The | ||
| 24 | serial line `c=[0x70020,0x0,0x0,0x0]` was therefore bogus on **every healthy | ||
| 25 | boot**. A full-grid dump on QEMU proves it: `nz=3700 blank=3700` — both | ||
| 26 | grids are 100% BLANK at shell entry. The OptiPlex's `0x00AAAAAA` firmware | ||
| 27 | scrub is still cleanly distinguishable (scrub ≠ 0x70020). | ||
| 28 | |||
| 29 | ## The two confirmed bugs | ||
| 30 | |||
| 31 | 1. **(A) scroll-crash family** — heap allocator returns an in-heap block whose | ||
| 32 | header is zeroed (`ptr=0x404443e0`). Dropping it panics in the guard. | ||
| 33 | 2. **(B) 0xAA family** — freed garbage pointer `0xaaaaaa00aaaaaa`; the memory at | ||
| 34 | that address is firmware scrub fill. Something handed the allocator a pointer | ||
| 35 | that was never a live allocation (or the VecData fields themselves got | ||
| 36 | clobbered with scrub values — the cap was `0xAA` garbage in the boot crash). | ||
| 37 | |||
| 38 | ## What we've fixed / proven already | ||
| 39 | |||
| 40 | - **IST=1 stack collision → ROOT CAUSE of ACTIVE_VOL corruption**: vec 14 ran on | ||
| 41 | `PF_STACK`, a 4 KiB `.bss` static overlapping FAT `ACTIVE_VOL`. Fixed in | ||
| 42 | `gdt.rs`: heap-allocated 32 KiB IST stacks. (Done, earlier.) | ||
| 43 | - **AHCI/DMA EXONERATED**: user removed all disks; crashes persisted. | ||
| 44 | - **All QEMU repro attempts were clean** (mem/klog/tail-crash) until the content | ||
| 45 | probe upgrade — which tripped the *content check only* on a healthy boot for | ||
| 46 | the reason above (expectation bug + break bug). Fixed: the check now expects | ||
| 47 | `blank_raw` (0x70020), records all four probe values, and adds a full-grid | ||
| 48 | nz/blank summary line. QEMU now reports `verify: ok` on every boot. | ||
| 49 | - **Clean heap geometry (OptiPlex)**: heap = `0x40400000..0xc7e34000` (~2.2 GB), | ||
| 50 | boot stack ~1 MiB above heap end. | ||
| 51 | - **Shadow geometry (QEMU)**: shadow = first alloc at `heap+0x20` (`0x1980020`), | ||
| 52 | 480000 words → span `[heap+0x20, heap+0x1D4C20)`; cells data at | ||
| 53 | `heap+0x1D4C40`; VecDatas at ~`heap+0x1D8630`. | ||
| 54 | - **Shadow-rejection hypothesis (unproven)**: on the OptiPlex the shadow alloc | ||
| 55 | may have been rejected (or the cells alloc landed inside the would-be shadow | ||
| 56 | span), leaving that block free + scrub-filled. `0x40444970 = heap+0x44970` is | ||
| 57 | *inside* the would-be shadow span. The last crash's `r9=0x405d8630 = | ||
| 58 | heap+0x1D8630` is the cells VecData region. | ||
| 59 | - **Original #GP resolved**: crashed image was RELEASE; drop of `Parsed` struct; | ||
| 60 | cause = real memory corruption (laytest excluded compiler bug). | ||
| 61 | - **Stale PDB trap**: fresh PDB re-resolved shell-entry #GP → | ||
| 62 | `rsnix::vt::put_byte+0x16a` (`mov %ax,(%rdx,%rcx,4)`) — u16 char at 4-byte | ||
| 63 | stride into cells buffer via garbage ptr from `r9=0x405d8630`. bt chain: | ||
| 64 | `shell::run → GopWriter::write_str → fmt::Formatter::pad_integral → | ||
| 65 | klog::LineBuf::write_str → core::fmt::write → Display(u32)`. | ||
| 66 | - **Crash decoded**: the dropped object in the last boot crash was a 126-byte | ||
| 67 | String (`cap 0x7e`) = the vt verify klog line (19-char prefix + 106-char text + | ||
| 68 | `\n`); `ptr=0x40444970` is STALE (inside the would-be shadow span, back-pointer | ||
| 69 | never written). | ||
| 70 | |||
| 71 | ## Allocator layout (verified, `memory.rs`) | ||
| 72 | |||
| 73 | - `HEADER_SIZE=16`, `MINI_SIZE=8`, `MIN_BLOCK=32`, `MIN_ALIGN=16` | ||
| 74 | - payload = `align_up(block+24, align)` | ||
| 75 | - split: rewrites rem header + `free_list_insert` + back-pointer at `payload-8` | ||
| 76 | - `back_pointer_ok(ptr)` validates `[ptr-8]`; guard v0.5.7 (`0187b80`) reports + | ||
| 77 | halts on failure; `corrupt` QEMU hook verified. | ||
| 78 | |||
| 79 | ## vt grid facts | ||
| 80 | |||
| 81 | - `put_cell(r,c,ch)` sets `ch` + fg/bg from statics `SGR_FG=7`/`SGR_BG=0`. | ||
| 82 | - Cell raw encoding: `ch | fg<<16 | bg<<24`; BLANK = 0; cursor-inverse blank = | ||
| 83 | `0x07000000` (bg and fg swapped). **`0x70020` is NOT a cursor-inverse cell** — | ||
| 84 | it's a literal space with fg=7 (ch=0x20, fg=7, bg=0). | ||
| 85 | - `flush()` diffs cells vs rendered; cursor pass writes inverse colors into both | ||
| 86 | FB and `rendered`. | ||
| 87 | - `term_println(s)` = `gop_print!` + blank line + explicit `serial::write_line`. | ||
| 88 | Display mirror to serial is NOT automatic for `gop_println!`. | ||
| 89 | - Boot order after vt init: shadow → boot markers → memory self-test → page | ||
| 90 | allocator + self-tests → paging adopt + self-test → sched init → | ||
| 91 | rtc/pci/ahci/fat/fs → **cursor spawn** → **`debug_verify()`** → `shell::run()` | ||
| 92 | (first banner `rs-dos v0.5.7 …`, then `=====`, then crash-log read). | ||
| 93 | |||
| 94 | ## Current committed work (post-`ab633ce`) | ||
| 95 | |||
| 96 | - `gop::boot_marker_at(msg, row)` + `gop::text_rows()` + `gop::bottom_row(off)` | ||
| 97 | — shadow marker now on the LAST text row, vt verify marker one above it | ||
| 98 | (both survive the banner, prompt, and crash.log replay). | ||
| 99 | - `debug_verify()` fixed: compares probes against `blank_raw` (0x70020), not | ||
| 100 | zero; records all four probe values (no early break); probes BOTH cells and | ||
| 101 | rendered; adds a full-grid `grids c nz=… blank=… r nz=… blank=…` summary | ||
| 102 | (serial). Marker format: | ||
| 103 | `vt: c {ptr:#x} cap={cap:#x} ok|GARBAGE | r {ptr:#x} ok|GARBAGE` at | ||
| 104 | `bottom_row(1)`. | ||
| 105 | - QEMU verified: `verify: ok` every boot; screendump shows the banner (gray), | ||
| 106 | prompt, and both bottom markers. test-msg + test-fault PASS; debug and | ||
| 107 | release builds 0 warnings. | ||
| 108 | |||
| 109 | ## QEMU reproduction of the content-check failure (RESOLVED) | ||
| 110 | |||
| 111 | - Serial showed `probes c=[0x70020,0x0,0x0,0x0] r=[0x70020,0x0,0x0,0x0]` — this | ||
| 112 | was **misread** as "a single stray cell". The full-grid dump (3700 reads) | ||
| 113 | showed `nz=3700 blank=3700` in both grids: **every cell is BLANK (0x70020), | ||
| 114 | the grids are healthy**. The probe array's 1–3 slots were zero-initialized | ||
| 115 | leftovers from the early-`break` bug. | ||
| 116 | - Memory dump proves grids are fully healthy on QEMU: cells block base | ||
| 117 | `0x1b54c20` `{size=0x39f0, free=0}`, back-ptr written; rendered block base | ||
| 118 | `0x1b58650`; both grids contain the shell banner post-draw. **No heap | ||
| 119 | corruption on QEMU.** | ||
| 120 | - QEMU verify geometry: cells ptr `0x1b54c40` cap `0xe74` len `0xe74`; rendered | ||
| 121 | ptr `0x1b58670`. | ||
| 122 | - ⇒ `CONTENT-GARBAGE` was never evidence of OptiPlex corruption — the check was | ||
| 123 | comparing against the wrong expectation. Now: probes compare against | ||
| 124 | `blank_raw` (0x70020); every probe value is logged; a `grids c nz=… blank=…` | ||
| 125 | summary is the definitive health signal. | ||
| 126 | |||
| 127 | ## The pre-verify `0x70020` at grid(0,0) — MYSTERY SOLVED | ||
| 128 | |||
| 129 | **There was no writer.** `0x70020` is the grid's own init fill: | ||
| 130 | `BLANK = Cell{ch:32, fg:7, bg:0}` encodes as `0x70020`, and `vt::init` builds | ||
| 131 | both grids with `vec![BLANK; rows*cols]`. The old content check expected | ||
| 132 | all-zero cells (wrong), and the probe loop's early `break` made probes 1–3 | ||
| 133 | look like zeros. Nothing writes a pre-banner space; the shell banner (gray | ||
| 134 | VGA-7 text, rows 0–1) and the `>` prompt (row 2) are the only vt writes | ||
| 135 | before the markers. | ||
| 136 | |||
| 137 | Also learned while verifying with a screendump: | ||
| 138 | |||
| 139 | - The shell banner occupies grid rows **0 and 1** (motd + `=====` line), and | ||
| 140 | the prompt sits at row 2 — so a marker at FB row 1 (the old shadow marker) | ||
| 141 | is **clobbered** by the banner, and row 2's first glyphs get eaten by the | ||
| 142 | prompt + cursor. Markers whose values matter now live at the **bottom** | ||
| 143 | (`gop::bottom_row(0)` = last row, `bottom_row(1)` = second-to-last), which | ||
| 144 | also survive a stale crash.log replay (~20 lines at grid rows 2+). | ||
| 145 | - Banner text renders VGA gray `0xAAAAAA` (fg 7), boot markers white `0xFFFFFF` | ||
| 146 | — a white-only pixel matcher misses the banner. QEMU screendump decode of | ||
| 147 | the bottom rows: `vt: c 0x1b54c40 cap=0xe74 ok | r 0x1b58670 ok` and | ||
| 148 | `gop: shadow 0x1980020 1.83 MiB`. | ||
| 149 | |||
| 150 | **Excluded candidates (all still true, now moot):** cursor task (FB/shadow | ||
| 151 | only), flush cursor-inverse pass (0x07000000 ≠ 0x70020), boot markers (direct | ||
| 152 | FB), memory self-test, shell banner (runs after verify), backspace (BLANK). | ||
| 153 | |||
| 154 | ## OptiPlex flash — what to look for (the linchpin) | ||
| 155 | |||
| 156 | New image puts on screen (markers are **bottom-anchored** now — they survive | ||
| 157 | the banner rows 0–1, the prompt row 2, AND a stale crash.log replay at rows | ||
| 158 | 2+; use `gop::bottom_row(off)` to place any future ones): | ||
| 159 | |||
| 160 | 1. **Last row (bottom_row(0))**: `gop: shadow {ptr:#x} {MiB}` vs | ||
| 161 | `gop: NO-SHADOW (fallback)`. | ||
| 162 | - If NO-SHADOW ⇒ shadow-rejection hypothesis gains weight (first block left | ||
| 163 | free + scrub-filled). | ||
| 164 | 2. **Second-to-last row (bottom_row(1))**: `vt: c {ptr:#x} cap={cap:#x} ok|GARBAGE | r {ptr:#x} ok|GARBAGE`. | ||
| 165 | - **The cells ptr value is the linchpin**: if it equals `0x40444970`, the | ||
| 166 | cells VecData itself is stale at shell entry (matches boot-crash decode). | ||
| 167 | - If cap shows `0xAA…` garbage ⇒ VecData fields scrubbed (bug family B). | ||
| 168 | - `ok` = all probed cells read BLANK (0x70020); `GARBAGE` = probe ≠ BLANK | ||
| 169 | (the OptiPlex firmware scrub reads back `0x00AAAAAA`). | ||
| 170 | 3. If it crashes: the full guard band (`hdr:` + all four `mem:` lines, 16 stack | ||
| 171 | words) plus the bottom markers if still visible. | ||
| 172 | |||
| 173 | Serial-side (QEMU only): the verify lines are captured in the klog ring | ||
| 174 | (`serial_println!` → ring), so a healthy OptiPlex boot can also dump them with | ||
| 175 | the `klog` shell command. | ||
| 176 | |||
| 177 | ## Build/flash/test loop (traps) | ||
| 178 | |||
| 179 | - Build 0 warnings, edition 2024, `no_std`, `x86_64-unknown-uefi`, workspace | ||
| 180 | v0.5.7. Zero new deps. | ||
| 181 | - Real-hardware boot chain: rsboot at `EFI/BOOT/BOOTX64.EFI` → kernel at | ||
| 182 | `bzImage`. QEMU dev loop boots kernel as BOOTX64.EFI. | ||
| 183 | - CRASHLOG stick must be FAT32; `mdir` grep gotcha: match `crash` | ||
| 184 | case-insensitively. | ||
| 185 | - **Stale qemu holds flock** → `pkill -9 -f 'qemu[-]system'` in a SEPARATE | ||
| 186 | command; `rm -f esp/NvVars` between boots. | ||
| 187 | - Use repo `scripts/` infra: test-msg/test-fault/test-mouse; monitor socket; HMP | ||
| 188 | `mouse_move`. | ||
| 189 | - Fault-context serial rule: never `serial_println!` in ISR/fault code; use | ||
| 190 | `serial::write_raw`; guard report must not allocate (stack `FmtBuf`). | ||
| 191 | - Marker on row 0 gets overwritten by the shell MOTD — use `boot_marker_at(msg, row>0)`. | ||
| 192 | - Stale PDBs misname symbols — re-resolve with a fresh PDB per build (PDB math | ||
| 193 | is decimal); `r9` = struct base with Vec layout `{ptr@0, cap@8, len@16}`. | ||
| 194 | |||
| 195 | ## Next steps (priority order) | ||
| 196 | |||
| 197 | 1. **Flash to OptiPlex** and relay the two bottom markers: shadow state (last | ||
| 198 | row) + vt marker (`bottom_row(1)` — the cells ptr value is the linchpin: | ||
| 199 | equals `0x40444970`? cap scrubbed `0xAA…`? `ok` vs `GARBAGE`?). If crash: | ||
| 200 | full `hdr:` + all four `mem:` lines. | ||
| 201 | 2. Reattach the window-manager milestone once the crash family is resolved. | ||
| 202 | |||
| 203 | (QEMU control arm is green: content check now passes on every healthy boot; | ||
| 204 | no further QEMU-side investigation is needed.) | ||
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 8e804e5..012a9d0 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -293,13 +293,16 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 293 | .map(|c| core::str::from_utf8(c).unwrap_or("?")) | 293 | .map(|c| core::str::from_utf8(c).unwrap_or("?")) |
| 294 | .collect(); | 294 | .collect(); |
| 295 | dump_lines(&lines); | 295 | dump_lines(&lines); |
| 296 | crate::gop::boot_marker("gop: NO-SHADOW (fallback mode)"); | 296 | crate::gop::boot_marker_at("gop: NO-SHADOW (fallback)", bottom_row(0)); |
| 297 | } else { | 297 | } else { |
| 298 | crate::gop::boot_marker(&alloc::format!( | 298 | crate::gop::boot_marker_at( |
| 299 | "gop: shadow {:#x} {:.2} MiB", | 299 | &alloc::format!( |
| 300 | SHADOW as usize, | 300 | "gop: shadow {:#x} {:.2} MiB", |
| 301 | swords as f64 * 4.0 / 1048576.0 | 301 | SHADOW as usize, |
| 302 | )); | 302 | swords as f64 * 4.0 / 1048576.0 |
| 303 | ), | ||
| 304 | bottom_row(0), | ||
| 305 | ); | ||
| 303 | } | 306 | } |
| 304 | } | 307 | } |
| 305 | } | 308 | } |
| @@ -313,6 +316,16 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 313 | /// direct-display glyph path (the shadow may not exist yet in early | 316 | /// direct-display glyph path (the shadow may not exist yet in early |
| 314 | /// boot); the shell banner overwrites row 0 at first paint. | 317 | /// boot); the shell banner overwrites row 0 at first paint. |
| 315 | pub fn boot_marker(msg: &str) { | 318 | pub fn boot_marker(msg: &str) { |
| 319 | boot_marker_at(msg, 0); | ||
| 320 | } | ||
| 321 | |||
| 322 | /// boot_marker at an explicit text row. The shell's first paint | ||
| 323 | /// occupies grid rows 0 (motd) and 1 (=== banner) and the prompt sits | ||
| 324 | /// at row 2 — markers whose *values* matter after boot (shadow state, | ||
| 325 | /// vt grid pointers) are anchored to the bottom of the screen via | ||
| 326 | /// `bottom_row`, which also survives a stale crash.log replay (the | ||
| 327 | /// fault band is ~20 lines starting at grid row 2). | ||
| 328 | pub fn boot_marker_at(msg: &str, row: usize) { | ||
| 316 | // Record the stage so a later panic band can show how far boot got. | 329 | // Record the stage so a later panic band can show how far boot got. |
| 317 | unsafe { | 330 | unsafe { |
| 318 | let n = msg.len().min(STAGE_MAX); | 331 | let n = msg.len().min(STAGE_MAX); |
| @@ -323,24 +336,39 @@ pub fn boot_marker(msg: &str) { | |||
| 323 | let Some(fb) = fb().as_ref() else { | 336 | let Some(fb) = fb().as_ref() else { |
| 324 | return; | 337 | return; |
| 325 | }; | 338 | }; |
| 339 | if row >= fb.height / 16 { | ||
| 340 | return; | ||
| 341 | } | ||
| 326 | let cols = fb.width / 8; | 342 | let cols = fb.width / 8; |
| 327 | let white = (0xFFu8, 0xFFu8, 0xFFu8); | 343 | let white = (0xFFu8, 0xFFu8, 0xFFu8); |
| 328 | let black = (0x00u8, 0x00u8, 0x00u8); | 344 | let black = (0x00u8, 0x00u8, 0x00u8); |
| 329 | let mut c = 0usize; | 345 | let mut c = 0usize; |
| 330 | for &b in msg.as_bytes().iter().take(cols) { | 346 | for &b in msg.as_bytes().iter().take(cols) { |
| 331 | if b < 0x80 { | 347 | if b < 0x80 { |
| 332 | blit_glyph(c, 0, b as u16, white, black); | 348 | blit_glyph(c, row, b as u16, white, black); |
| 333 | c += 1; | 349 | c += 1; |
| 334 | } | 350 | } |
| 335 | } | 351 | } |
| 336 | // Blank the rest of the row so a shorter message can't leave stale | 352 | // Blank the rest of the row so a shorter message can't leave stale |
| 337 | // glyphs behind. | 353 | // glyphs behind. |
| 338 | while c < cols { | 354 | while c < cols { |
| 339 | blit_glyph(c, 0, b' ' as u16, white, black); | 355 | blit_glyph(c, row, b' ' as u16, white, black); |
| 340 | c += 1; | 356 | c += 1; |
| 341 | } | 357 | } |
| 342 | } | 358 | } |
| 343 | 359 | ||
| 360 | /// Text-row count of the active framebuffer (each row is 16 px tall). | ||
| 361 | pub fn text_rows() -> usize { | ||
| 362 | fb().as_ref().map(|f| f.height / 16).unwrap_or(0) | ||
| 363 | } | ||
| 364 | |||
| 365 | /// Row index of the `off`-th text row from the bottom (0 = last row). | ||
| 366 | /// Bottom-anchored markers survive the shell banner + prompt at the top | ||
| 367 | /// and a stale crash.log replay below it. | ||
| 368 | pub fn bottom_row(off: usize) -> usize { | ||
| 369 | text_rows().saturating_sub(1 + off) | ||
| 370 | } | ||
| 371 | |||
| 344 | // --------------------------------------------------------------------------- | 372 | // --------------------------------------------------------------------------- |
| 345 | // Public text API — the text surface now lives in `crate::vt`; these wrappers | 373 | // Public text API — the text surface now lives in `crate::vt`; these wrappers |
| 346 | // keep callers (shell, macros) unchanged. | 374 | // keep callers (shell, macros) unchanged. |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 2906258..6e1f12f 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -173,48 +173,124 @@ pub fn debug_verify() { | |||
| 173 | }) | 173 | }) |
| 174 | .unwrap_or(false); | 174 | .unwrap_or(false); |
| 175 | // Content check: at shell entry nothing has been printed yet, so | 175 | // Content check: at shell entry nothing has been printed yet, so |
| 176 | // every cell must still be BLANK (all-zero). A stale cells data | 176 | // every cell must still hold the grid's init fill. NOTE: that |
| 177 | // pointer that happens to be in-heap still passes the range | 177 | // fill is BLANK (space + fg 7 + bg 0 = raw 0x70020), NOT zero — |
| 178 | // checks (len/cap look fine) but its "cells" read back as the | 178 | // `vec![BLANK; n]` fills the grid with BLANK, and all boot |
| 179 | // underlying memory's leftover fill — the OptiPlex shows the | 179 | // output to this point is serial-only (boot markers are a |
| 180 | // firmware 0x00AAAAAA scrub there. This catches that. | 180 | // direct-FB blit, never the vt grid). The v0.5.7 check |
| 181 | // compared against 0 and therefore tripped on every healthy | ||
| 182 | // boot (the "0x70020 at grid(0,0)" mystery — the grid's own | ||
| 183 | // init value, with the probe loop's early `break` leaving the | ||
| 184 | // other three probe slots as zero-initialized). A stale cells | ||
| 185 | // data pointer that happens to be in-heap still passes the | ||
| 186 | // range checks (len/cap look fine) but its "cells" read back as | ||
| 187 | // the underlying memory's leftover fill — the OptiPlex shows | ||
| 188 | // the firmware 0x00AAAAAA scrub there. BLANK (0x70020) vs | ||
| 189 | // scrub (0x00AAAAAA) is cleanly distinguishable. | ||
| 190 | let blank_raw = BLANK.ch as u32 | ((BLANK.fg as u32) << 16) | ((BLANK.bg as u32) << 24); | ||
| 181 | let mut content_ok = true; | 191 | let mut content_ok = true; |
| 192 | let mut rcontent_ok = true; | ||
| 193 | let mut c_probes_raw: [u32; 4] = [0; 4]; | ||
| 194 | let mut r_probes_raw: [u32; 4] = [0; 4]; | ||
| 182 | if heap_ok && c.len() == total && total > 0 { | 195 | if heap_ok && c.len() == total && total > 0 { |
| 183 | let mut i = 0usize; | 196 | // Sample the first cell, the last cell, and two interior |
| 184 | // Sample the first cell, the last cell, and a few interior | ||
| 185 | // cells — reading the whole grid here would be ~60 KiB of | 197 | // cells — reading the whole grid here would be ~60 KiB of |
| 186 | // checks at every boot for no real benefit. | 198 | // checks at every boot for no real benefit. All four |
| 199 | // probes are recorded unconditionally (no early break) so | ||
| 200 | // a serial log shows every value. | ||
| 187 | let probes = [0usize, total / 3, (2 * total) / 3, total - 1]; | 201 | let probes = [0usize, total / 3, (2 * total) / 3, total - 1]; |
| 188 | for &p in probes.iter() { | 202 | for (pi, &p) in probes.iter().enumerate() { |
| 189 | if i >= probes.len() { | ||
| 190 | break; | ||
| 191 | } | ||
| 192 | i += 1; | ||
| 193 | let cell = *c.as_ptr().add(p); | 203 | let cell = *c.as_ptr().add(p); |
| 194 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | 204 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); |
| 195 | if raw != 0 { | 205 | c_probes_raw[pi] = raw; |
| 206 | if raw != blank_raw { | ||
| 196 | content_ok = false; | 207 | content_ok = false; |
| 197 | break; | 208 | } |
| 209 | } | ||
| 210 | // Same probe on the rendered grid: it is a *separate* | ||
| 211 | // allocation, so its content discriminates "only the cells | ||
| 212 | // VecData's ptr field is stale" (rendered ok) from "both | ||
| 213 | // grids' memory was clobbered / both VecData ptrs are | ||
| 214 | // stale" (rendered garbage too). | ||
| 215 | for (pi, &p) in probes.iter().enumerate() { | ||
| 216 | let cell = *r.as_ptr().add(p); | ||
| 217 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | ||
| 218 | r_probes_raw[pi] = raw; | ||
| 219 | if raw != blank_raw { | ||
| 220 | rcontent_ok = false; | ||
| 221 | } | ||
| 222 | } | ||
| 223 | } | ||
| 224 | // Full-grid summary (cheap: a few thousand reads, serial-only). | ||
| 225 | // Definitive health signal on QEMU: healthy = every cell is | ||
| 226 | // BLANK (nz == blanks == total); the probes above are just the | ||
| 227 | // tripwire, this catches a partial clobber that the four | ||
| 228 | // samples happen to miss. | ||
| 229 | let (mut c_nz, mut c_blank) = (0usize, 0usize); | ||
| 230 | let (mut r_nz, mut r_blank) = (0usize, 0usize); | ||
| 231 | if heap_ok && c.len() == total && r.len() == total && total > 0 { | ||
| 232 | for i in 0..total { | ||
| 233 | let cell = *c.as_ptr().add(i); | ||
| 234 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | ||
| 235 | if raw != 0 { | ||
| 236 | c_nz += 1; | ||
| 237 | } | ||
| 238 | if raw == blank_raw { | ||
| 239 | c_blank += 1; | ||
| 240 | } | ||
| 241 | let cell = *r.as_ptr().add(i); | ||
| 242 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | ||
| 243 | if raw != 0 { | ||
| 244 | r_nz += 1; | ||
| 245 | } | ||
| 246 | if raw == blank_raw { | ||
| 247 | r_blank += 1; | ||
| 198 | } | 248 | } |
| 199 | } | 249 | } |
| 200 | } | 250 | } |
| 201 | if c.len() != total || r.len() != total || !heap_ok || !content_ok { | 251 | msg(&alloc::format!( |
| 252 | "verify: grids c nz={} blank={} r nz={} blank={} (of {})", | ||
| 253 | c_nz, c_blank, r_nz, r_blank, total | ||
| 254 | )); | ||
| 255 | if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok { | ||
| 202 | msg("verify: FAILED — cells/rendered not sane at shell entry"); | 256 | msg("verify: FAILED — cells/rendered not sane at shell entry"); |
| 203 | crate::gop::boot_marker(&alloc::format!( | 257 | msg(&alloc::format!( |
| 204 | "vt: cells {:#x} {}/{} {}", | 258 | "verify: probes c=[{:#x},{:#x},{:#x},{:#x}] r=[{:#x},{:#x},{:#x},{:#x}]", |
| 205 | c.as_ptr() as usize, | 259 | c_probes_raw[0], |
| 206 | c.len(), | 260 | c_probes_raw[1], |
| 207 | total, | 261 | c_probes_raw[2], |
| 208 | if content_ok { "range" } else { "CONTENT-GARBAGE" } | 262 | c_probes_raw[3], |
| 263 | r_probes_raw[0], | ||
| 264 | r_probes_raw[1], | ||
| 265 | r_probes_raw[2], | ||
| 266 | r_probes_raw[3] | ||
| 209 | )); | 267 | )); |
| 268 | // Bottom-anchored: row 0-1 get overwritten by the shell | ||
| 269 | // banner, row 2 by the prompt (and a stale crash.log | ||
| 270 | // replays at rows 2+), so this marker must live at the very | ||
| 271 | // bottom to stay readable until the user can copy it. | ||
| 272 | crate::gop::boot_marker_at( | ||
| 273 | &alloc::format!( | ||
| 274 | "vt: c {:#x} cap={:#x} {} | r {:#x} {}", | ||
| 275 | c.as_ptr() as usize, | ||
| 276 | c.capacity(), | ||
| 277 | if content_ok { "ok" } else { "GARBAGE" }, | ||
| 278 | r.as_ptr() as usize, | ||
| 279 | if rcontent_ok { "ok" } else { "GARBAGE" } | ||
| 280 | ), | ||
| 281 | crate::gop::bottom_row(1), | ||
| 282 | ); | ||
| 210 | } else { | 283 | } else { |
| 211 | msg("verify: ok"); | 284 | msg("verify: ok"); |
| 212 | crate::gop::boot_marker(&alloc::format!( | 285 | crate::gop::boot_marker_at( |
| 213 | "vt: cells {:#x} {}/{} ok", | 286 | &alloc::format!( |
| 214 | c.as_ptr() as usize, | 287 | "vt: c {:#x} cap={:#x} ok | r {:#x} ok", |
| 215 | c.len(), | 288 | c.as_ptr() as usize, |
| 216 | total | 289 | c.capacity(), |
| 217 | )); | 290 | r.as_ptr() as usize |
| 291 | ), | ||
| 292 | crate::gop::bottom_row(1), | ||
| 293 | ); | ||
| 218 | } | 294 | } |
| 219 | } | 295 | } |
| 220 | } | 296 | } |
