SchOS/.claude/memory/schunix-qr-encoder.md
Nexoniarz 57aff40228 docs: four directories, and a rule for when a thing leaves the roadmap
docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.

A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.

So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.

schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.

Three corrections found on the way, all load-bearing:

  * projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
    sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
  * The root README said "No TCP/IP" months after the stack landed.
  * history/kernel.md sec.7 was marked "in progress" on a problem that process
    groups solved.

Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-12 17:13:35 +02:00

71 lines
4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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<cpu> p<pid> t<ticks> <name>`, `b<kernel base>`, 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 <dir> 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.