Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,21 @@ add_library(mm_core STATIC
src/core/HttpServerModule.cpp
src/core/FilesystemModule.cpp
src/core/Scheduler.cpp
src/core/moonlive/MoonLive.cpp
src/core/moonlive/MoonLiveCompiler.cpp
)
target_include_directories(mm_core PUBLIC src/)
target_link_libraries(mm_core PUBLIC mm_platform)
Comment on lines +58 to 62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Keep compiled MoonLive core out of mm_core.

Adding MoonLive.cpp/MoonLiveCompiler.cpp to the root core target and linking mm_core to mm_platform breaks the declared build/layering contract. Move the compiled/platform-dependent lifecycle behind the platform target or refactor this surface to preserve the header-only core boundary.

As per path instructions, root CMakeLists.txt says “Core library is INTERFACE (header-only). Only platform .cpp files need compilation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CMakeLists.txt` around lines 58 - 62, The root core target is pulling
compiled MoonLive sources into mm_core and linking it to mm_platform, which
violates the header-only core boundary. Remove MoonLive.cpp and
MoonLiveCompiler.cpp from mm_core and move their platform-dependent
implementation behind mm_platform (or another platform target), keeping mm_core
as an INTERFACE-only surface. Use the mm_core target setup and the MoonLive
symbols to locate the affected build wiring and preserve the declared layering
contract.

Source: Path instructions

# `add_dependencies(mm_core ui_embed)` is below, after the ui_embed target is defined.

# Platform library (desktop)
add_library(mm_platform src/platform/desktop/platform_desktop.cpp)
# Platform library (desktop). moonlive_emit.cpp is the desktop MoonLive backend (host-ISA
# codegen) — it lives here because emitted machine code is platform/ISA-specific.
add_library(mm_platform
src/platform/desktop/platform_desktop.cpp
src/platform/desktop/moonlive_emit.cpp
src/platform/desktop/moonlive_asm_host.cpp
src/platform/desktop/moonlive_lower_host.cpp
)
target_include_directories(mm_platform PUBLIC src/ src/platform/desktop/)
# Winsock for the desktop socket surface on Windows.
target_link_libraries(mm_platform PUBLIC $<$<PLATFORM_ID:Windows>:ws2_32>)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Drive large LED installations and DMX lighting from ESP32, Teensy, Raspberry Pi, Windows, macOS or Linux desktop. One source tree, multiple targets.

![Web UI](docs/assets/screenshots/ui_overview.png)
![Web UI](docs/assets/screenshots/ui_theme.gif)

👉 **Try it now:** flash an ESP32 straight from your browser → <https://moonmodules.org/projectMM/install/> — step-by-step in the [Getting started guide](docs/gettingstarted.md).

Expand Down
28 changes: 21 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Coding conventions live in [coding-standards.md](coding-standards.md); how to bu
- [Effects](#effects)
- [Dimensionality](#dimensionality)
- [Robustness rules](#robustness-rules)
- [MoonLive: the live-script engine](#moonlive-the-live-script-engine)
- [Modifiers](#modifiers)
- [Mapping and blending](#mapping-and-blending)
- [Drivers](#drivers)
Expand Down Expand Up @@ -210,6 +211,7 @@ Only abstract what you actually need. Currently:

- **Time**: `millis()`, `micros()`. Monotonic, microsecond resolution. (`esp_timer` / `std::chrono`)
- **Memory**: `alloc(size)`, `free(ptr)`. Prefers PSRAM on ESP32, falls back to regular heap. `freeHeap()`, `maxAllocBlock()` for diagnostics. (`heap_caps_malloc` / `std::malloc`)
- **Executable memory**: `allocExec(size)` / `freeExec(ptr, size)` allocate memory the CPU can *fetch and execute* from, and `writeExec(dst, src, len)` copies emitted machine code into it safely. Used by the MoonLive live-script engine (below) to place the native code it compiles. All the W^X / instruction-cache quirks live behind these three functions: ESP32 IRAM via `MALLOC_CAP_EXEC` with 32-bit-aligned stores plus a cache sync so the core fetches fresh code; an `mmap` `PROT_EXEC` page on desktop (macOS-arm64 `MAP_JIT` + a write-protect toggle). (`heap_caps_malloc(MALLOC_CAP_EXEC)` / `mmap`)
- **Networking**: `UdpSocket` for ArtNet send. `TcpConnection` / `TcpServer` for HTTP + WebSocket; `TcpConnection::writeSome` is a non-blocking partial write (returns bytes written, 0 = would-block) so a backpressured browser can't stall the render loop. (lwIP sockets / BSD sockets)
- **Scheduling**: `yield()` (cooperative yield to OS/RTOS), `delayMs(ms)` (blocking sleep, off-path only), `delayUs(us)` (microsecond busy-wait, only for sub-millisecond hardware timing a driver owns — e.g. the WS2812 ≥300 µs inter-frame latch in `RmtLedDriver`; never for general pacing, which uses the non-blocking `millis()` gate), `reboot()`. (`vTaskDelay` / `esp_rom_delay_us` / `esp_restart` on ESP32; `std::this_thread::sleep_for` / `std::exit` on desktop)
- **Platform config**: `platform_config.h` per platform: compile-time constants like `hasPsram` and `hasWiFi`. Each platform provides its own version; `types.h` includes it without `#ifdef`. Core code branches on these via `if constexpr` (e.g. NetworkModule drops its WiFi cascade when `hasWiFi` is false), so the dead branch is removed from the binary with no `#ifdef` outside `src/platform/`.
Expand Down Expand Up @@ -353,9 +355,9 @@ A **Layer** (a MoonModule, child of Layers) owns:

A layer can have **multiple effects**. Each effect writes to the buffer sequentially in its listed order, overwriting or adding to the previous — so the effects stack (a base-colour effect followed by a sparkle effect).

A layer applies its **first enabled modifier** during LUT build (`Layer::rebuildLUT`). Modifiers are **reorderable** in the UI, and order is meaningful (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, just as mirror-then-rotate differs from rotate-then-mirror). Applying several modifiers in sequence (chaining) is on the [backlog](backlog/README.md).
A layer applies **all its enabled modifiers as a chain** during the mapping build (`Layer::rebuildLUT`): each modifier is a coordinate fold, and they compose in child order (M₁∘M₂∘…). Modifiers are **reorderable** in the UI, and order is meaningful (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, just as mirror-then-rotate differs from rotate-then-mirror). The fold contract (the three hooks, the physical→logical build, the live pass) is documented in [ModifierBase](moonmodules/light/ModifierBase.md).

Each layer references the shared Layouts. The layer builds its own LUT by iterating the Layouts container's coordinates and applying its static modifiers in order. Different layers in Layers can have different modifiers, producing different LUTs from the same Layouts.
Each layer references the shared Layouts. The layer builds its mapping by walking the Layouts container's **physical** coordinates and folding each through the static modifier chain to its logical cell — N physical lights folding onto one logical cell is the fan-out (a Multiply kaleidoscope), so the build never produces a fan-out overflow. Different layers in Layers can have different modifiers, producing different mappings from the same Layouts.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Effects

Expand Down Expand Up @@ -398,14 +400,26 @@ See NoiseEffect / MetaballsEffect for the canonical pattern. Animation speed mus

**An effect renders a pattern; it does not transform geometry.** When migrating or adding an effect, strip out anything that is really a *modifier* — mirroring, tiling, rotation, scrolling/offset, a kaleidoscope fold, masking, any remap of *where* pixels land — and add it as a separate [modifier](#modifiers) instead. WLED (and other sources we port from) routinely fold these into the effect's own loop (a "mirror" checkbox, a "2D" rotation, a built-in pinwheel), because WLED has no modifier concept; we do. Keeping them out of the effect is what lets any effect compose with any modifier (the same RotateModifier rotates Fire, Noise, or a network-received frame) instead of every effect re-implementing its own half-baked mirror. The test: an effect's `loop()` should only *write colours into the logical buffer for its own coordinates*; if it's reading or rewriting positions to move/fold/duplicate the image, that behaviour belongs in a modifier. (This is the light-domain face of *Complexity lives in core; domain modules stay simple* — geometry transforms are the modifier's job, shared once, not duplicated into every effect.)

## MoonLive: the live-script engine

MoonLive lets you author an effect (later: a layout, modifier, driver, or core rule) as **text** and run it on a running device, with no recompile-and-flash cycle. Its standout property is *how* it runs the script: not a bytecode interpreter, but a **native-codegen compiler** — source text is lexed, parsed, lowered to a typed IR, and assembled to real machine code that the render loop calls through a plain function pointer, so a scripted effect runs at near-hand-written speed in the hot path. This is the core construct; a scripted effect (`MoonLiveEffect`) is the thin binding that gives it the MoonModule lifecycle.

The engine is a **domain-neutral core** with one narrow seam, structured as three tiers so adding a CPU is additive, never a rewrite:

- **Front-end** (`src/core/moonlive/`, platform-independent): a recursive-descent lexer + parser over an expression grammar (every function argument is a literal or a nested call) that lowers each statement to a typed **IR** — a flat list of three-address ops over virtual registers. The IR is the seam: it knows *operations*, never an ISA and never a domain. It is compile-time only — consumed during lowering and discarded, so it costs nothing at run time; the CPU executes only the final native instructions.
- **Host builtin table** (the domain seam): the core owns no function names. A *host* registers `{name → descriptor}` — `setRGB`/`fill`/`random16` for LEDs (`src/light/moonlive/`), something else for a display or sensor. A descriptor is either a `Call` (a generic call to a host C function pointer — a pure helper like `random16`) or an `Inline` op (a neutral opcode tag the backend emits inline — a buffer writer, no per-pixel call). This is the [ESPLiveScript / ARTI bound-function model](backlog/livescripts-analysis-top-down.md); it is what keeps the core LED-free while the hot path stays inline. The LED *names* and the "an element is 3 RGB bytes" meaning live only in the light-domain registration and the per-ISA lowering, never in core.
- **Per-ISA backend** (`src/platform/`, behind the boundary): a tiny named-instruction MacroAssembler (the textbook V8 / LLVM / asmjit shape — append one instruction, back-patch label offsets) plus the IR→bytes lowering that drives it. Xtensa (classic ESP32 / S3), RISC-V (P4), and the host ISA (desktop arm64/x86-64) each are *a new backend file behind the unchanged IR* — the front-end and IR never branch on ISA. Emitted code goes into an `allocExec` block (see [§ Platform abstraction](#platform-abstraction)) and is called each tick.

A recompile is the normal cold-path rebuild: editing the `source` control routes through the same `onBuildState()` sweep every control change uses, so a new script swaps in live (no reboot), and a parse error surfaces in the module status while the layer renders dark — robust to any input. The full design (the staged language ladder, the safety model, the performance budget, the memory-arena plan as the language grows) lives in [docs/backlog/livescripts-analysis-top-down.md](backlog/livescripts-analysis-top-down.md); the module contract is [docs/moonmodules/light/moonlive/MoonLiveEffect.md](moonmodules/light/moonlive/MoonLiveEffect.md).

## Modifiers

A modifier (MoonModule) lives inside a layer alongside its effects. Modifiers expose a virtual interface: the Layer calls modifier methods without knowing the concrete type (no `dynamic_cast`).
A modifier (MoonModule) lives inside a layer alongside its effects. Modifiers expose a virtual interface: the Layer calls modifier methods without knowing the concrete type (no `dynamic_cast`). A layer applies **all** its enabled modifiers as a chain, in child order — each a coordinate fold composed into one mapping (see [§ Layers and Layer](#layers-and-layer)).

A modifier can:
A modifier is a coordinate transform, applied in one of two ways (the fold contract is in [ModifierBase](moonmodules/light/ModifierBase.md)):

- Transform the mapping LUT via `transformCoord()`: rebuilt on the cold path, zero render cost.
- Transform light values via `transformLights()` on the hot path: per-light cost, enables dynamic animations like rotation.
- **Static** (`modifyLogicalSize` + `modifyLogical`): folded into the mapping during the cold-path build, so it costs nothing per frame (Region crop, Multiply tile/mirror, a mask).
- **Live** (`modifyLive`): a per-frame coordinate remap for animation (rotation), run only when an enabled modifier needs it — a static-only chain pays nothing.

**Dimensionality** for modifiers defaults to `Dim::D3` (assumed to work in all three axes unless declared otherwise). Unlike for effects, this is purely advisory: the Layer doesn't extrude modifier output. It exists so the UI can render the 📏/🟦/🧊 chip on the card. **MultiplyModifier** is D3 (it has independent multiplyX/Y/Z + mirrorX/Y/Z toggles).

Expand Down Expand Up @@ -522,7 +536,7 @@ The light domain plugs into the UI at three points: a fixed top-level tree (Layo

## What we leave undesigned

Genuinely open questions, *not* the same as a 🚧 marker. A 🚧 item is a committed design that simply isn't coded yet (multi-layer composition, two-core handover, time sync); the items here are ones where the *design itself* isn't settled, deferred until a concrete need forces the decision:
Genuinely open questions, *not* the same as a 🚧 marker. A 🚧 item has a settled, committed design (two-core handover, clock sync, device-to-device light distribution) — code is written toward it; the items here are ones where the *design itself* is still open, deferred until a concrete need forces the decision:

- **WiFi runtime disable**: today the eth-only build profile compiles WiFi out. Whether runtime gating should key off detected hardware presence, an explicit control, or a deviceModel-catalog field isn't decided; the eth-only build covers the need until one is.
- **Mixing light types in one Layouts**: each layout child describes one light type (all LED strips, or all par lights). Whether a single Layouts container should hold mixed types (LED strips + par lights together), and how the channel layout would reconcile across them, isn't designed; one Layouts per light type is the current model.
Binary file added docs/assets/screenshots/ui_light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/screenshots/ui_overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshots/ui_theme.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/backlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ A map of everything in the three files, by theme.

### Mixed ([backlog-mixed.md](backlog-mixed.md))

- MultiplyModifier mapping-LUT memory at large grids; composed modifiers (chain the whole stack, not just the first); intermittent ~0.5 s RMT LED pauses; NoiseEffect simplex cost on ESP32.
- MultiplyModifier mapping-LUT memory at large grids; intermittent ~0.5 s RMT LED pauses; NoiseEffect simplex cost on ESP32.

## In-flight draft specs

Expand Down
2 changes: 1 addition & 1 deletion docs/backlog/backlog-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ Forward-looking companion to the shipped UI spec, [moonmodules/core/ui.md](../mo
These don't block the shipped baseline but should be answered before 1.0:

- **Multi-layer UI** — [architecture.md](../architecture.md) plans for N layers blended into one Drivers. The current card layout shows one Layer. Likely needs a tab/accordion to switch layers, or a per-layer column.
- **Modifier chain visualization** — show the modifier order visually. Today they're a flat list, and only the **first enabled** modifier actually applies (the `children[]` order is *not* yet an apply order — see [Composed modifiers](backlog-mixed.md#composed-modifiers--chain-the-whole-modifier-stack-not-just-the-first-planned-multi-commit)). This viz item only becomes meaningful *after* composed modifiers land; until then a chain UI would imply a stacking the engine doesn't do.
- **Modifier chain visualization** — show the modifier order visually. They're a flat list today, but the `children[]` order **is** the apply order now (modifiers compose as a chain, M₁∘M₂∘…), so a visual that conveys the stacking (and that order matters) would help users reason about a multi-modifier layer.
- **Presets** — save/load named bundles of control values. Persistence already stores them; needs a UI surface.
- **Canvas/node-graph view** — v2 attempted this. Powerful for complex setups but doubles the UI surface. A reasonable v3 follow-up gated on user demand.

Expand Down
Loading
Loading