summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-03 01:34:31 +0200
committersalmonik <salmonik@tuta.io>2026-08-03 01:34:31 +0200
commitd5f41d6e057300c82b83653b4afadeffb45a9f19 (patch)
tree4386a91f9bf89e87b8a1b708ef2b1836412fb2f4
parentv0.5.7: guard report else-branch initializes all mem lines (fix broken build) (diff)
downloadrs-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.md204
-rw-r--r--rsnix/src/gop.rs44
-rw-r--r--rsnix/src/vt.rs132
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
3Handoff state: **HEAD = `ab633ce`**; the mystery below is SOLVED and the
4verify 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
311. **(A) scroll-crash family** — heap allocator returns an in-heap block whose
32 header is zeroed (`ptr=0x404443e0`). Dropping it panics in the guard.
332. **(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
131both grids with `vec![BLANK; rows*cols]`. The old content check expected
132all-zero cells (wrong), and the probe loop's early `break` made probes 1–3
133look like zeros. Nothing writes a pre-banner space; the shell banner (gray
134VGA-7 text, rows 0–1) and the `>` prompt (row 2) are the only vt writes
135before the markers.
136
137Also 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
151only), flush cursor-inverse pass (0x07000000 ≠ 0x70020), boot markers (direct
152FB), memory self-test, shell banner (runs after verify), backspace (BLANK).
153
154## OptiPlex flash — what to look for (the linchpin)
155
156New image puts on screen (markers are **bottom-anchored** now — they survive
157the banner rows 0–1, the prompt row 2, AND a stale crash.log replay at rows
1582+; use `gop::bottom_row(off)` to place any future ones):
159
1601. **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).
1642. **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`).
1703. If it crashes: the full guard band (`hdr:` + all four `mem:` lines, 16 stack
171 words) plus the bottom markers if still visible.
172
173Serial-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
175the `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
1971. **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.
2012. 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;
204no 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.
315pub fn boot_marker(msg: &str) { 318pub 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).
328pub 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).
361pub 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.
368pub 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}