Skip to content

Share one toolbox: power functions, particles, shaders — and presets - #62

Merged
ewowi merged 10 commits into
mainfrom
next-iteration
Aug 8, 2026
Merged

Share one toolbox: power functions, particles, shaders — and presets#62
ewowi merged 10 commits into
mainfrom
next-iteration

Conversation

@ewowi

@ewowi ewowi commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Effects used to hand-roll their own drawing, math and motion: each one carried its own line
walker, its own sine, its own fade, its own idea of how fast a frame is. This branch replaces
that with one shared toolbox, and migrates the existing effects onto it.

Two things fall out. Effects get shorter and more capable — anti-aliasing, sub-pixel
positioning and signed distance fields arrive everywhere at once rather than per effect. And
motion becomes a property of time rather than of hardware: the same settings look the same on
a 30 fps wall and a 5,000 fps desktop, with the faster device drawing it more smoothly.

What landed

Presets and a control surface. ControlModule saves and restores any part of the module
tree as a named JSON file. A Layers-only preset is hardware-portable; adding Drivers makes
it a device snapshot carrying pin maps. Presets publish to Home Assistant as a select.

The power-function library (docs/moonmodules/light/power-functions.md lists every
function with its callers):

  • DrawingCanvas, lines, bars, rects, circles, scroll, splat, anti-aliased lines
  • Mathsin16/cos16, atan16, dist16, map32, easings, BeatPhase, hashInt
  • Fields — value noise at 8 and 16 bits, fbm, turbulence, warp, blobs
  • Signed distance fields — shapes, smin, and free anti-aliasing from coverage()
  • Particles — an SoA pool over caller-owned buffers, forces, semi-implicit Euler, walls,
    collisions, emitters
  • Shaders — the GLSL vocabulary in fixed point, plus raymarching where the SoC has an FPU

Twelve new effects (40 → 52), each a showcase for one part of the toolbox: SdfShapes,
PolarNoise, WaterRipple, Tunnel, Echo, Dissolve, Spectrum, Fireworks, Ballpit, Truchet,
Raymarch, VectorBalls.

Framerate independence as a system rule (architecture.md). Everything that changes over
time is driven by elapsed time, never by frame count. Quantising to a fixed 60 Hz and skipping
frames is explicitly the wrong fix, since it discards the smoothness the extra frames buy.
unit_Effects_framerate.cpp audits all 52 effects at 60 vs 1200 fps.

Bugs this found and fixed

Several were live before the branch, and none were visible from reading the code:

  • sin16/cos16 returned unsigned where FastLED master and WLED main both return signed. A
    ported effect that writes sin16(x) + 32768 is offset by half scale with no error anywhere.
  • isqrt64 returned 0 for UINT64_MAX — its first Newton step overflowed. Once collide()
    used it, that would have read as zero separation.
  • lerp16 overflowed int32 on roughly a quarter of samples (undefined behaviour), and fbm16
    shifted each octave down by 8 before summing, making its output 8-bit in a 16-bit type: 195
    distinct values over 20,000 samples, now 15,118.
  • Echo ran 1 feedback pass per second at 240 fps instead of 60 — the trail effectively froze.
  • ParticlesEffect froze entirely at low speed on a fast device, where the per-frame step
    truncated to zero.

Performance

main branch
desktop tick (NoiseEffect, 4,096 lights) 129–142 µs 134–140 µs
esp32 tick 4,164 µs 4,164 µs
esp32s3-n16r8 flash 1,628 KB 1,699 KB

Desktop tick measured three runs each, side by side against a main worktree: the ranges
overlap and there is no regression. The repo-health "+13 µs ⚠" marker compares two single
samples of a noisy metric. Flash grows by the new effects and the library.

Raymarching measures 96 cycles/pixel on an S3 — below the 292 budgeted for a full 128×128
wall — which is why it is gated on SOC_CPU_HAS_FPU rather than restricted to desktop.

Known and deferred

  • Fireworks still counts frames for its launch roll. It has two framerate bugs that
    currently cancel: frame-counted launches, and a fadeToBlackBy floor that erases the trail
    faster on a fast device. Fixing one exposes the other, and fixing both changes how the
    effect looks, so it gets its own commit.
  • BouncingBalls has not moved onto the particle kernel; it wants ball-ball collisions,
    which is a rework rather than a migration.
  • Worley noise and the remaining 16-bit noise tuning (gradient vs value noise, 3D
    fbm16, warp16/turbulence16) are backlogged, each to land with the first effect that
    needs it.

Review

CodeRabbit and two Reviewer passes. Findings fixed, except: the two grid guards in effects
(Layer::tick already gates on hasGrid, so a guard there is dead code and an orchestration
violation), map32's split numerator (needs both spans near 2^32; no call site does), and
moving FrameTime to core / abstracting raymarch behind a platform interface — both real,
both refactors across every caller rather than review fixes.

Summary by CodeRabbit

  • New Features

    • Added a control surface for saving, applying, renaming, deleting, and ordering device presets.
    • Added physical-style encoders, faders, preset pads, visual feedback, drag-and-drop editing, and responsive layouts.
    • Added Home Assistant and WLED preset integration with live preset updates.
    • Added numerous lighting effects, including fireworks, particles, tunnels, ripples, spectrum visualization, and raymarching.
    • Added desktop port selection through command-line options.
  • Bug Fixes

    • Improved rendering across grid sizes, channel counts, empty layouts, and varying frame rates.
    • Prevented invalid preset data and unsafe names from altering device state.

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant