--- name: schunix-qr-encoder description: "kernel/core/qr.c — the crash-report QR encoder, how it's verified, and the segno padding bug that is NOT ours." metadata: node_type: memory type: project originSessionId: 9d8a2aef-bcf0-47a0-96c6-f706efab4a95 modified: 2026-08-08T16:22:01.468Z --- Built 2026-08-08. `kernel/core/qr.c` + `include/qr.h`, the Phase 8 crash channel (see [[schunix-crash-screen]] and `docs/plan/roadmap.md`). **Wired into the crash screen the same day.** `panic.c` builds a compact payload and paints a white card on the right of PSOGM. Verified end-to-end with `make crashshot && make crashqr`, which decodes the symbol out of the captured framebuffer — not out of the encoder. Payload format `SXPSOGM1`: magic, reason, `c p t `, `b`, then call-chain frames as **offsets from the kernel base** (8 hex digits, not 16 — that is what makes a useful number of frames fit in 134 bytes). The reason is written against a buffer end held back by 34 bytes, so a long message costs frames but never all of them. Deliberately not a URL: it encodes the report itself and reaches nothing. **Scope, deliberately narrow:** byte mode only, EC level L only, versions 1–6 only (max 134 bytes). Version 7 is where QR starts needing an 18-bit version-information block in two extra places; version 10 is where the character count grows to 16 bits and the alignment grid becomes 2-D. Stopping at 6 avoids both. If a payload ever needs more, shorten the report rather than add the tables. Freestanding: no allocation, no libc beyond memset/memmove, GF(256) multiply by shift-and-add so there is no table init to depend on — all of which matters because the caller is a panic handler. **How it is verified — this is the important part.** A wrong QR still looks exactly like a QR, so "it renders" proves nothing. - `make qrtest` — self-checks (table arithmetic, RS generator polynomials, finder/timing placement) and writes `build/qr.png` plus a terminal rendering a phone can read straight off the screen. `-q` suppresses the ANSI art, `-m TEXT` dumps the module matrix, `-f TEXT` dumps the function-pattern map. - `make qrcheck` (`tools/qrcheck.py`) — the real oracle. Decodes with OpenCV, *and* compares module-for-module against segno. Needs `pip install --target opencv-python-headless numpy segno` and PYTHONPATH; not wired into `make`. **Two traps found doing this, both worth remembering:** 1. **segno adds a spurious zero pad codeword to every byte-mode symbol.** Its `write_padding_bits()` does `extend([0] * (8 - (length % 8)))`, which emits a whole byte when the stream is already codeword-aligned — and in byte mode it always is (8L+16 bits after the terminator). ISO/IEC 18004 makes it conditional and segno quotes that exact sentence above the line. Harmless (decoders read `count` bytes and ignore padding) but it means the two encoders **cannot** agree bit-for-bit on any padded payload. qrcheck compares full matrices only for exact-fill payloads and decodes the rest. Our behaviour is correct; do not "fix" it to match. 2. **OpenCV's QRCodeDetector fails on perfectly valid symbols**, depending on scale and quiet-zone width. qrcheck tries six renderings, and if one still fails it re-tests segno's symbol for the same payload — if that fails too it reports `[detector]`, not `[FAIL]`. Without that step the check produces false failures, which is worse than not running it. **Debugging technique that cracked it:** dump the module matrix *and* the function map, run the zigzag backwards in Python to recover the codewords from both encoders, and diff those. "First differing codeword index 18" localised it instantly; comparing matrices only ever said "108 of 441 modules differ". Note the mask must be read from the format bits and un-applied first — un-masking with the wrong mask makes your own output look like noise, which cost a round. Windows note: `subprocess` cannot launch a relative path with forward slashes; `os.path.abspath` it.