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 | |
| 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.
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 83 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 1 | ||||
| -rw-r--r-- | rsnix/src/qr.rs | 852 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 6 | ||||
| -rw-r--r-- | tools/qrcheck.py | 208 | ||||
| -rw-r--r-- | tools/qrcheck/.gitignore | 1 | ||||
| -rw-r--r-- | tools/qrcheck/Cargo.lock | 7 | ||||
| -rw-r--r-- | tools/qrcheck/Cargo.toml | 13 | ||||
| -rw-r--r-- | tools/qrcheck/src/main.rs | 58 |
11 files changed, 1232 insertions, 5 deletions
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.5.7" | 7 | version = "0.5.8" |
| 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.7" | 22 | version = "0.5.8" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.5.7" | 26 | version = "0.5.8" |
| 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.7" | 6 | version = "0.5.8" |
| 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/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. |
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index f4645a5..dbb327c 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs | |||
| @@ -14,6 +14,7 @@ pub mod klog; | |||
| 14 | pub mod memory; | 14 | pub mod memory; |
| 15 | pub mod paging; | 15 | pub mod paging; |
| 16 | pub mod pci; | 16 | pub mod pci; |
| 17 | pub mod qr; | ||
| 17 | pub mod power; | 18 | pub mod power; |
| 18 | pub mod ps2; | 19 | pub mod ps2; |
| 19 | pub mod rtc; | 20 | pub mod rtc; |
diff --git a/rsnix/src/qr.rs b/rsnix/src/qr.rs new file mode 100644 index 0000000..84151e6 --- /dev/null +++ b/rsnix/src/qr.rs | |||
| @@ -0,0 +1,852 @@ | |||
| 1 | //! QR Code encoder — byte mode, EC level L, versions 1..=40. | ||
| 2 | //! | ||
| 3 | //! `no_std` + heap-free: the module matrix and all work buffers live in | ||
| 4 | //! `.bss` statics, so the encoder is safe to run from the fault/panic | ||
| 5 | //! path. Real hardware has no serial console, so the crash log is drawn | ||
| 6 | //! on the framebuffer as a scannable QR code instead of a frozen text | ||
| 7 | //! band. | ||
| 8 | //! | ||
| 9 | //! Tables follow ISO/IEC 18004 (block structure Table 9 level L, | ||
| 10 | //! alignment positions, version info), cross-verified bit-for-bit | ||
| 11 | //! against segno (`tools/qrcheck` + `tools/qrcheck.py`). | ||
| 12 | |||
| 13 | /// Largest supported version (177 modules; v40-L byte capacity 2953). | ||
| 14 | pub const MAX_VERSION: usize = 40; | ||
| 15 | /// Modules per side of the largest supported QR. | ||
| 16 | pub const MAX_MODULES: usize = 177; | ||
| 17 | /// v40-L byte-mode capacity — the hard input limit. | ||
| 18 | pub const MAX_BYTES: usize = 2953; | ||
| 19 | |||
| 20 | const MATRIX_LEN: usize = MAX_MODULES * MAX_MODULES; | ||
| 21 | |||
| 22 | /// EC-L block structure per version: | ||
| 23 | /// `(ec_codewords_per_block, &[(num_blocks, data_codewords_per_block)])`. | ||
| 24 | const ECC_L: &[(u8, &[(u8, u16)])] = &[ | ||
| 25 | // v1 | ||
| 26 | (7, &[(1, 19)]), | ||
| 27 | // v2 | ||
| 28 | (10, &[(1, 34)]), | ||
| 29 | // v3 | ||
| 30 | (15, &[(1, 55)]), | ||
| 31 | // v4 | ||
| 32 | (20, &[(1, 80)]), | ||
| 33 | // v5 | ||
| 34 | (26, &[(1, 108)]), | ||
| 35 | // v6 | ||
| 36 | (18, &[(2, 68)]), | ||
| 37 | // v7 | ||
| 38 | (20, &[(2, 78)]), | ||
| 39 | // v8 | ||
| 40 | (24, &[(2, 97)]), | ||
| 41 | // v9 | ||
| 42 | (30, &[(2, 116)]), | ||
| 43 | // v10 | ||
| 44 | (18, &[(2, 68), (2, 69)]), | ||
| 45 | // v11 | ||
| 46 | (20, &[(4, 81)]), | ||
| 47 | // v12 | ||
| 48 | (24, &[(2, 92), (2, 93)]), | ||
| 49 | // v13 | ||
| 50 | (26, &[(4, 107)]), | ||
| 51 | // v14 | ||
| 52 | (30, &[(3, 115), (1, 116)]), | ||
| 53 | // v15 | ||
| 54 | (22, &[(5, 87), (1, 88)]), | ||
| 55 | // v16 | ||
| 56 | (24, &[(5, 98), (1, 99)]), | ||
| 57 | // v17 | ||
| 58 | (28, &[(1, 107), (5, 108)]), | ||
| 59 | // v18 | ||
| 60 | (30, &[(5, 120), (1, 121)]), | ||
| 61 | // v19 | ||
| 62 | (28, &[(3, 113), (4, 114)]), | ||
| 63 | // v20 | ||
| 64 | (28, &[(3, 107), (5, 108)]), | ||
| 65 | // v21 | ||
| 66 | (28, &[(4, 116), (4, 117)]), | ||
| 67 | // v22 | ||
| 68 | (28, &[(2, 111), (7, 112)]), | ||
| 69 | // v23 | ||
| 70 | (30, &[(4, 121), (5, 122)]), | ||
| 71 | // v24 | ||
| 72 | (30, &[(6, 117), (4, 118)]), | ||
| 73 | // v25 | ||
| 74 | (26, &[(8, 106), (4, 107)]), | ||
| 75 | // v26 | ||
| 76 | (28, &[(10, 114), (2, 115)]), | ||
| 77 | // v27 | ||
| 78 | (30, &[(8, 122), (4, 123)]), | ||
| 79 | // v28 | ||
| 80 | (30, &[(3, 117), (10, 118)]), | ||
| 81 | // v29 | ||
| 82 | (30, &[(7, 116), (7, 117)]), | ||
| 83 | // v30 | ||
| 84 | (30, &[(5, 115), (10, 116)]), | ||
| 85 | // v31 | ||
| 86 | (30, &[(13, 115), (3, 116)]), | ||
| 87 | // v32 | ||
| 88 | (30, &[(17, 115)]), | ||
| 89 | // v33 | ||
| 90 | (30, &[(17, 115), (1, 116)]), | ||
| 91 | // v34 | ||
| 92 | (30, &[(13, 115), (6, 116)]), | ||
| 93 | // v35 | ||
| 94 | (30, &[(12, 121), (7, 122)]), | ||
| 95 | // v36 | ||
| 96 | (30, &[(6, 121), (14, 122)]), | ||
| 97 | // v37 | ||
| 98 | (30, &[(17, 122), (4, 123)]), | ||
| 99 | // v38 | ||
| 100 | (30, &[(4, 122), (18, 123)]), | ||
| 101 | // v39 | ||
| 102 | (30, &[(20, 117), (4, 118)]), | ||
| 103 | // v40 | ||
| 104 | (30, &[(19, 118), (6, 119)]), | ||
| 105 | ]; | ||
| 106 | |||
| 107 | /// Alignment-pattern centre coordinate sets per version (index = version-2; | ||
| 108 | /// v1 has none). Patterns sit at every (r, c) pair from the set, minus the | ||
| 109 | /// three corners that overlap the finder patterns. | ||
| 110 | const ALIGNMENT_POS: &[&[u8]] = &[ | ||
| 111 | // v2 | ||
| 112 | &[6, 18], | ||
| 113 | // v3 | ||
| 114 | &[6, 22], | ||
| 115 | // v4 | ||
| 116 | &[6, 26], | ||
| 117 | // v5 | ||
| 118 | &[6, 30], | ||
| 119 | // v6 | ||
| 120 | &[6, 34], | ||
| 121 | // v7 | ||
| 122 | &[6, 22, 38], | ||
| 123 | // v8 | ||
| 124 | &[6, 24, 42], | ||
| 125 | // v9 | ||
| 126 | &[6, 26, 46], | ||
| 127 | // v10 | ||
| 128 | &[6, 28, 50], | ||
| 129 | // v11 | ||
| 130 | &[6, 30, 54], | ||
| 131 | // v12 | ||
| 132 | &[6, 32, 58], | ||
| 133 | // v13 | ||
| 134 | &[6, 34, 62], | ||
| 135 | // v14 | ||
| 136 | &[6, 26, 46, 66], | ||
| 137 | // v15 | ||
| 138 | &[6, 26, 48, 70], | ||
| 139 | // v16 | ||
| 140 | &[6, 26, 50, 74], | ||
| 141 | // v17 | ||
| 142 | &[6, 30, 54, 78], | ||
| 143 | // v18 | ||
| 144 | &[6, 30, 56, 82], | ||
| 145 | // v19 | ||
| 146 | &[6, 30, 58, 86], | ||
| 147 | // v20 | ||
| 148 | &[6, 34, 62, 90], | ||
| 149 | // v21 | ||
| 150 | &[6, 28, 50, 72, 94], | ||
| 151 | // v22 | ||
| 152 | &[6, 26, 50, 74, 98], | ||
| 153 | // v23 | ||
| 154 | &[6, 30, 54, 78, 102], | ||
| 155 | // v24 | ||
| 156 | &[6, 28, 54, 80, 106], | ||
| 157 | // v25 | ||
| 158 | &[6, 32, 58, 84, 110], | ||
| 159 | // v26 | ||
| 160 | &[6, 30, 58, 86, 114], | ||
| 161 | // v27 | ||
| 162 | &[6, 34, 62, 90, 118], | ||
| 163 | // v28 | ||
| 164 | &[6, 26, 50, 74, 98, 122], | ||
| 165 | // v29 | ||
| 166 | &[6, 30, 54, 78, 102, 126], | ||
| 167 | // v30 | ||
| 168 | &[6, 26, 52, 78, 104, 130], | ||
| 169 | // v31 | ||
| 170 | &[6, 30, 56, 82, 108, 134], | ||
| 171 | // v32 | ||
| 172 | &[6, 34, 60, 86, 112, 138], | ||
| 173 | // v33 | ||
| 174 | &[6, 30, 58, 86, 114, 142], | ||
| 175 | // v34 | ||
| 176 | &[6, 34, 62, 90, 118, 146], | ||
| 177 | // v35 | ||
| 178 | &[6, 30, 54, 78, 102, 126, 150], | ||
| 179 | // v36 | ||
| 180 | &[6, 24, 50, 76, 102, 128, 154], | ||
| 181 | // v37 | ||
| 182 | &[6, 28, 54, 80, 106, 132, 158], | ||
| 183 | // v38 | ||
| 184 | &[6, 32, 58, 84, 110, 136, 162], | ||
| 185 | // v39 | ||
| 186 | &[6, 26, 54, 82, 110, 138, 166], | ||
| 187 | // v40 | ||
| 188 | &[6, 30, 58, 86, 114, 142, 170], | ||
| 189 | ]; | ||
| 190 | |||
| 191 | /// Version info 18-bit BCH values (index = version-7, v7..v40). | ||
| 192 | const VERSION_INFO: &[u32] = &[ | ||
| 193 | 31892, 34236, 39577, 42195, 48118, 51042, 55367, 58893, 63784, 68472, 70749, 76311, 79154, | ||
| 194 | 84390, 87683, 92361, 96236, 102084, 102881, 110507, 110734, 117786, 119615, 126325, 127568, | ||
| 195 | 133589, 136944, 141498, 145311, 150283, 152622, 158308, 161089, 167017, | ||
| 196 | ]; | ||
| 197 | |||
| 198 | /// Working matrix: 0 = light, 1 = dark. Row-major, top-left origin. | ||
| 199 | static mut MAT: [u8; MATRIX_LEN] = [0; MATRIX_LEN]; | ||
| 200 | /// Function-module map: 1 = fixed pattern cell (never masked, skipped by the | ||
| 201 | /// data zigzag), 0 = data cell. | ||
| 202 | static mut FUNC: [u8; MATRIX_LEN] = [0; MATRIX_LEN]; | ||
| 203 | /// Modules per side of the current matrix. | ||
| 204 | static mut SIZE: usize = 0; | ||
| 205 | /// Mask chosen by the most recent auto-mask encode. | ||
| 206 | static mut LAST_MASK: usize = 0; | ||
| 207 | /// Total codewords of the most recent encode (debug hook). | ||
| 208 | static mut LAST_TOTAL: usize = 0; | ||
| 209 | |||
| 210 | // GF(256) tables (primitive poly 0x11D), computed once on first encode. | ||
| 211 | static mut GF_EXP: [u8; 512] = [0; 512]; | ||
| 212 | static mut GF_LOG: [u8; 256] = [0; 256]; | ||
| 213 | static mut GF_READY: bool = false; | ||
| 214 | |||
| 215 | // Data codewords (max v40-L = 2956) and the final interleaved codeword | ||
| 216 | // stream (max v40 total = 3706). | ||
| 217 | static mut CW: [u8; 2956] = [0; 2956]; | ||
| 218 | static mut FINAL_CW: [u8; 3706] = [0; 3706]; | ||
| 219 | |||
| 220 | fn gf_init() { | ||
| 221 | unsafe { | ||
| 222 | if !GF_READY { | ||
| 223 | let mut x: u8 = 1; | ||
| 224 | for i in 0..255 { | ||
| 225 | GF_EXP[i] = x; | ||
| 226 | GF_LOG[x as usize] = i as u8; | ||
| 227 | x = if x & 0x80 != 0 { (x << 1) ^ 0x1D } else { x << 1 }; | ||
| 228 | } | ||
| 229 | for i in 255..512 { | ||
| 230 | GF_EXP[i] = GF_EXP[i - 255]; | ||
| 231 | } | ||
| 232 | GF_READY = true; | ||
| 233 | } | ||
| 234 | } | ||
| 235 | } | ||
| 236 | |||
| 237 | #[inline] | ||
| 238 | fn gf_mul(a: u8, b: u8) -> u8 { | ||
| 239 | if a == 0 || b == 0 { | ||
| 240 | return 0; | ||
| 241 | } | ||
| 242 | unsafe { GF_EXP[GF_LOG[a as usize] as usize + GF_LOG[b as usize] as usize] } | ||
| 243 | } | ||
| 244 | |||
| 245 | /// Build the Reed-Solomon generator polynomial for `ec` EC codewords into | ||
| 246 | /// `out[0..=ec]` (out[0] = leading coefficient, always 1). | ||
| 247 | fn rs_generator(ec: usize, out: &mut [u8; 31]) { | ||
| 248 | unsafe { | ||
| 249 | out[0] = 1; | ||
| 250 | let mut len = 1; | ||
| 251 | for i in 0..ec { | ||
| 252 | for j in (0..len).rev() { | ||
| 253 | out[j + 1] ^= gf_mul(out[j], GF_EXP[i]); | ||
| 254 | } | ||
| 255 | len += 1; | ||
| 256 | } | ||
| 257 | } | ||
| 258 | } | ||
| 259 | |||
| 260 | /// Reed-Solomon encode: `out` (len `ec`) receives the EC codewords. | ||
| 261 | fn rs_encode(data: &[u8], ec: usize, rs_gen: &[u8; 31], out: &mut [u8]) { | ||
| 262 | for i in 0..ec { | ||
| 263 | out[i] = 0; | ||
| 264 | } | ||
| 265 | for &b in data { | ||
| 266 | let factor = b ^ out[0]; | ||
| 267 | for i in 0..ec - 1 { | ||
| 268 | out[i] = out[i + 1]; | ||
| 269 | } | ||
| 270 | out[ec - 1] = 0; | ||
| 271 | if factor != 0 { | ||
| 272 | for i in 0..ec { | ||
| 273 | out[i] ^= gf_mul(rs_gen[i + 1], factor); | ||
| 274 | } | ||
| 275 | } | ||
| 276 | } | ||
| 277 | } | ||
| 278 | |||
| 279 | /// Byte-mode data capacity of `version` (EC-L), including the 4-bit mode | ||
| 280 | /// indicator and 8/16-bit character-count field, rounded down to whole bytes. | ||
| 281 | fn byte_capacity(version: usize) -> usize { | ||
| 282 | let (_, blocks) = ECC_L[version - 1]; | ||
| 283 | let data_cw: usize = blocks.iter().map(|(n, d)| *n as usize * *d as usize).sum(); | ||
| 284 | let count_bits = if version <= 9 { 8 } else { 16 }; | ||
| 285 | (data_cw * 8 - 4 - count_bits) / 8 | ||
| 286 | } | ||
| 287 | |||
| 288 | /// Bit-packed writer over a byte buffer. | ||
| 289 | struct BitWriter<'a> { | ||
| 290 | buf: &'a mut [u8], | ||
| 291 | bitpos: usize, | ||
| 292 | } | ||
| 293 | |||
| 294 | impl BitWriter<'_> { | ||
| 295 | fn push(&mut self, value: u32, nbits: u8) { | ||
| 296 | for i in (0..nbits).rev() { | ||
| 297 | let bit = ((value >> i) & 1) as u8; | ||
| 298 | let byte = self.bitpos / 8; | ||
| 299 | if bit != 0 { | ||
| 300 | self.buf[byte] |= 1 << (7 - (self.bitpos % 8)); | ||
| 301 | } | ||
| 302 | self.bitpos += 1; | ||
| 303 | } | ||
| 304 | } | ||
| 305 | } | ||
| 306 | |||
| 307 | /// Build the data codeword stream for `data` (mode 0100, byte count, pad to | ||
| 308 | /// `data_cw` bytes with 0xEC/0x11). | ||
| 309 | fn build_codewords(data: &[u8], data_cw: usize, version: usize) { | ||
| 310 | unsafe { | ||
| 311 | let cw = &mut *core::ptr::addr_of_mut!(CW); | ||
| 312 | for i in 0..data_cw { | ||
| 313 | cw[i] = 0; | ||
| 314 | } | ||
| 315 | let count_bits = if version <= 9 { 8 } else { 16 }; | ||
| 316 | let mut w = BitWriter { buf: &mut cw[..data_cw], bitpos: 0 }; | ||
| 317 | w.push(0b0100, 4); | ||
| 318 | w.push(data.len() as u32, count_bits); | ||
| 319 | for &b in data { | ||
| 320 | w.push(b as u32, 8); | ||
| 321 | } | ||
| 322 | let capacity_bits = data_cw * 8; | ||
| 323 | // Terminator: up to 4 zero bits while capacity allows. | ||
| 324 | let mut term = 0; | ||
| 325 | while w.bitpos < capacity_bits && term < 4 { | ||
| 326 | w.push(0, 1); | ||
| 327 | term += 1; | ||
| 328 | } | ||
| 329 | // Pad to a byte boundary, then 0xEC/0x11 alternating pad bytes. | ||
| 330 | while !w.bitpos.is_multiple_of(8) && w.bitpos < capacity_bits { | ||
| 331 | w.push(0, 1); | ||
| 332 | } | ||
| 333 | let mut pad: u8 = 0xEC; | ||
| 334 | while w.bitpos < capacity_bits { | ||
| 335 | w.push(pad as u32, 8); | ||
| 336 | pad = if pad == 0xEC { 0x11 } else { 0xEC }; | ||
| 337 | } | ||
| 338 | } | ||
| 339 | } | ||
| 340 | |||
| 341 | fn n_modules(version: usize) -> usize { | ||
| 342 | 17 + 4 * version | ||
| 343 | } | ||
| 344 | |||
| 345 | /// Mark a module as a function cell with value `val`. | ||
| 346 | fn func_set(x: usize, y: usize, val: u8) { | ||
| 347 | unsafe { | ||
| 348 | let idx = y * SIZE + x; | ||
| 349 | MAT[idx] = val; | ||
| 350 | FUNC[idx] = 1; | ||
| 351 | } | ||
| 352 | } | ||
| 353 | |||
| 354 | fn finder_cell(x: usize, y: usize, r: usize, c: usize) { | ||
| 355 | let dark = r == 0 || r == 6 || c == 0 || c == 6 || ((2..=4).contains(&r) && (2..=4).contains(&c)); | ||
| 356 | func_set(x + c, y + r, dark as u8); | ||
| 357 | } | ||
| 358 | |||
| 359 | fn draw_finders(n: usize) { | ||
| 360 | // Top-left (0,0). | ||
| 361 | for r in 0..7 { | ||
| 362 | for c in 0..7 { | ||
| 363 | finder_cell(0, 0, r, c); | ||
| 364 | } | ||
| 365 | } | ||
| 366 | for c in 0..8 { | ||
| 367 | func_set(c, 7, 0); // bottom separator | ||
| 368 | } | ||
| 369 | for r in 0..8 { | ||
| 370 | func_set(7, r, 0); // right separator | ||
| 371 | } | ||
| 372 | // Top-right (n-7, 0). | ||
| 373 | for r in 0..7 { | ||
| 374 | for c in 0..7 { | ||
| 375 | finder_cell(n - 7, 0, r, c); | ||
| 376 | } | ||
| 377 | } | ||
| 378 | for c in 0..8 { | ||
| 379 | func_set(n - 8 + c, 7, 0); // bottom separator | ||
| 380 | } | ||
| 381 | for r in 0..8 { | ||
| 382 | func_set(n - 8, r, 0); // left separator | ||
| 383 | } | ||
| 384 | // Bottom-left (0, n-7). | ||
| 385 | for r in 0..7 { | ||
| 386 | for c in 0..7 { | ||
| 387 | finder_cell(0, n - 7, r, c); | ||
| 388 | } | ||
| 389 | } | ||
| 390 | for c in 0..8 { | ||
| 391 | func_set(c, n - 8, 0); // top separator | ||
| 392 | } | ||
| 393 | for r in 0..8 { | ||
| 394 | func_set(7, n - 8 + r, 0); // right separator | ||
| 395 | } | ||
| 396 | } | ||
| 397 | |||
| 398 | fn draw_timing(n: usize) { | ||
| 399 | for i in 8..=n - 9 { | ||
| 400 | let dark = (i % 2 == 0) as u8; | ||
| 401 | func_set(i, 6, dark); | ||
| 402 | func_set(6, i, dark); | ||
| 403 | } | ||
| 404 | } | ||
| 405 | |||
| 406 | fn draw_alignment(version: usize) { | ||
| 407 | if version < 2 { | ||
| 408 | return; | ||
| 409 | } | ||
| 410 | let coords = ALIGNMENT_POS[version - 2]; | ||
| 411 | let last = *coords.last().unwrap() as usize; | ||
| 412 | for &r in coords { | ||
| 413 | for &c in coords { | ||
| 414 | let (r, c) = (r as usize, c as usize); | ||
| 415 | // Skip the three corners that overlap finder patterns. | ||
| 416 | if (r == 6 && c == 6) || (r == 6 && c == last) || (r == last && c == 6) { | ||
| 417 | continue; | ||
| 418 | } | ||
| 419 | for dr in 0..5 { | ||
| 420 | for dc in 0..5 { | ||
| 421 | let dark = dr == 0 || dr == 4 || dc == 0 || dc == 4 || (dr == 2 && dc == 2); | ||
| 422 | func_set(c - 2 + dc, r - 2 + dr, dark as u8); | ||
| 423 | } | ||
| 424 | } | ||
| 425 | } | ||
| 426 | } | ||
| 427 | } | ||
| 428 | |||
| 429 | /// Mark the two format-info regions as function cells (values set later, | ||
| 430 | /// after mask selection) so the data zigzag skips them. | ||
| 431 | fn reserve_format(n: usize) { | ||
| 432 | for i in 0..6 { | ||
| 433 | func_set(8, i, 0); | ||
| 434 | func_set(5 - i, 8, 0); | ||
| 435 | } | ||
| 436 | func_set(8, 7, 0); | ||
| 437 | func_set(8, 8, 0); | ||
| 438 | func_set(7, 8, 0); | ||
| 439 | // Copy 2: top-right row 8 (bits 0..7), bottom-left col 8 (bits 8..14). | ||
| 440 | for i in 0..8 { | ||
| 441 | func_set(n - 1 - i, 8, 0); | ||
| 442 | } | ||
| 443 | for i in 0..7 { | ||
| 444 | func_set(8, n - 7 + i, 0); | ||
| 445 | } | ||
| 446 | } | ||
| 447 | |||
| 448 | /// Place the interleaved codewords in the data region (no mask yet). | ||
| 449 | fn place_data(total_cw: usize, n: usize) { | ||
| 450 | let mut bit_idx = 0usize; | ||
| 451 | let mut up = true; | ||
| 452 | let mut right = n - 1; | ||
| 453 | while right >= 1 { | ||
| 454 | if right == 6 { | ||
| 455 | right = 5; | ||
| 456 | } | ||
| 457 | for vert in 0..n { | ||
| 458 | let row = if up { n - 1 - vert } else { vert }; | ||
| 459 | for j in 0..2 { | ||
| 460 | let x = right - j; | ||
| 461 | if unsafe { FUNC[row * SIZE + x] } == 0 && bit_idx < total_cw * 8 { | ||
| 462 | let byte = unsafe { FINAL_CW[bit_idx / 8] }; | ||
| 463 | let bit = (byte >> (7 - bit_idx % 8)) & 1; | ||
| 464 | unsafe { MAT[row * SIZE + x] = bit }; | ||
| 465 | bit_idx += 1; | ||
| 466 | } | ||
| 467 | } | ||
| 468 | } | ||
| 469 | up = !up; | ||
| 470 | if right >= 2 { | ||
| 471 | right -= 2; | ||
| 472 | } else { | ||
| 473 | break; | ||
| 474 | } | ||
| 475 | } | ||
| 476 | debug_assert_eq!(bit_idx, total_cw * 8); | ||
| 477 | } | ||
| 478 | |||
| 479 | fn mask_bit(mask: usize, x: usize, y: usize) -> bool { | ||
| 480 | match mask { | ||
| 481 | 0 => (x + y).is_multiple_of(2), | ||
| 482 | 1 => y.is_multiple_of(2), | ||
| 483 | 2 => x.is_multiple_of(3), | ||
| 484 | 3 => (x + y).is_multiple_of(3), | ||
| 485 | 4 => (x / 3 + y / 2).is_multiple_of(2), | ||
| 486 | 5 => (x * y) % 2 + (x * y) % 3 == 0, | ||
| 487 | 6 => ((x * y) % 2 + (x * y) % 3).is_multiple_of(2), | ||
| 488 | 7 => ((x + y) % 2 + (x * y) % 3).is_multiple_of(2), | ||
| 489 | _ => false, | ||
| 490 | } | ||
| 491 | } | ||
| 492 | |||
| 493 | /// Apply (XOR) a mask over the data cells only. | ||
| 494 | fn apply_mask(mask: usize, n: usize) { | ||
| 495 | for y in 0..n { | ||
| 496 | for x in 0..n { | ||
| 497 | if unsafe { FUNC[y * SIZE + x] } == 0 && mask_bit(mask, x, y) { | ||
| 498 | unsafe { MAT[y * SIZE + x] ^= 1 }; | ||
| 499 | } | ||
| 500 | } | ||
| 501 | } | ||
| 502 | } | ||
| 503 | |||
| 504 | /// BCH remainder over GF(2) (`rs_gen` must have its MSB set). | ||
| 505 | fn bch_remainder(mut v: u32, rs_gen: u32) -> u32 { | ||
| 506 | let gl = 32 - rs_gen.leading_zeros(); | ||
| 507 | let mut vl = 32 - v.leading_zeros(); | ||
| 508 | while vl >= gl { | ||
| 509 | v ^= rs_gen << (vl - gl); | ||
| 510 | vl = 32 - v.leading_zeros(); | ||
| 511 | } | ||
| 512 | v | ||
| 513 | } | ||
| 514 | |||
| 515 | /// 15-bit format info for EC level L and `mask`. | ||
| 516 | fn format_info(mask: usize) -> u16 { | ||
| 517 | let data = (0b01 << 3) | mask as u32; | ||
| 518 | let rem = bch_remainder(data << 10, 0x537); | ||
| 519 | (((data << 10) | rem) ^ 0x5412) as u16 | ||
| 520 | } | ||
| 521 | |||
| 522 | fn set_format(format: u16, n: usize) { | ||
| 523 | let b = |i: usize| ((format >> i) & 1) as u8; | ||
| 524 | // Copy 1: around the top-left finder. | ||
| 525 | for i in 0..6 { | ||
| 526 | func_set(8, i, b(i)); | ||
| 527 | } | ||
| 528 | func_set(8, 7, b(6)); | ||
| 529 | func_set(8, 8, b(7)); | ||
| 530 | func_set(7, 8, b(8)); | ||
| 531 | for i in 0..6 { | ||
| 532 | func_set(5 - i, 8, b(9 + i)); | ||
| 533 | } | ||
| 534 | // Copy 2: top-right row 8 (bits 0..7) + bottom-left col 8 (bits 8..14). | ||
| 535 | for i in 0..8 { | ||
| 536 | func_set(n - 1 - i, 8, b(i)); | ||
| 537 | } | ||
| 538 | for i in 0..7 { | ||
| 539 | func_set(8, n - 7 + i, b(8 + i)); | ||
| 540 | } | ||
| 541 | } | ||
| 542 | |||
| 543 | /// Mark the version-info cells as function (leaving MAT light) so the data | ||
| 544 | /// placement skips them. Called before `place_data`; the actual version bits | ||
| 545 | /// are written later by `write_version_info` (ISO: version info is added | ||
| 546 | /// after data masking is chosen, and must not influence the penalty score). | ||
| 547 | fn reserve_version(n: usize) { | ||
| 548 | if n < 45 { | ||
| 549 | return; // no version-info region below version 7 | ||
| 550 | } | ||
| 551 | for i in 0..18 { | ||
| 552 | func_set(n - 11 + (i % 3), i / 3, 0); // upper right: (row i/3, col n-11+i%3) | ||
| 553 | func_set(i / 3, n - 11 + (i % 3), 0); // lower left: (row n-11+i%3, col i/3) | ||
| 554 | } | ||
| 555 | } | ||
| 556 | |||
| 557 | /// Write the 18-bit version info (v7..v40) into the reserved cells. | ||
| 558 | fn write_version_info(version: usize, n: usize) { | ||
| 559 | if version < 7 { | ||
| 560 | return; | ||
| 561 | } | ||
| 562 | let v = VERSION_INFO[version - 7]; | ||
| 563 | // Upper right 3x6 block: bit i at (row i/3, col n - 11 + i%3). | ||
| 564 | // Lower left 6x3 block: bit i at (row n - 11 + i%3, col i/3). | ||
| 565 | for i in 0..18 { | ||
| 566 | let bit = ((v >> i) & 1) as u8; | ||
| 567 | func_set(n - 11 + (i % 3), i / 3, bit); | ||
| 568 | func_set(i / 3, n - 11 + (i % 3), bit); | ||
| 569 | } | ||
| 570 | } | ||
| 571 | |||
| 572 | /// Count N3 penalty occurrences of the finder-like 1:1:3:1:1 core | ||
| 573 | /// (1011101) in `seq`, following segno's `mask_scores` rules: an edge | ||
| 574 | /// position (0 or n-7) always counts; otherwise the 4 modules before and | ||
| 575 | /// after the core must each be entirely light (short windows near the | ||
| 576 | /// edges are allowed). After a counted match the scan resumes at idx+7, | ||
| 577 | /// after a rejected core at idx+4 (identical to segno's find() offsets). | ||
| 578 | fn n3_count(seq: &[u8], n: usize) -> usize { | ||
| 579 | const CORE: [u8; 7] = [1, 0, 1, 1, 1, 0, 1]; | ||
| 580 | let mut count = 0usize; | ||
| 581 | let mut idx = 0usize; | ||
| 582 | while idx + 7 <= n { | ||
| 583 | if seq[idx..idx + 7] != CORE { | ||
| 584 | idx += 1; | ||
| 585 | continue; | ||
| 586 | } | ||
| 587 | let after = idx + 7; | ||
| 588 | let before_light = seq[idx.saturating_sub(4)..idx].iter().all(|&v| v == 0); | ||
| 589 | let after_light = seq[after..(after + 4).min(n)].iter().all(|&v| v == 0); | ||
| 590 | if idx == 0 || idx == n - 7 || before_light || after_light { | ||
| 591 | count += 40; | ||
| 592 | idx += 7; | ||
| 593 | } else { | ||
| 594 | idx += 4; | ||
| 595 | } | ||
| 596 | } | ||
| 597 | count | ||
| 598 | } | ||
| 599 | |||
| 600 | /// Penalty score (rules N1-N4) of the current matrix, used for mask choice. | ||
| 601 | fn penalty(n: usize) -> usize { | ||
| 602 | let mut score = 0usize; | ||
| 603 | |||
| 604 | // N1: runs of >= 5 same colour in rows and columns. | ||
| 605 | for y in 0..n { | ||
| 606 | let mut run = 1usize; | ||
| 607 | let mut prev = unsafe { MAT[y * SIZE] }; | ||
| 608 | for x in 1..n { | ||
| 609 | let cur = unsafe { MAT[y * SIZE + x] }; | ||
| 610 | if cur == prev { | ||
| 611 | run += 1; | ||
| 612 | } else { | ||
| 613 | if run >= 5 { | ||
| 614 | score += 3 + run - 5; | ||
| 615 | } | ||
| 616 | run = 1; | ||
| 617 | prev = cur; | ||
| 618 | } | ||
| 619 | } | ||
| 620 | if run >= 5 { | ||
| 621 | score += 3 + run - 5; | ||
| 622 | } | ||
| 623 | } | ||
| 624 | for x in 0..n { | ||
| 625 | let mut run = 1usize; | ||
| 626 | let mut prev = unsafe { MAT[x] }; | ||
| 627 | for y in 1..n { | ||
| 628 | let cur = unsafe { MAT[y * SIZE + x] }; | ||
| 629 | if cur == prev { | ||
| 630 | run += 1; | ||
| 631 | } else { | ||
| 632 | if run >= 5 { | ||
| 633 | score += 3 + run - 5; | ||
| 634 | } | ||
| 635 | run = 1; | ||
| 636 | prev = cur; | ||
| 637 | } | ||
| 638 | } | ||
| 639 | if run >= 5 { | ||
| 640 | score += 3 + run - 5; | ||
| 641 | } | ||
| 642 | } | ||
| 643 | |||
| 644 | // N2: 2x2 blocks of identical colour. | ||
| 645 | for y in 0..n - 1 { | ||
| 646 | for x in 0..n - 1 { | ||
| 647 | let a = unsafe { MAT[y * SIZE + x] }; | ||
| 648 | if a == unsafe { MAT[y * SIZE + x + 1] } | ||
| 649 | && a == unsafe { MAT[(y + 1) * SIZE + x] } | ||
| 650 | && a == unsafe { MAT[(y + 1) * SIZE + x + 1] } | ||
| 651 | { | ||
| 652 | score += 3; | ||
| 653 | } | ||
| 654 | } | ||
| 655 | } | ||
| 656 | |||
| 657 | // N3: finder-like 1011101 core (1:1:3:1:1) preceded/followed by light | ||
| 658 | // area. Mirrors segno's `mask_scores` exactly (edge positions count, | ||
| 659 | // after a match the scan resumes 7 cells later, after a rejected core | ||
| 660 | // 4 cells later), so the auto-mask choice matches the reference. | ||
| 661 | let mut buf = [0u8; 177]; | ||
| 662 | for y in 0..n { | ||
| 663 | for x in 0..n { | ||
| 664 | buf[x] = unsafe { MAT[y * SIZE + x] }; | ||
| 665 | } | ||
| 666 | score += n3_count(&buf[..n], n); | ||
| 667 | } | ||
| 668 | for x in 0..n { | ||
| 669 | for y in 0..n { | ||
| 670 | buf[y] = unsafe { MAT[y * SIZE + x] }; | ||
| 671 | } | ||
| 672 | score += n3_count(&buf[..n], n); | ||
| 673 | } | ||
| 674 | |||
| 675 | // N4: dark-module proportion. | ||
| 676 | let mut dark = 0usize; | ||
| 677 | for y in 0..n { | ||
| 678 | for x in 0..n { | ||
| 679 | dark += unsafe { MAT[y * SIZE + x] } as usize; | ||
| 680 | } | ||
| 681 | } | ||
| 682 | let total = n * n; | ||
| 683 | let percent = dark * 100 / total; | ||
| 684 | let k = (percent as isize - 50).unsigned_abs() / 5; | ||
| 685 | score += 10 * k; | ||
| 686 | |||
| 687 | score | ||
| 688 | } | ||
| 689 | |||
| 690 | /// Encode `data` with a fixed version and mask (`None` = auto mask via | ||
| 691 | /// penalty scoring). Reference/test hook; the kernel path uses `encode`. | ||
| 692 | /// Returns 0 if `version` cannot hold `data` or `mask` is invalid. | ||
| 693 | pub fn encode_at(data: &[u8], version: usize, mask: Option<usize>) -> usize { | ||
| 694 | if !(1..=MAX_VERSION).contains(&version) || data.len() > byte_capacity(version) { | ||
| 695 | return 0; | ||
| 696 | } | ||
| 697 | if let Some(m) = mask | ||
| 698 | && m > 7 { | ||
| 699 | return 0; | ||
| 700 | } | ||
| 701 | gf_init(); | ||
| 702 | |||
| 703 | let (ec_per, blocks) = ECC_L[version - 1]; | ||
| 704 | let data_cw: usize = blocks.iter().map(|(n, d)| *n as usize * *d as usize).sum(); | ||
| 705 | let total_cw: usize = blocks | ||
| 706 | .iter() | ||
| 707 | .map(|(n, d)| *n as usize * (*d as usize + ec_per as usize)) | ||
| 708 | .sum(); | ||
| 709 | |||
| 710 | build_codewords(data, data_cw, version); | ||
| 711 | |||
| 712 | // Expand the block structure into per-block offsets/lengths. | ||
| 713 | let mut blk_off = [0usize; 32]; | ||
| 714 | let mut blk_len = [0usize; 32]; | ||
| 715 | let mut nblocks = 0usize; | ||
| 716 | let mut off = 0usize; | ||
| 717 | for &(nb, nd) in blocks { | ||
| 718 | for _ in 0..nb { | ||
| 719 | blk_off[nblocks] = off; | ||
| 720 | blk_len[nblocks] = nd as usize; | ||
| 721 | off += nd as usize; | ||
| 722 | nblocks += 1; | ||
| 723 | } | ||
| 724 | } | ||
| 725 | |||
| 726 | let mut rs_gen = [0u8; 31]; | ||
| 727 | rs_generator(ec_per as usize, &mut rs_gen); | ||
| 728 | |||
| 729 | unsafe { | ||
| 730 | let cw = &*core::ptr::addr_of_mut!(CW); | ||
| 731 | let final_cw = &mut *core::ptr::addr_of_mut!(FINAL_CW); | ||
| 732 | |||
| 733 | // Interleave data codewords column-major over blocks. | ||
| 734 | let max_data = *blk_len.iter().max().unwrap(); | ||
| 735 | let mut k = 0usize; | ||
| 736 | for i in 0..max_data { | ||
| 737 | for b in 0..nblocks { | ||
| 738 | if i < blk_len[b] { | ||
| 739 | final_cw[k] = cw[blk_off[b] + i]; | ||
| 740 | k += 1; | ||
| 741 | } | ||
| 742 | } | ||
| 743 | } | ||
| 744 | // Encode EC per block, interleave column-major. | ||
| 745 | let mut ecs = [[0u8; 30]; 32]; | ||
| 746 | for b in 0..nblocks { | ||
| 747 | rs_encode( | ||
| 748 | &cw[blk_off[b]..blk_off[b] + blk_len[b]], | ||
| 749 | ec_per as usize, | ||
| 750 | &rs_gen, | ||
| 751 | &mut ecs[b][..ec_per as usize], | ||
| 752 | ); | ||
| 753 | } | ||
| 754 | for i in 0..ec_per as usize { | ||
| 755 | for b in 0..nblocks { | ||
| 756 | final_cw[k] = ecs[b][i]; | ||
| 757 | k += 1; | ||
| 758 | } | ||
| 759 | } | ||
| 760 | debug_assert_eq!(k, total_cw); | ||
| 761 | } | ||
| 762 | |||
| 763 | let n = n_modules(version); | ||
| 764 | unsafe { LAST_TOTAL = total_cw }; | ||
| 765 | unsafe { | ||
| 766 | SIZE = n; | ||
| 767 | for i in 0..n * n { | ||
| 768 | MAT[i] = 0; | ||
| 769 | FUNC[i] = 0; | ||
| 770 | } | ||
| 771 | } | ||
| 772 | |||
| 773 | draw_finders(n); | ||
| 774 | draw_timing(n); | ||
| 775 | draw_alignment(version); | ||
| 776 | func_set(8, n - 8, 1); // dark module (row n-8, col 8) | ||
| 777 | reserve_version(n); | ||
| 778 | reserve_format(n); | ||
| 779 | |||
| 780 | place_data(total_cw, n); | ||
| 781 | |||
| 782 | if let Some(m) = mask { | ||
| 783 | apply_mask(m, n); | ||
| 784 | set_format(format_info(m), n); | ||
| 785 | write_version_info(version, n); | ||
| 786 | unsafe { LAST_MASK = m }; | ||
| 787 | } else { | ||
| 788 | // ISO/IEC 18004: format and version info are added *after* mask | ||
| 789 | // selection (they depend on the mask), so the penalty must be | ||
| 790 | // evaluated with those regions still light. | ||
| 791 | let mut best_mask = 0usize; | ||
| 792 | let mut best_score = usize::MAX; | ||
| 793 | for m in 0..8 { | ||
| 794 | apply_mask(m, n); | ||
| 795 | let s = penalty(n); | ||
| 796 | if s < best_score { | ||
| 797 | best_score = s; | ||
| 798 | best_mask = m; | ||
| 799 | } | ||
| 800 | // Un-mask for the next candidate. | ||
| 801 | apply_mask(m, n); | ||
| 802 | } | ||
| 803 | apply_mask(best_mask, n); | ||
| 804 | set_format(format_info(best_mask), n); | ||
| 805 | write_version_info(version, n); | ||
| 806 | unsafe { LAST_MASK = best_mask }; | ||
| 807 | } | ||
| 808 | |||
| 809 | version | ||
| 810 | } | ||
| 811 | |||
| 812 | /// Encode `data` (byte mode, EC-L): smallest fitting version, best mask. | ||
| 813 | /// Returns the version used (1..=40) or 0 if `data` is too long. | ||
| 814 | pub fn encode(data: &[u8]) -> usize { | ||
| 815 | if data.is_empty() || data.len() > MAX_BYTES { | ||
| 816 | return 0; | ||
| 817 | } | ||
| 818 | let mut version = 0; | ||
| 819 | for v in 1..=MAX_VERSION { | ||
| 820 | if byte_capacity(v) >= data.len() { | ||
| 821 | version = v; | ||
| 822 | break; | ||
| 823 | } | ||
| 824 | } | ||
| 825 | if version == 0 { | ||
| 826 | return 0; | ||
| 827 | } | ||
| 828 | encode_at(data, version, None) | ||
| 829 | } | ||
| 830 | |||
| 831 | /// Modules per side of the most recent successful encode (0 = none). | ||
| 832 | pub fn size() -> usize { | ||
| 833 | unsafe { SIZE } | ||
| 834 | } | ||
| 835 | |||
| 836 | /// Mask chosen by the most recent auto-mask encode (0..7; debug hook for | ||
| 837 | /// the host-side cross-check tool). | ||
| 838 | pub fn last_mask() -> usize { | ||
| 839 | unsafe { LAST_MASK } | ||
| 840 | } | ||
| 841 | |||
| 842 | /// Final interleaved codeword stream (data + EC) of the most recent | ||
| 843 | /// encode — debug hook for the host-side cross-check tool. | ||
| 844 | pub fn codeword_stream() -> &'static [u8] { | ||
| 845 | unsafe { &FINAL_CW[..LAST_TOTAL] } | ||
| 846 | } | ||
| 847 | |||
| 848 | /// Whether module `(x, y)` of the current matrix is dark. Valid for | ||
| 849 | /// `x, y < size()`. | ||
| 850 | pub fn module(x: usize, y: usize) -> bool { | ||
| 851 | unsafe { MAT[y * SIZE + x] != 0 } | ||
| 852 | } | ||
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 6e1f12f..88f8d43 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -250,7 +250,11 @@ pub fn debug_verify() { | |||
| 250 | } | 250 | } |
| 251 | msg(&alloc::format!( | 251 | msg(&alloc::format!( |
| 252 | "verify: grids c nz={} blank={} r nz={} blank={} (of {})", | 252 | "verify: grids c nz={} blank={} r nz={} blank={} (of {})", |
| 253 | c_nz, c_blank, r_nz, r_blank, total | 253 | c_nz, |
| 254 | c_blank, | ||
| 255 | r_nz, | ||
| 256 | r_blank, | ||
| 257 | total | ||
| 254 | )); | 258 | )); |
| 255 | if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok { | 259 | if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok { |
| 256 | msg("verify: FAILED — cells/rendered not sane at shell entry"); | 260 | msg("verify: FAILED — cells/rendered not sane at shell entry"); |
diff --git a/tools/qrcheck.py b/tools/qrcheck.py new file mode 100644 index 0000000..305594c --- /dev/null +++ b/tools/qrcheck.py | |||
| @@ -0,0 +1,208 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Cross-check the kernel QR encoder (tools/qrcheck) against segno. | ||
| 3 | |||
| 4 | Strategy: segno's own bitstream differs from the kernel's in the pad bits | ||
| 5 | (segno emits a full extra zero byte when the stream is already byte-aligned), | ||
| 6 | so instead of comparing against `segno.make()` directly we feed the kernel's | ||
| 7 | own interleaved codeword stream through segno's placement primitives | ||
| 8 | (make_matrix / add_finder_patterns / add_alignment_patterns / add_codewords / | ||
| 9 | apply_mask / add_format_info / add_version_info) and compare matrices | ||
| 10 | bit-for-bit. For auto-mask cases the chosen mask is validated against | ||
| 11 | segno's `mask_scores` penalty evaluation on the same (kernel-stream) matrix. | ||
| 12 | """ | ||
| 13 | import importlib | ||
| 14 | import subprocess | ||
| 15 | import sys | ||
| 16 | |||
| 17 | sys.path.insert(0, "/tmp/segno_pkg") | ||
| 18 | segno = importlib.import_module("segno") # vendored reference at /tmp/segno_pkg | ||
| 19 | E = importlib.import_module("segno.encoder") | ||
| 20 | C = importlib.import_module("segno.consts") | ||
| 21 | |||
| 22 | BIN = "tools/qrcheck/target/debug/qrcheck" | ||
| 23 | ROOT = "/home/salmon/Documents/rust/rsdos" | ||
| 24 | |||
| 25 | |||
| 26 | def run(args): | ||
| 27 | out = subprocess.run(args, capture_output=True, text=True, cwd=ROOT) | ||
| 28 | if out.returncode != 0: | ||
| 29 | print("RUST FAIL:", out.stderr.strip()) | ||
| 30 | sys.exit(1) | ||
| 31 | return out.stdout | ||
| 32 | |||
| 33 | |||
| 34 | def rust_matrix(text, version, mask): | ||
| 35 | args = [BIN, text] | ||
| 36 | if version: | ||
| 37 | args.append(str(version)) | ||
| 38 | if mask is not None: | ||
| 39 | args.append(str(mask)) | ||
| 40 | lines = run(args).splitlines() | ||
| 41 | return lines[0], lines[1:] | ||
| 42 | |||
| 43 | |||
| 44 | def rust_raw(text, version): | ||
| 45 | """Kernel interleaved codeword stream (data+EC) as bytes.""" | ||
| 46 | hexs = run([BIN, "--raw", text, str(version)]).strip() | ||
| 47 | return bytes.fromhex(hexs) | ||
| 48 | |||
| 49 | |||
| 50 | def _to_bits(my_cw): | ||
| 51 | try: | ||
| 52 | return [int(b) for byte in my_cw for b in f"{byte:08b}"] | ||
| 53 | except (ValueError, TypeError) as exc: | ||
| 54 | print(f"bad codeword stream {my_cw!r}: {exc}") | ||
| 55 | sys.exit(1) | ||
| 56 | |||
| 57 | |||
| 58 | def build_matrix(version, my_cw, mask, with_info): | ||
| 59 | """Expected matrix from the kernel's codeword stream via segno primitives. | ||
| 60 | |||
| 61 | `with_info=True` also writes format + version info (final symbol); | ||
| 62 | `with_info=False` leaves them light (as during mask evaluation). | ||
| 63 | """ | ||
| 64 | n = E.calc_matrix_size(version) | ||
| 65 | bits = _to_bits(my_cw) | ||
| 66 | m = E.make_matrix(n, n) | ||
| 67 | E.add_finder_patterns(m, n, n) | ||
| 68 | E.add_alignment_patterns(m, n, n) | ||
| 69 | E.add_codewords(m, bits, version) | ||
| 70 | fm = E.make_matrix(n, n) | ||
| 71 | E.add_finder_patterns(fm, n, n) | ||
| 72 | E.add_alignment_patterns(fm, n, n) | ||
| 73 | fm[-8][8] = 0x1 | ||
| 74 | E.apply_mask(m, E.get_data_mask_functions(False)[mask], n, n, | ||
| 75 | lambda i, j: fm[i][j] > 0x1) | ||
| 76 | if with_info: | ||
| 77 | E.add_format_info(m, version, C.ERROR_LEVEL_L, mask) | ||
| 78 | E.add_version_info(m, version) | ||
| 79 | # Normalize leftover (remainder) cells: 0x2/0x3 -> 0/1. | ||
| 80 | return [[v & 0x1 if v in (2, 3) else v for v in row] for row in m] | ||
| 81 | |||
| 82 | |||
| 83 | def render(rows): | ||
| 84 | return ["".join("#" if c else "." for c in row) for row in rows] | ||
| 85 | |||
| 86 | |||
| 87 | def compare_forced(text, version, mask): | ||
| 88 | hdr, rows = rust_matrix(text, version, mask) | ||
| 89 | my_cw = rust_raw(text, version) | ||
| 90 | exp = build_matrix(version, my_cw, mask, with_info=True) | ||
| 91 | srows = render(exp) | ||
| 92 | tag = f"v{version} m{mask} {text[:28]!r}" | ||
| 93 | if len(rows) != len(srows): | ||
| 94 | print(f"FAIL {tag}: rust {len(rows)}x vs expected {len(srows)}x") | ||
| 95 | return False | ||
| 96 | for y, (r, s) in enumerate(zip(rows, srows)): | ||
| 97 | if r != s: | ||
| 98 | print(f"FAIL {tag}: row {y} differs (rust v{mask})") | ||
| 99 | for dy in range(max(0, y - 2), min(len(rows), y + 3)): | ||
| 100 | print(f" rust row{dy}: {rows[dy]}") | ||
| 101 | print(f" exp row{dy}: {srows[dy]}") | ||
| 102 | return False | ||
| 103 | print(f"OK {tag} ({len(rows)}x{len(rows)}, {hdr})") | ||
| 104 | return True | ||
| 105 | |||
| 106 | |||
| 107 | def segno_best_mask(version, my_cw): | ||
| 108 | """Mask segno's penalty scoring would choose for the kernel stream.""" | ||
| 109 | n = E.calc_matrix_size(version) | ||
| 110 | bits = _to_bits(my_cw) | ||
| 111 | scores = [] | ||
| 112 | for mask in range(8): | ||
| 113 | m = E.make_matrix(n, n) | ||
| 114 | E.add_finder_patterns(m, n, n) | ||
| 115 | E.add_alignment_patterns(m, n, n) | ||
| 116 | E.add_codewords(m, bits, version) | ||
| 117 | fm = E.make_matrix(n, n) | ||
| 118 | E.add_finder_patterns(fm, n, n) | ||
| 119 | E.add_alignment_patterns(fm, n, n) | ||
| 120 | fm[-8][8] = 0x1 | ||
| 121 | E.apply_mask(m, E.get_data_mask_functions(False)[mask], n, n, | ||
| 122 | lambda i, j: fm[i][j] > 0x1) | ||
| 123 | scores.append((sum(E.mask_scores(m, n, n)), mask)) | ||
| 124 | return min(scores) # (score, mask); first-lowest wins like segno | ||
| 125 | |||
| 126 | |||
| 127 | def _parse_hdr(hdr): | ||
| 128 | try: | ||
| 129 | v = int(hdr.split()[0][1:]) | ||
| 130 | m = int(hdr.split()[1][1:]) | ||
| 131 | except (ValueError, IndexError) as exc: | ||
| 132 | print(f"bad header {hdr!r}: {exc}") | ||
| 133 | sys.exit(1) | ||
| 134 | return v, m | ||
| 135 | |||
| 136 | |||
| 137 | def compare_auto(text, version=None): | ||
| 138 | args = [BIN, text] | ||
| 139 | if version: | ||
| 140 | args.append(str(version)) | ||
| 141 | lines = run(args).splitlines() | ||
| 142 | hdr = lines[0] # "v<N> m<M>" | ||
| 143 | rows = lines[1:] | ||
| 144 | v, m = _parse_hdr(hdr) | ||
| 145 | my_cw = rust_raw(text, v) | ||
| 146 | score, s_mask = segno_best_mask(v, my_cw) | ||
| 147 | tag = f"v{v} auto-mask {text[:28]!r}" | ||
| 148 | if m != s_mask: | ||
| 149 | print(f"FAIL {tag}: rust chose m{m}, segno penalty chose m{s_mask} " | ||
| 150 | f"(score {score})") | ||
| 151 | return False | ||
| 152 | # Full final-matrix check with the chosen mask. | ||
| 153 | exp = build_matrix(v, my_cw, m, with_info=True) | ||
| 154 | srows = render(exp) | ||
| 155 | if len(rows) != len(srows): | ||
| 156 | print(f"FAIL {tag}: size mismatch") | ||
| 157 | return False | ||
| 158 | for y, (r, s) in enumerate(zip(rows, srows)): | ||
| 159 | if r != s: | ||
| 160 | print(f"FAIL {tag}: row {y} differs") | ||
| 161 | for dy in range(max(0, y - 2), min(len(rows), y + 3)): | ||
| 162 | print(f" rust row{dy}: {rows[dy]}") | ||
| 163 | print(f" exp row{dy}: {srows[dy]}") | ||
| 164 | return False | ||
| 165 | print(f"OK {tag} (m{m} matches segno, {len(rows)}x{len(rows)})") | ||
| 166 | return True | ||
| 167 | |||
| 168 | |||
| 169 | def main(): | ||
| 170 | ok = True | ||
| 171 | texts = { | ||
| 172 | 1: "A", | ||
| 173 | 2: "hello", | ||
| 174 | 3: "the quick brown fox", | ||
| 175 | 4: "0123456789ABCDEF", | ||
| 176 | 5: "byte mode QR encoder cross-check", | ||
| 177 | 6: "x" * 100, | ||
| 178 | 7: "hello world, this is a slightly longer message for v7", | ||
| 179 | 10: "x" * 250, | ||
| 180 | 14: "x" * 400, | ||
| 181 | 20: "x" * 800, | ||
| 182 | 25: "x" * 1200, | ||
| 183 | 40: "x" * 2900, | ||
| 184 | } | ||
| 185 | for v in (1, 2, 3, 4, 5, 6, 7, 10, 14, 20, 25, 40): | ||
| 186 | for m in range(8): | ||
| 187 | ok &= compare_forced(texts[v], v, m) | ||
| 188 | crash = ( | ||
| 189 | "!!! heap corruption in dealloc\n" | ||
| 190 | "ptr=0x404443e0 block=0x1980000 heap=0x1980000 rsp=0x1fffe0\n" | ||
| 191 | "mem: x=0x41414141 x=0x41414141 x=0x41414141 x=0x41414141\n" | ||
| 192 | "mem: x=0x41414141 x=0x41414141 x=0x41414141 x=0x41414141\n" | ||
| 193 | "stk: x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0\n" | ||
| 194 | "hdr: b=0x1980000 s=0x20 f=0 b16=0x1980000 s16=0x20 f16=0\n" | ||
| 195 | "RSDOS fault: #GP at rip=0x12345678 cr2=0x0 task=main\n" | ||
| 196 | ) | ||
| 197 | ok &= compare_auto(crash) | ||
| 198 | ok &= compare_auto("hello world", 1) | ||
| 199 | ok &= compare_auto("byte mode QR encoder cross-check", 5) | ||
| 200 | ok &= compare_auto("hello world, this is a slightly longer message for v7", 7) | ||
| 201 | ok &= compare_auto("x" * 400, 14) | ||
| 202 | ok &= compare_auto("x" * 2900, 40) | ||
| 203 | print("ALL OK" if ok else "FAILURES") | ||
| 204 | sys.exit(0 if ok else 1) | ||
| 205 | |||
| 206 | |||
| 207 | if __name__ == "__main__": | ||
| 208 | main() | ||
diff --git a/tools/qrcheck/.gitignore b/tools/qrcheck/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/tools/qrcheck/.gitignore | |||
| @@ -0,0 +1 @@ | |||
| /target | |||
diff --git a/tools/qrcheck/Cargo.lock b/tools/qrcheck/Cargo.lock new file mode 100644 index 0000000..141dd97 --- /dev/null +++ b/tools/qrcheck/Cargo.lock | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | # This file is automatically @generated by Cargo. | ||
| 2 | # It is not intended for manual editing. | ||
| 3 | version = 4 | ||
| 4 | |||
| 5 | [[package]] | ||
| 6 | name = "qrcheck" | ||
| 7 | version = "0.1.0" | ||
diff --git a/tools/qrcheck/Cargo.toml b/tools/qrcheck/Cargo.toml new file mode 100644 index 0000000..4d4487c --- /dev/null +++ b/tools/qrcheck/Cargo.toml | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | [package] | ||
| 2 | name = "qrcheck" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2024" | ||
| 5 | |||
| 6 | [[bin]] | ||
| 7 | name = "qrcheck" | ||
| 8 | path = "src/main.rs" | ||
| 9 | |||
| 10 | [profile.release] | ||
| 11 | panic = "abort" | ||
| 12 | |||
| 13 | [workspace] | ||
diff --git a/tools/qrcheck/src/main.rs b/tools/qrcheck/src/main.rs new file mode 100644 index 0000000..d294485 --- /dev/null +++ b/tools/qrcheck/src/main.rs | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | //! Host-side cross-check tool for the kernel QR encoder. | ||
| 2 | //! | ||
| 3 | //! Usage: `qrcheck TEXT [VERSION] [MASK]` | ||
| 4 | //! - with VERSION+MASK: force both and dump the matrix (compared against | ||
| 5 | //! segno by `tools/qrcheck.py`); | ||
| 6 | //! - with neither: auto-pick version and best mask, print `v<ver> m<mask>`. | ||
| 7 | //! | ||
| 8 | //! Output: first line `v<N> m<N|999>`, then one line per matrix row of | ||
| 9 | //! `#` (dark) / `.` (light). | ||
| 10 | #![allow(static_mut_refs)] | ||
| 11 | |||
| 12 | #[path = "../../../rsnix/src/qr.rs"] | ||
| 13 | mod qr; | ||
| 14 | |||
| 15 | fn main() { | ||
| 16 | let args: Vec<String> = std::env::args().collect(); | ||
| 17 | if args.get(1).map(String::as_str) == Some("--raw") { | ||
| 18 | let text = args.get(2).map(String::as_str).unwrap_or("hello"); | ||
| 19 | let version: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| 20 | let v = if version > 0 { | ||
| 21 | qr::encode_at(text.as_bytes(), version, Some(0)) | ||
| 22 | } else { | ||
| 23 | qr::encode(text.as_bytes()) | ||
| 24 | }; | ||
| 25 | if v == 0 { | ||
| 26 | eprintln!("FAIL"); | ||
| 27 | std::process::exit(1); | ||
| 28 | } | ||
| 29 | for b in qr::codeword_stream() { | ||
| 30 | print!("{:02x}", b); | ||
| 31 | } | ||
| 32 | println!(); | ||
| 33 | return; | ||
| 34 | } | ||
| 35 | let text = args.get(1).map(String::as_str).unwrap_or("hello world"); | ||
| 36 | let version: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| 37 | let mask: Option<usize> = args.get(3).and_then(|s| s.parse().ok()); | ||
| 38 | |||
| 39 | let v = if version > 0 { | ||
| 40 | qr::encode_at(text.as_bytes(), version, mask) | ||
| 41 | } else { | ||
| 42 | qr::encode(text.as_bytes()) | ||
| 43 | }; | ||
| 44 | if v == 0 { | ||
| 45 | eprintln!("FAIL: cannot encode {} bytes at v{}", text.len(), version); | ||
| 46 | std::process::exit(1); | ||
| 47 | } | ||
| 48 | let size = qr::size(); | ||
| 49 | let m = if let Some(m) = mask { m } else { qr::last_mask() }; | ||
| 50 | println!("v{} m{}", v, m); | ||
| 51 | for y in 0..size { | ||
| 52 | let mut row = String::with_capacity(size); | ||
| 53 | for x in 0..size { | ||
| 54 | row.push(if qr::module(x, y) { '#' } else { '.' }); | ||
| 55 | } | ||
| 56 | println!("{}", row); | ||
| 57 | } | ||
| 58 | } | ||
