diff --git a/CMakeLists.txt b/CMakeLists.txt index c7cda635..b08bc3fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ add_library(mm_core STATIC src/core/Scheduler.cpp src/core/moonlive/MoonLive.cpp src/core/moonlive/MoonLiveCompiler.cpp + src/core/moonlive/MoonLiveSpill.cpp ) target_include_directories(mm_core PUBLIC src/) target_link_libraries(mm_core PUBLIC mm_platform) diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index 624ae916..9e34049e 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -20,6 +20,23 @@ projectMM ships **no migration code**: the persistence layer is robust by defaul ## Unreleased (`next-iteration`) +### MoonLive scripts move to the filesystem (2026-08-11) + +A scripted module used to carry its script as a `source` textarea — a fixed 1 KB array per module, plus a second 1 KB copy to notice edits, **resident whether or not a script was loaded**. Six modules cost 13 KB of a classic ESP32's 320 KB for text that was mostly empty. The script now lives in a file under `/moonlive/`, and the module holds only its **name** (~32 bytes): it is read into a right-sized buffer to compile and freed immediately, so nothing script-sized stays in RAM. A script is bounded by the filesystem instead of by a 1 KB array. + +**Action: *re-add a module* — or, to keep your scripts, *update a file* first.** + +The `source` control no longer exists, so a persisted `"source"` value is an unknown key and is ignored (the robust-reader rule). A MoonLive module therefore boots with **no script**, reporting `no script — set the script name`, and renders nothing until one is named. + +| What | Why | What to do | +|---|---|---| +| Your script text | It was persisted under `source`, a control that is gone | **Copy it out before updating** — it is in `/.config/Layouts.json` (or `Effects.json`) as `"N.source"`. Save it as `/moonlive/.mlv` via the File Manager, then set the module's `script` control to `.mlv` | +| The module's own controls | A script's `@control` sliders exist only once it has compiled, so they are absent until a script is named | Nothing — they reappear with the script, keeping their persisted values | + +`/moonlive/` is created on demand: naming a script is enough to make the folder appear, so a fresh device needs no setup. + +**Editing today** goes through the File Manager rather than the module's own card. Wiring the card's editor to the same file is a separate change. + ### MoonLive: a script can no longer declare a name the engine supplies (2026-08-10) `t` (elapsed milliseconds), `width`/`height`/`depth` (the logical grid) and `x`/`y`/`z` (the light a modifier is transforming) are now **system variables** the engine supplies, so a script cannot declare one. Previously each binding faked them by prepending hidden declarations to the script, which meant an effect could declare its own `width` and quietly disagree with the layer it was drawing into. @@ -32,7 +49,7 @@ A **layout** is the one script that legitimately used those names for its own co | What | Why | What to do | |---|---|---| -| A scripted layout declaring `width`/`height` | The name is what the layout is defining, so the declaration is a compile error and no lights are placed | Edit the script's `source` control, renaming its own controls (the shipped `grid.mlv` uses `cols`/`rows`) | +| A scripted layout declaring `width`/`height` | The name is what the layout is defining, so the declaration is a compile error and no lights are placed | Edit the `.mlv` file in the File Manager, renaming its own controls (the shipped `grid.mlv` uses `cols`/`rows`), then set the module's `script` control to that file | | A scripted **modifier** using `x`, `y` or `z` as a loop variable | A modifier IS handed a coordinate under those names, so they cannot also be counters there | Rename the loop variable to something the modifier is not handed (`i`, `n`) | Effects and modifiers need no change: they were already being handed these values, just through a preamble instead of by name. The error names the clash, and the module shows it on its card, so a broken script says why rather than failing silently. diff --git a/docs/backlog/backlog-light.md b/docs/backlog/backlog-light.md index 8ca1324e..16896faf 100644 --- a/docs/backlog/backlog-light.md +++ b/docs/backlog/backlog-light.md @@ -290,6 +290,36 @@ The LED-driver increments **shipped**: increment 1 (RMT/WS2812B single-strand on Compiling inside `defineControls` is NOT the fix (tried): it makes the default script's controls exist before `setSource` runs, and swapping the source then re-seeds every control from its new declared default — the same value-loss, moved. The real fix is ordering: the engine must compile once the persisted `source` is in place but before controls are published, which is a Scheduler-phase question (the same parent-before-child ordering the `const_cast` in `MoonLiveLayout::compile` already works around). Affects all three MoonLive bindings, not just the layout. +- **Xtensa's vreg map violates the windowed ABI** (2026-08-13). ROOT CAUSE, found by comparing a + working ESP32-S31 (RISC-V) against a crashing S3 on identical firmware and scripts. + + `call8` rotates the register window by eight: the callee's `a0..a7` ARE the caller's `a8..a15`, so + every host call clobbers `a8..a15`. ESP-IDF states it plainly — `a8..a15 clobbered (if + window_spill8)` in `components/xtensa/include/xtensa/coreasm.h`. Our map is `a2..a11`, so `a8..a11` + hold script values inside the rotation window. Measured crash: `A0 = 0x00000100` — that is + `nLights` (256), a script value sitting in the return-address register. Intermittent, because + whether it is fatal depends on which vreg held what when the window turned; that intermittency is + what made it survive a day of plausible theories. + + Only `a0`(return address), `a1`(sp) and `a2..a7` survive a call, so the safe map is **six** vregs, + not ten. Six does not fit today: five are the fixed ABI vregs (`buf`, `nLights`, `cpl`, `t`, + `ctrls`), leaving one. Closing this needs those five OFF permanent registers and into the frame — + the register-promotion question [Plan-20260813](../history/plans/Plan-20260813%20-%20MoonLive%20on%20a%20stack%20machine%20%E2%80%94%20the%20frame%20is%20where%20values%20live.md) + defers on purpose. RISC-V is unaffected: no window, and its `call()` saves 14 registers into an + explicit 80-byte frame, which is why the S31 runs layout + plasma + modifier stably. + + Until then a scripted layout, and any effect whose script CALLS a builtin, is unreliable on Xtensa. + Straight-line and inline-only scripts (`setRGB`, `fill`) are fine — they emit no call. + +- **A scripted LAYOUT crashes on Xtensa: the emitted code is wrong** (2026-08-12). Naming any script on a `MoonLiveLayout` resets an S3 — `addLight(0, 0, 0);` alone is enough. A scripted EFFECT on the same board is fine, which is the whole clue: an effect writes pixels through `setRGB`, a layout calls `addLight`, and only the layout path faults. + + **Measured, so the earlier theories are retired.** Per-stage timings on the S3 read `stat 2.0 ms, alloc 8 us, read 2.7 ms, compile 0.5 ms` — the compile COMPLETES, in about five milliseconds, and the crash comes after it returns. So this is not a slow compile and not the task watchdog running out: tracing either side of `runScript` shows the fault lands *inside the JIT'd code*, as `Guru Meditation Error: Double exception` with a corrupted backtrace, or as `LoadProhibited` at a nonsense address. (Filesystem access IS expensive — ~90% of the load — but that is a cost, not the bug.) + + **The defect is visible in the disassembly.** `uv run moondeck/moonlive/disasm.py '