diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-03 02:23:35 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-03 02:23:35 +0200 |
| commit | 9fde94d068f87b34e0bc6bfa48388d89bb54f1e4 (patch) | |
| tree | a5a4ce95363e7a316a23371489f2b0b85c1a8735 /rsnix/src/gop.rs | |
| parent | v0.5.7: fix vt verify content check — expect BLANK (0x70020), not zero (diff) | |
| download | rs-dos-9fde94d068f87b34e0bc6bfa48388d89bb54f1e4.tar.gz rs-dos-9fde94d068f87b34e0bc6bfa48388d89bb54f1e4.zip | |
v0.5.8: heap-free QR encoder + crash-dump QR on framebuffer
Real hardware has no serial console, so a kernel crash only leaves a
frozen screen. Every dump_lines() report now also renders the crash
text as a scannable QR below the white-on-red band (photograph the
screen instead of relaying text).
- rsnix/src/qr.rs: from-scratch no_std/no_alloc byte-mode QR encoder
(EC-L, v1..v40, .bss statics only — safe on the fault path):
RS over GF(256), codeword interleave, zigzag placement, 8 masks with
N1-N4 penalty auto-selection, format/version info.
- gop.rs: dump_qr() joins dump lines into a static buffer, truncates
with '...' at the v40-L capacity, encodes, and renders centered
black-on-white with a 4-module quiet zone.
- tools/qrcheck: host harness (embedds qr.rs) + qrcheck.py cross-check
against segno's placement primitives. Verified bit-exact: 12
versions x 8 masks forced, plus auto-mask choice matches segno's
penalty evaluation. Caught 3 encoder bugs (multi-block interleave
offsets, transposed version info, mask eval ordering).
- QEMU e2e: page-fault dump screendump decodes via zbarimg, content
matches the serial log line-for-line.
- test-msg.sh and test-fault.sh both PASS; debug+release 0 warnings.
Diffstat (limited to 'rsnix/src/gop.rs')
| -rw-r--r-- | rsnix/src/gop.rs | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 012a9d0..ecf8fa9 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -516,6 +516,89 @@ pub fn dump_lines(lines: &[&str]) { | |||
| 516 | } | 516 | } |
| 517 | } | 517 | } |
| 518 | } | 518 | } |
| 519 | // Scannable QR of the same text below the band (heap-free encoder; | ||
| 520 | // real hardware has no serial console — photograph the screen). | ||
| 521 | dump_qr(lines); | ||
| 522 | } | ||
| 523 | |||
| 524 | /// Render the crash/fault lines as a QR code below the dump band. | ||
| 525 | /// | ||
| 526 | /// The joined text is built in a `.bss` buffer (the fault path must not | ||
| 527 | /// allocate) and encoded byte-mode EC-L by `crate::qr` (also heap-free, | ||
| 528 | /// static matrix). Over-long text is truncated with a "…" marker. If | ||
| 529 | /// the text does not fit a QR or there is no framebuffer, this is a no-op | ||
| 530 | /// and the band text remains the only channel. | ||
| 531 | static mut QR_TEXT: [u8; crate::qr::MAX_BYTES + 4] = [0; crate::qr::MAX_BYTES + 4]; | ||
| 532 | |||
| 533 | pub fn dump_qr(lines: &[&str]) { | ||
| 534 | let Some(fb) = fb().as_ref() else { | ||
| 535 | return; | ||
| 536 | }; | ||
| 537 | unsafe { | ||
| 538 | let text = &mut *core::ptr::addr_of_mut!(QR_TEXT); | ||
| 539 | let mut n = 0usize; | ||
| 540 | for line in lines { | ||
| 541 | let b = line.as_bytes(); | ||
| 542 | let take = b.len().min(text.len() - n); | ||
| 543 | text[n..n + take].copy_from_slice(&b[..take]); | ||
| 544 | n += take; | ||
| 545 | if n < text.len() { | ||
| 546 | text[n] = b'\n'; | ||
| 547 | n += 1; | ||
| 548 | } | ||
| 549 | } | ||
| 550 | while n > 0 && text[n - 1] == b'\n' { | ||
| 551 | n -= 1; | ||
| 552 | } | ||
| 553 | if n == 0 { | ||
| 554 | return; | ||
| 555 | } | ||
| 556 | let cap = crate::qr::MAX_BYTES.saturating_sub(3); | ||
| 557 | if n > cap { | ||
| 558 | n = cap; | ||
| 559 | text[n..n + 3].copy_from_slice(b"..."); | ||
| 560 | n += 3; | ||
| 561 | } | ||
| 562 | if crate::qr::encode(&text[..n]) == 0 { | ||
| 563 | return; | ||
| 564 | } | ||
| 565 | } | ||
| 566 | render_qr(fb, lines.len() * 16); | ||
| 567 | } | ||
| 568 | |||
| 569 | /// Draw the current QR matrix (black on white, quiet zone) centred below | ||
| 570 | /// the band, scaled to the largest square that fits. | ||
| 571 | fn render_qr(fb: &Framebuffer, band_px: usize) { | ||
| 572 | let size = crate::qr::size(); | ||
| 573 | if size == 0 { | ||
| 574 | return; | ||
| 575 | } | ||
| 576 | let total = size + 8; // 4-module quiet zone each side | ||
| 577 | let avail_w = fb.width.saturating_sub(24); | ||
| 578 | let avail_h = fb.height.saturating_sub(band_px + 16); | ||
| 579 | let scale = (avail_w / total).min(avail_h / total); | ||
| 580 | if scale < 1 { | ||
| 581 | return; | ||
| 582 | } | ||
| 583 | let px = total * scale; | ||
| 584 | let x0 = (fb.width - px) / 2; | ||
| 585 | let y0 = band_px + 8 + (avail_h.saturating_sub(px) / 2); | ||
| 586 | let white = rgb(0xFF, 0xFF, 0xFF); | ||
| 587 | let black = rgb(0, 0, 0); | ||
| 588 | fill_rect(x0, y0, px, px, white); | ||
| 589 | for y in 0..size { | ||
| 590 | for x in 0..size { | ||
| 591 | if crate::qr::module(x, y) { | ||
| 592 | fill_rect( | ||
| 593 | x0 + (x + 4) * scale, | ||
| 594 | y0 + (y + 4) * scale, | ||
| 595 | scale, | ||
| 596 | scale, | ||
| 597 | black, | ||
| 598 | ); | ||
| 599 | } | ||
| 600 | } | ||
| 601 | } | ||
| 519 | } | 602 | } |
| 520 | 603 | ||
| 521 | /// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. | 604 | /// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. |
