Skip to content

Backlog — mixed (core + light)

Forward-looking items whose work genuinely spans both the core and light domains — a core mechanism interacting with a light driver/effect/modifier, where assigning it to one side would misrepresent it. Core-only items are in backlog-core.md, light-only in backlog-light.md. Index + overview: README.md.

Cross-domain

LightsControl — the central control hub (palette + global params + presets + external controllers)

The eventual home for global light control, modelled on MoonLight's ModuleLightsControl (source, docs — our own project, study don't copy). The concept worth carrying forward: one module is the integration point between the device and the outside world (IR remotes, DMX-in, MQTT / Home Assistant, hardware buttons, PIR) and the owner of the global light state effects read — so effects respond to one normalised interface instead of N disparate inputs. It owns: master on/off + brightness, RGB tint multipliers, the active palette, global bpm / intensity sliders broadcast to all active effects, and the preset system (below).

Pragmatic interim (decided 2026-06-30): we are not building this module now. Its two pieces we need first live on the Drivers container (already the owner of global render params — brightness, lightPreset, the shared Correction): the active palette lands there in the palette stage (a palette select; effects read it via a static Palettes::active() seam, the AudioService::latestFrame() pattern), and global bpm / intensity can follow the same way when a consumer needs them. When LightsControl is eventually built, it absorbs these controls from Drivers and becomes the hub the external controllers feed. Not a blocker for the palette work.

Light presets — save / load / loop a whole effect configuration

MoonLight's preset system (part of ModuleLightsControl): 64 named slots, each capturing the complete effect-tree configuration (all module control values), with save / load / delete, preset looping on a timer (rotate between a first/last slot), and Home-Assistant registration for external recall. The product owner has flagged this as definitely needed.

Not a dependency of palettes — a separate feature, its natural home the LightsControl module above (or Drivers in the interim). Persistence already round-trips control values (the same overlay that restores the device-tree), so a preset is "snapshot the relevant subtree's JSON to a named file, restore it on recall" — the mechanism mostly exists; what's missing is the slot management + UI + loop timer. Complements the existing presets UI note (control-value bundles) — this is the light-domain, whole-effect-config version with looping. Build as its own stage when LightsControl (or its interim) is ready.

MultiplyModifier mapping-LUT memory at large grids (investigation, re-verify on classic)

scenario_perf_full on the S3 (2026-06-17) measured the MultiplyModifier's cost across grid sizes. The finding, stated correctly: the modifier reduces compute (with the default 2×2 kaleidoscope the effect renders only the ¼-size logical quadrant — Noise+Multiply at 16K is 29,647µs vs 50,555µs for Noise alone), and its real cost is memory — the mapping LUT's destinations array. Measured modifier heap cost on the S3: 16²→1.7KB, 32²→10.8KB, 64²→23.5KB, 128²(16K)→93KB (nrOfLightsType is uint32_t on a PSRAM board). On the S3's 8MB PSRAM this is trivial. Under the physical→logical fold build each physical light contributes ≤1 destination, so the destinations array is bounded by the real light count regardless of chain depth — there is no build-time fan-out.

This is NOT a no-PSRAM blocker — 16K Noise + Multiply has run on a classic ESP32 (no PSRAM, 320KB internal) before at 10–20 FPS (WiFi vs Ethernet), sending frames out over ArtNet to a display, not physical LED drivers. It works there because classic's nrOfLightsType is uint16_t (half the LUT size) and the modifier shrinks the logical render grid. So the action is re-verify the working classic setup when a classic board is connected (find the config — grid, mirror, ArtNet target — that reproduces the historical 10–20 FPS), not "fix an impossibility." Worth investigating only if that re-verification shows the LUT memory has regressed since: the destinations array is the obvious lever (it stores a nrOfLightsType per physical destination; a 2× kaleidoscope is 1:1 in count so the LUT need not store fan-out > the physical count — confirm it isn't over-allocating to maxMultiplier() when the effective fan-out is 1). Capture the classic numbers into performance.md's multi-board table first.

NoiseEffect simplex cost on ESP32 (investigation)

With mirror XY at 128×128, NoiseEffect renders the 64×64 logical quadrant in ~11 ms/tick on the Olimex (measured) — the simplex math dominates, since the Xtensa LX6 has no FPU and float math is software-emulated. (RainbowEffect on the same pipeline is much cheaper.) This is correct, non-degraded behaviour; it's only worth revisiting if a deployment needs Noise faster than ~11 ms at this grid.

Worth investigating if so:

  • Q16 fixed-point simplex instead of float (kills the software-float emulation cost).
  • Lower-precision hash — current simplex uses a 256-entry permutation lookup; a smaller / SIMD-friendly hash may be faster on Xtensa.
  • Strided sampling + interpolation — render at 32×32, bilinear up to 64×64. Visual quality cost; needs A/B comparison.
  • Inline / unroll the inner per-pixel loop to keep the simplex state in registers.

None of these are obviously free, and a fixed-point port may shift the visual signature. Defer until there's a real use case — on the no-PSRAM Olimex at large grids the tick is dominated by the synchronous ArtNet send (~35 ms), not Noise, so the effect is rarely the bottleneck there.

S3 render-only data point (2026-06-17, scenario_perf_full): on the PSRAM S3 with no output driver, Noise is the dominant cost at every grid and there's no ArtNet floor to hide it: 16²→738µs, 32²→2,831µs, 64²→11,235µs, 128²(16K)→50,555µs (~20 FPS) — clean ~linear-in-pixels (67×), so no fragmentation/realloc pathology, just raw simplex compute. The light effect (Checkerboard) on the same sweep is 6–11× faster (16K→7,949µs, ~128 FPS). So on a PSRAM board the heavy effect IS the 16K bottleneck (where on the Olimex the network send was). This is the strongest case for the fixed-point/strided-sampling ideas above, since a PSRAM board can run 16K grids that the network-bound Olimex never reaches. The S3 has a real FPU (LX7), so the win is less about software-float emulation and more about per-pixel simplex work; profile before committing.