# murphy-m4-simulator **Repository Path**: iwinyeah/murphy-m4-simulator ## Basic Information - **Project Name**: murphy-m4-simulator - **Description**: 导自einklover/murphy-m4-simulator - **Primary Language**: C++ - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-08-16 - **Last Updated**: 2026-08-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # M4Sim — deterministic Murphy M4 simulator A hardware-behavior simulator for the Murphy M4 (ESP32-S3 N16R8 + SSD1677 800×480 e-ink). It does **not** pretend to "look like" the M4 — it models the exact behaviors that caused real production bugs, deterministically, so the firmware development loop becomes: ``` edit → m4_sim test → PASS (normal/low-mem/fragmented/slow-SD/rapid-tap) → flash once ``` ## What it models | Layer | File | Models | |---|---|---| | Virtual time + scheduler | `core/SimKernel.h` | deterministic events; same-time ordering keyed by `--seed`; event-driven safety hook | | Unified event trace | `core/SimKernel.h` | INPUT/PAGE_TARGET/FRAME_RENDERED/EPD_*/ALLOC/OOM/TLS_*/SD_READ/... + replay hash | | Temporal assertions | `core/SimKernel.h` | `eventually` / `never` (event-driven, per scheduler event) / `after` | | Page-turn FSM | `core/PageTurnCoordinator.h` | **shared production state machine** — the real TxtReaderActivity runs the same code | | Heap | `memory/SimHeap.h` | REAL first-fit allocator over DRAM/PSRAM pools → real fragmentation, real OOM; self-consistency checked | | Memory contracts | `memory/SimHeap.h` | framebuffer/TTF-cmap/page-buffer MUST be PSRAM → CONTRACT_VIOLATION (incl. maxBytes) | | E-ink panel | `hardware/SimPanel.h` | renderFB / pending / physical + BUSY durations + frame provenance + abort refresh-token | | SD | `hardware/SimStorage.h` | per-op latency, slow-SD multiplier, short-read fault | | Network/TLS | `network/SimNetwork.h` | streaming chunked decoder (arbitrary TCP fragments), TLS handshake-peak vs resident RAM | | Reader adapter | `model/ReaderModel.h` | thin adapter: wires PageTurnCoordinator to SimPanel/SimStorage/scheduler | ## The five hard invariants 1. **Wrong RAM placement fails automatically** — memory contracts reject framebuffer-in-internal even when memory is "available". 2. **Fragmentation produces real OOM** — free total OK but largest block < request → `nullptr` (the historical TLS OOM). 3. **Every valid page-turn intent eventually commits** — `eventually(physical == target)`, unless superseded by a later intent. 4. **Firmware's physical page always equals the panel's committed frame** — `never(firmwarePhysical != panelPhysical)`, checked **after every scheduler event** (a transient divergence fails even if it self-heals before the next poll). 5. **Every failure carries a timeline + seed + replay hash** — run with `-v` to dump the full event timeline for any scenario. ## Scenarios (regression suite) ``` single_tap_slow_index PASS ONE intent to an un-indexed page (INPUT==1) → lands bug_lost_tap FAIL★ tap during BUSY + no catch-up → intent lost (caught) rapid_tap_correct PASS three taps mid-animation → lands on final target bug_divergence FAIL★ live-currentPage physical record → divergence (caught) tls_fragmented_heap PASS largest internal < 48KB peak → handshake must OOM tls_keepalive PASS connection reuse pays only resident, not handshake peak framebuffer_contract PASS malloc(48KB) framebuffer → CONTRACT_VIOLATION chunked_quiet_eof PASS chunk gap >700ms + byte/random TCP segmentation slow_sd_tap PASS tap during slow SD READ (panel still idle) → intent survives heap_stress PASS random alloc/free churn: list integrity + full coalesce ``` ★ = regression caught: the scenario flips a bug knob, the temporal assertion fires, and that failing is the PASS. These prove the simulator can detect the bug class before the firmware ever gets flashed. ## Determinism vs schedule fuzz (distinct) ``` # deterministic replay: same seed → identical full-timeline hash (run twice, # hash must match). Stronger than "two seeds give the same failure vector". ./build/m4_simulator # schedule fuzz: many seeds may produce DIFFERENT timelines, but every # invariant must hold in all of them. ./build/m4_simulator --seeds 1:1000 ``` ## Usage The simulator is dependency-free (C++17, standard library only) and builds with CMake on macOS / Linux / Windows (MSVC): ```bash cmake -S . -B build cmake --build build -j ./build/m4_simulator # all scenarios + replay hash + seed-2 fuzz ./build/m4_simulator --list # list scenarios ./build/m4_simulator --seed 1 bug_divergence -v # one scenario + full timeline ./build/m4_simulator --seeds 1:200 # schedule fuzz # ASan/UBSan (UBSan works on stock toolchains; ASan may need a matching runtime) cmake -S . -B build-asan -DM4SIM_SANITIZE=ON ``` On macOS with a broken Command Line Tools libc++, pass `-DCMAKE_CXX_COMPILER= -DCMAKE_OSX_SYSROOT=`. ## Recent review-driven fixes **v0.2** (issue #1 review): - Shared state machine: page-turn FSM extracted to `PageTurnCoordinator.h`. - Event-driven `never`: safety invariants run after every scheduler event. - Heap allocator hardened: split/unlink/coalesce free-list bugs fixed; stats recomputed from the free list; `liveAllocs_` removes records on free; `maxBytes` enforced; `heap_stress` self-check. - Scenarios wait for real state (panel idle / EPD busy / SD read in flight); `single_tap_slow_index` targets an un-indexed page with INPUT count == 1. - TLS model split: handshake-peak (transient 48KB) vs resident (12KB held). - Chunked decoder is a real streaming state machine (arbitrary TCP fragments). - Deterministic replay = timeline hash; schedule fuzz is a separate `--seeds`. - abortRefresh() token: aborted refresh never fires the commit callback. **v0.3** (issue #2 review): - **SimTls contradiction fixed**: resident-alloc failure now makes `open()` return false (was: `open()==true` with `isOpen()==false`). - **ESP-IDF capability semantics**: region eligible iff `(region.caps & requested) == requested` (whole set, not "any of"); region caps include 8BIT/DMA so `SPIRAM|8BIT` contracts work. - **Multiple internal banks** (256K/128K/128K + PSRAM): free blocks in different banks never coalesce — `freeInternal()` (sum) vs `largestInternal()` (max over banks) now differ, the real fragmentation OOM class. OOM report shows per-bank free/largest. - **realloc** added (grow-in-place via successor absorb/split, else move); fuzz found and fixed a rest-header underflow bug in the split path. - **double free → ASSERT FAIL** (ASSERT event + counter), not tolerated. - **heap_stress expanded**: realloc grow/shrink/move, exact-boundary alloc, calloc overflow refusal, fail-Nth injection, baseline-after churn. - **CLI order fixed**: `--seeds 1:200 ` now fuzzes the named scenario. - **PageTurnCoordinator** moved to neutral `m4reader` namespace; bug knobs are a `TestPolicy` (production default = no bugs). ## Notes / next phases - Boot heap baseline is synthesized ("free ~162KB, largest ~46KB"); calibrate from a real device (`m4adb` heap snapshot → `profiles/murphy_m4.json`). - EPD timings in `EpdProfile` mirror observed FAST/HALF/FULL durations; tune from device logs. - **Highest-value next step**: wire the real `TxtReaderActivity` to `PageTurnCoordinator` (the FSM is already extracted; the firmware side of the wiring is the remaining work). Then a full native compile is a later bonus. - QEMU (ESP32-S3 smoke) is a later layer; this core is what QEMU builds on.