diff --git a/ISSUES.md b/ISSUES.md index 4e24dd1..6f54240 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -8,11 +8,17 @@ Opening `/Test/example.txt` through QEMU `vvfat` could corrupt coroutine/guest s `ssi-sd` consumed a write-data token while leaving response mode, then parsed payload bytes as commands. The adapter now enters token mode immediately after the final R1 byte. SPI CMD13 also uses the upstream-correct `sd_r1` internal response so status polling no longer loops on `Unexpected response to cmd 13`. -## Reader page can render completely black +## Worked around: WASM reader panic in uzlib font decompression -Opening a generated crash report can appear to work—the Back button returns normally and the file is marked as read—but the reader page is entirely black. +Opening a book panicked **only in WASM** (`Invalid read ... reason: rejected` → Load access fault) inside `uzlib_uncompress` during the reader's font `prewarmCache`. Native QEMU from the same submodule decompresses the identical static flash font and renders text, so it is a wasm32 TCG backend miscompile, not firmware/SD. -This is likely an e-ink model issue where an intermediate all-black waveform phase is exposed instead of the completed frame. It is separate from the SD pointer panic. +Worked around by forcing store-containing TBs to TCI (`wasm_tci_only_tb = true` in `tcg_out_qemu_st`). Reader now opens and paints in the browser. This is a workaround with a perf ceiling (store-containing TBs run interpreted); the precise miscompiled op is not yet localized. Full investigation and next steps: `docs/wasm-inflate-panic.md`. + +## Reader inverts to muddled grayscale after the initial page + +With the panic worked around, the reader first paints a correct BW page (black text on white), then a follow-up refresh inverts it into muddled white-on-black text. This reproduces in the browser and is the current top reader defect. + +The reader uses the X3 community **4-level grayscale** path (DTM1=LSB, DTM2=MSB, grayscale LUT), whose plane polarity differs from the BW menu mode. The `xteink_x3_eink` model ignores LUT/grayscale commands and renders only DTM2 as `bit=white`, so the grayscale pass comes out inverted/garbled. Fixing this means tracking grayscale mode and rendering DTM1+DTM2 as 2-bit levels. Iterable on native (fast, no wasm rebuild). ## Cold boot requires a manual power-button hold diff --git a/docs/wasm-inflate-panic.md b/docs/wasm-inflate-panic.md new file mode 100644 index 0000000..00169ed --- /dev/null +++ b/docs/wasm-inflate-panic.md @@ -0,0 +1,120 @@ +# WASM-only reader panic in uzlib inflate + +Living record of the investigation into the browser-only crash when opening a +book. Native QEMU (same submodule) does **not** crash, so this is a wasm32 TCG +backend bug. Update as things are ruled in/out. + +## Symptom + +Opening a book in the browser panics; native opens the same book fine. + +```text +Invalid read at addr 0x3FCB7999, size 1, region '(null)' size 0x0 owner '(none)' ram=0, reason: rejected +Guru Meditation Error: Core 0 panic'ed (Load access fault). Exception was unhandled. +MEPC : 0x4201e210 RA : 0x4201e18c SP : 0x3fcb3810 +``` + +## Established facts + +- **Fault site (via same-version local ELF addr2line, not byte-identical):** + `uzlib_uncompress` (tinflate.c:613) ← `tinf_inflate_block_data` (tinflate.c:476) + ← `InflateReader::read` ← `FontDecompressor::decompressGroup` ← `prewarmCache`. + "Indexing" = the reader inflating compressed glyph groups from flash into a + `malloc`'d temp buffer. +- **Input is static flash data**, independent of SD. The compressed font bank is + in the firmware image; the SD file only selects *which* glyph groups get + prewarmed (the characters present in the text). +- **Native renders the reader** ("chapter one") with no fault — proven with + `_scratch/native-reader-repro.py` + QMP screendumps. Confirms the firmware, + the font data, and the decompression logic are all correct. +- **Fault address is non-deterministic:** `0x3FCB7999` and `0x3FCB7997` across + runs. A static machine-map boundary would fault at a fixed address. Drift ⇒ a + computed pointer is slightly wrong, i.e. runtime/codegen, not the memory map. +- **Rejected region is bogus:** `region '(null)' size 0x0 owner '(none)' ram=0`. + The address lands in mapped DRAM (`0x3fc80000`+`0x60000`) but the wasm softmmu + TLB routed it to the IO slow path with a zero-size section. Hallmark of a bad + address/TLB result from a JIT-compiled TB. +- **Menus work.** Home/file-browser render crisp text through the same font + system and the same TLB fast path, so the TLB codegen is not broadly broken — + the bug is pattern/data specific in a hot, JIT-promoted TB. +- **User observation:** firmware-written crash logs open fine; prepopulated + files do not. Consistent with data-dependence — a crash log's character set + prewarms different glyph groups than lorem-ipsum, avoiding the group whose + compressed byte pattern trips the miscompiled path. + +## Architecture notes (why JIT is implicated) + +- `tcg/wasm32.c`: TBs run on the TCI interpreter until executed + `INSTANTIATE_NUM` times, then get compiled to native WASM (`instantiate_wasm`) + and run as added table functions. The inflate hot loop gets promoted. +- `WASM_TCI_ONLY_ENTRY` forces a TB to stay on TCI forever; + `wasm_tci_only_tb` (per-TB, reset each translation) requests that. +- Dynamic promotion must stay on for usable performance (all-TCI starves the + guest watchdog), so a permanent fix must correct codegen, not disable JIT. + +## Hypotheses + +| # | Hypothesis | Status | +|---|-----------|--------| +| H1 | A store-containing TB is miscompiled (the cleanup removed `wasm_tci_only_tb = true` from `tcg_out_qemu_st`); the compiled TB corrupts a pointer a later load dereferences | **confirmed** (E1) | +| H2 | Compiled **load** codegen bug (`tcg_wasm_out_qemu_ld` / `_direct`) | untested | +| H3 | TLB fast-path codegen edge case (`tcg_wasm_out_tlb_load`) hit only by certain access patterns | untested | +| H4 | asyncify/fiber unwind corrupts vCPU state mid-inflate | unlikely (inflate makes no async import calls) | +| H5 | mimalloc / linear-memory / threads issue | unlikely (native heap layout differs, no crash) | + +## Experiments + +### E1 — force store-containing TBs to TCI (tests H1) ✅ CONFIRMED +Re-added `wasm_tci_only_tb = true;` to `tcg_out_qemu_st` in +`tcg/wasm32/tcg-target.c.inc`. + +- Result: **panic gone.** `make browser-test` reaches the reader and renders a + correct page (black lorem-ipsum on white, footer `80% example 1/3 33%`). No + reject, no `Guru Meditation`, 1 boot, 0 watchdog panics. New refresh frames + (`dtm2_white=0.891`) appear past the old `0.960` stall. +- **Conclusion: the compiled `qemu_st` codegen is the bug.** Forcing store TBs + to TCI is a working *workaround*, not the fix — every store-containing TB + then runs interpreted (broad perf cost; prior attempts reported watchdog + starvation under sustained load, though this finite-burst run was clean). +- The reader first paints a correct BW page (black text on white), then a + follow-up **grayscale** refresh pass inverts it into muddled white-on-black + text. That second issue is real and still open (see ISSUES.md "Reader renders + inverted grayscale"); it is independent of this panic fix. The `browser-test` + wait-for-stable happened to screenshot the initial white page. + +### Caveat on E1's specificity +Forcing store-*containing* TBs to TCI drops the whole TB (its loads, stores, +and arithmetic) to the interpreter. So the bug is *in a TB that also has a +store*, not provably the store op. `tcg_wasm_out_qemu_st_direct` and +`tcg_wasm_out_qemu_ld_direct` are byte-for-byte symmetric and look correct in +isolation, which points away from the direct emit and toward the store path's +surrounding block/rewind/helper scaffolding in `tcg_wasm_out_qemu_st`, or an +interaction with a co-located op. + +### Next options +1. **Ship E1 as a guarded workaround** — reader works now; measure real + navigation/page-turn perf before judging it unacceptable. +2. **Localize precisely** — bisect by making the TCI trigger conditional + (store width; presence of the miss/helper path; TB size) to shrink the + suspect set, then read the generated wasm for that TB. + +## Diagnostics currently in the tree (uncommitted) + +- `system/memory.c`: reject log prints region size/owner/ram to identify the + faulting region. +- `hw/display/xteink_x3_eink.c`: `[eink] refresh` popcount trace (per-plane + white ratio) — also used for the separate grayscale-rendering issue. + +## Reproduction + +- **WASM:** `make browser-test` (the test does 4 confirm presses: Browse Files → + Test → highlight → Open). Panic appears in `_scratch/browser-raw-sd.log`. +- **Native (control, no panic):** `nix develop -c python3 _scratch/native-reader-repro.py`, + then inspect `_scratch/native-step4-reader.ppm`. + +## Ruled out + +- Not SD / filesystem: fixed separately (raw FAT32, SSI-SD token, CMD13); the + file reads correctly and "Indexing" begins. +- Not firmware / font data: native decompresses and renders identical input. +- Not the machine memory map: fault address drifts; native map is the same. diff --git a/tests/browser_raw_sd.py b/tests/browser_raw_sd.py index 669d351..d8d3182 100644 --- a/tests/browser_raw_sd.py +++ b/tests/browser_raw_sd.py @@ -56,9 +56,10 @@ try: wait_stable() press(confirm) # Browse Files - press(confirm) # Test - press(confirm) # example.txt - time.sleep(60) + press(confirm) # Test directory + press(confirm) # highlight example.txt (browser indexes a preview) + press(confirm) # Open example.txt into the reader + wait_stable(quiet=10, timeout=600) status = driver.find_element(By.ID, 'status').text log = driver.find_element(By.ID, 'log').text diff --git a/third_party/qemu b/third_party/qemu index d2bb76e..1f76f73 160000 --- a/third_party/qemu +++ b/third_party/qemu @@ -1 +1 @@ -Subproject commit d2bb76e67255b9a3e377a955f153cf12881f44c2 +Subproject commit 1f76f73feef6dc0ecd956ac19e25cde2f7b905ba