Reader opens in WASM: bump QEMU store-TB workaround; document panic

Bumps third_party/qemu to the wasm32 store-TB TCI workaround so the
browser reader no longer panics in uzlib font decompression. Adds
docs/wasm-inflate-panic.md with the full investigation (native control,
non-deterministic reject, JIT promotion) and next steps. Updates ISSUES:
the panic is worked around; the reader now inverts into muddled
grayscale after the initial BW page (new top reader defect). tests
navigate the 4th Open press into the reader.
This commit is contained in:
2026-07-21 09:16:40 -04:00
parent b959776068
commit 376d302e1d
4 changed files with 134 additions and 7 deletions
+9 -3
View File
@@ -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
+120
View File
@@ -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.
+4 -3
View File
@@ -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