Skip to content

Scenario Tests

Auto-generated from test/scenarios/{core,light}/scenario_*.json by moondeck/docs/generate_test_docs.py. Do not edit by hand β€” update the JSON file's top-level fields and per-step description / bounds / contract / observed instead, then regenerate.

Scenario tests are the integration tier in the test strategy: each one is a JSON script that drives the full pipeline (desktop or live ESP32) and captures tick / heap per step against per-target contracts. Run them with moondeck/scenario/run_scenario.py (desktop) or moondeck/scenario/run_live_scenario.py (live device). See testing.md Β§ Performance contracts for the contract semantics.

AudioService

scenario_Audio_mutation

test/scenarios/light/scenario_Audio_mutation.json β€” Add / configure / remove the AudioService (a Service module) and an audio-reactive effect while the render pipeline runs, proving the robustness rule for the audio producer/consumer pair. AudioService is a Service (it sits beside the pipeline, publishing an AudioFrame), and the audio effects read it through the static AudioService::latestFrame() accessor, NOT a boot-time pointer, so add/remove can happen in any order at runtime. The checks assert the pipeline keeps RENDERING (buffer non-null, fps measurable) through each mutation: adding the mic, configuring it one control write at a time (device / sampleRate / gain, the install fan-out's exact add-then-configure sequence from a catalog entry's modules+controls), adding a consumer effect, and crucially REMOVING the mic while a consumer is still live (the consumer must fall back to a silent frame, never deref a dangling pointer, the bug the boot-loop fix and the unit lifecycle tests pin, here proven end-to-end through the Scheduler). The sequential writes include a REAL live capture reinit on capture hosts; the pipeline never stalls through any of it. On a capture-capable host the device write triggers a REAL OS-capture reinit mid-render (silence-tolerant: a locked-down host degrades to a status line); the I2S pin path is proven on hardware. Grid is 64x64 so the tick stays above the host microsecond clock at every step.

Mode: mutate Β· Also touches: SystemModule, Services, Layouts, GridLayout, Layer, RainbowEffect, AudioVolumeEffect, AudioSpectrumEffect, Drivers, PreviewDriver

measure-pipeline-only (measure) πŸ“

Baseline: the render pipeline runs with no audio module present.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 21,739-50,000 β€” / β€” β€” / β€”
desktop-windows β€” / 27,778 β€” / β€” β€” / β€”
esp32 β€” / 76.9-4,115 β€” / 49KB-95KB β€” / 38KB-48KB
esp32s3-n16r8 β€” / 29,412 β€” / 7402KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-audio-added (measure) πŸ“

Pipeline still renders with the (idle, unconfigured) mic added.

Setup (preceding non-measured steps): - add-audio-module (add_module) β€” Add the AudioService under the Services container (where the user adds a capability module). Pins default unset, so it stays idle; the pipeline must keep rendering.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 14,286-38,462 β€” / β€” β€” / β€”
desktop-windows β€” / 27,778 β€” / β€” β€” / β€”
esp32 β€” / 77.0-4,237 β€” / 51KB-89KB β€” / 38KB-72KB
esp32s3-n16r8 β€” / 26,316 β€” / 7467KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-capture-configured (measure) πŸ“

Audio input configured via the sequential install-fan-out order: pipeline still renders through the full add-then-configure flow (add AudioService, then device/sampleRate/gain one write at a time, a live capture reinit among them on capture hosts).

Setup (preceding non-measured steps): - configure-device (set_control) β€” Pick the capture device (index 0, the OS default). On a capture-capable host this triggers a REAL live reinit: the OS device opens (or fails cleanly on a locked-down host) while the pipeline renders, the desktop equivalent of the install fan-out's pin writes. The pipeline must keep rendering through it. Optional: the control exists only where hasAudioCapture is true, so board targets record a clean skip. - configure-rate (set_control) β€” Change the sample rate (44100). Rebuilds the capture (or I2S) channel live, mid-render, the second config write of the sequence. - configure-gain (set_control) β€” Set the analysis gain. A plain (non-prepare) control write completing the add-then-configure flow a catalog inject performs; the prepareTree rebuilds before it must not have disturbed the running pipeline.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 15,873-38,462 β€” / β€” β€” / β€”
desktop-windows β€” / 27,778 β€” / β€” β€” / β€”
esp32 β€” / 77.4-90.6 β€” / 48KB-92KB β€” / 26KB-88KB
esp32s3-n16r8 β€” / 21,277 β€” / 7402KB β€” / 76KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-consumer-live (measure) πŸ“

Pipeline renders with the producer + consumer both wired.

Setup (preceding non-measured steps): - add-audio-consumer (add_module) β€” Add an AudioVolumeEffect consumer under the Layer. It reads the mic via the static accessor; with the mic present it gets the live (silent, on host) frame.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 14,706-38,462 β€” / β€” β€” / β€”
desktop-windows β€” / 25,000 β€” / β€” β€” / β€”
esp32 β€” / 73.8-85.8 β€” / 48KB-91KB β€” / 26KB-88KB
esp32s3-n16r8 β€” / 21,739 β€” / 7379KB β€” / 60KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-after-mic-removed (measure) πŸ“

Mic gone, consumer remains: pipeline keeps rendering on silent audio (buffer non-null, fps measurable). No crash from the orphaned consumer.

Setup (preceding non-measured steps): - remove-audio-module (remove_module) β€” Remove the mic while the consumer is STILL live. The consumer must fall back to AudioService::latestFrame()'s static silence β€” no dangling pointer, no crash. This is the robustness rule's hardest case for this pair.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 14,493-43,478 β€” / β€” β€” / β€”
desktop-windows β€” / 25,000 β€” / β€” β€” / β€”
esp32 β€” / 70.5-81.9 β€” / 54KB-97KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 27,027 β€” / 7480KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-back-to-baseline (measure) πŸ“

Both audio modules gone: back to the pipeline-only baseline, still rendering.

Setup (preceding non-measured steps): - remove-audio-consumer (remove_module) β€” Remove the orphaned consumer too β€” clean teardown, pipeline still live.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 12,658-50,000 β€” / β€” β€” / β€”
desktop-windows β€” / 28,571 β€” / β€” β€” / β€”
esp32 β€” / 65.2-76.0 β€” / 54KB-96KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 29,412 β€” / 7482KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

AuroraEffect

scenario_Aurora_fps

test/scenarios/light/scenario_Aurora_fps.json β€” What a layered shader costs, one control at a time. Aurora samples a warped noise field once per layer per pixel, so its frame time is set by three knobs that multiply: how many layers, how many octaves inside each, and whether the field warps at all. The steps walk them from the cheapest configuration to the most expensive on one grid, so the ratios between the observed numbers are what an author reads to choose settings for a fixture, and a jump in any one of them between commits is a regression with a name. The grid is deliberately 64x64: large enough that per-pixel cost dominates, small enough to run on every target the contracts name.

Mode: mutate Β· Also touches: OscillatorBank, PolarLut, GridLayout, Layer

aurora-defaults (add_module) πŸ“

Aurora as it ships: 3 layers, 2 octaves, warp on. This is the number a catalog card promises, and the one every step below is read against.

Setup (preceding non-measured steps): - canvas-clear-layers (clear_children) β€” Self-canvas: clear and rebuild the pipeline this scenario assumes, so it runs from any device state. - canvas-clear-layouts (clear_children) - canvas-grid (add_module) - canvas-layer (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 333 / 719-1,170 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: Aurora at 64x64, 3 layers, 2 octaves, warp on" Β· observed 2026-09-06

one-layer (set_control) πŸ“

The cost knob at its cheapest. Layers are the outer multiplier: each one is a full warped field sample per pixel, so this should land near a third of the step above.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 667 / 2,227-3,145 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: one layer, roughly a third of the three-layer cost" Β· observed 2026-09-06

one-layer-no-warp (set_control) πŸ“

Warp off: the field is sampled where it lies rather than where the field displaced it, which is two fewer noise samples per pixel. The cheapest Aurora that still reads as Aurora, and the configuration a large wall runs.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 1,111 / 4,082-5,348 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: one layer without domain warping" Β· observed 2026-09-06

four-layers-no-warp (set_control) πŸ“

Four unwarped layers: the same total field samples as roughly one warped layer, arranged as more structure instead of more folding. Reading this against one-layer-no-warp is how an author trades detail for depth at a fixed budget.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 294 / 1,425-1,815 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: four layers, warp off" Β· observed 2026-09-06

four-layers-four-octaves (set_control) πŸ“

Four unwarped layers at four octaves: the octave knob at its top, with warp still off from the step above. Read against four-layers-no-warp, this is what octaves alone cost.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 143 / 864-1,053 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: four layers, four octaves, warp still off" Β· observed 2026-09-06

warp-back-on (set_control) πŸ“

Warp back on, so the two steps below measure the configuration Aurora actually ships. Setting it explicitly rather than relying on the default is what keeps each number readable on its own.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 111 / 534-679 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: four layers, four octaves, warp on: the true ceiling" Β· observed 2026-09-06

kaleidoscope (set_control) πŸ“

Fold the composition into six wedges, at the ceiling settings above. The symmetry is applied to the angle before the field is ever sampled, so it costs one modulo per pixel and not a second pass over the field: the small step from warp-back-on is what pins that claim.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 105 / 547-661 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: the fold adds one modulo per pixel, not a pass" Β· observed 2026-09-06

back-to-defaults (set_control) πŸ“

Octaves back to the shipped 2, with four layers and the fold still on. Returning near the four-layers steps above is what shows the effect carries no state that accumulates across reconfiguration.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 238 / 826-895 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: four layers, two octaves, folded" Β· observed 2026-09-06

Effects

scenario_Effects_composition

test/scenarios/light/scenario_Effects_composition.json — Multi-layer composition end-to-end: Layouts→Grid, TWO Layers under one Effects container (bottom Spiral, top Rainbow), Drivers→NetworkSendDriver. Proves the Drivers composite loop builds, allocates its output buffer, blends both enabled layers and feeds the result to the driver without crashing, and gates the bounded FPS so the N-pass composite cost is tracked. The exact alpha/additive blend math and the disable-drops-to-single-layer path are pinned by the unit tests (unit_BlendMap, unit_Effects_container); construct-mode set_control can't apply controls (built post-scheduler), so this scenario uses each Layer's default blend (additive, full opacity) and asserts wired liveness + tick, not per-byte blend output.

Mode: construct Β· Also touches: Layer, GridLayout, RainbowEffect, SpiralEffect, Drivers, NetworkSendDriver

add-artnet (add_module) πŸ“

Add NetworkSendDriver and run the bounded FPS measurement over the two-layer composite (min_pct gates on hardware; skipped with a WARN in the desktop runner).

Setup (preceding non-measured steps): - add-layout-group (add_module) β€” Top-level Layouts container. - add-grid (add_module) β€” 128x128 GridLayout under Layouts (above host clock resolution so the composite tick is measurable). - add-layers-group (add_module) β€” Top-level Effects container β€” the multi-layer composition host. - add-bottom-layer (add_module) β€” Bottom Layer (composited first β€” clears + overwrites the output buffer). RGB. - add-bottom-effect (add_module) β€” A Spiral base as the bottom layer's effect. - add-top-layer (add_module) β€” Top Layer (composited second β€” blends onto the bottom with its default blend). RGB. - add-top-effect (add_module) β€” Rainbow as the top layer's effect β€” composited over the Spiral base. - add-driver-group (add_module) β€” Top-level Drivers container wired to the Effects container (composites all enabled layers into its output buffer).

Bounds: - FPS β‰₯ 80% of baseline - FPS Γ— lights β‰₯ 294,912

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 1,302-5,917 β€” / β€” β€” / β€”
desktop-windows β€” / 1,821 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08

FluidEffect

scenario_Fluid_solver

test/scenarios/light/scenario_Fluid_solver.json β€” What a stable-fluid solver costs, and that it survives being reshaped underneath itself. Unlike every other flow in the library the velocity here is STATE: six grids the solver owns, reallocated whenever the fixture changes, which is what makes the resize steps a real contract rather than a formality. A stale grid would address cells that no longer exist. The cost knob is iterations, the pressure solve that keeps the flow divergence-free: each one is another Gauss-Seidel sweep over the whole grid, so the ladder from 1 to 20 is close to linear and it is what an author reads to pick a value for their fixture. On a cube every slice is its own medium, with nothing carried between slices, so the cost is depth times one panel. The frame times are a TREND per target rather than a pass mark; the solver is several passes over the grid per frame, so this is a desktop and P4 effect and the device numbers are the ones that decide where it can run.

Mode: mutate Β· Also touches: Fluid, draw, OscillatorBank, GridLayout, Layer

add-fluid (add_module) πŸ“

The solver on the layer. prepare() allocates six velocity and pressure grids plus two dye planes, so the allocation happens here rather than on the first rendered frame.

Setup (preceding non-measured steps): - canvas-clear-layers (clear_children) β€” Self-canvas: rebuild the pipeline this scenario assumes, so it runs from any device state. - canvas-clear-layouts (clear_children) - canvas-grid (add_module) β€” A 32x32 panel to start: small enough that the six solver grids are cheap while the shape of the cost is already visible. - canvas-layer (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 13,699-33,333 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

iterations-1 (set_control) πŸ“

The pressure solve at its cheapest. At one sweep the flow is not properly divergence-free and reads springy, which is the visible cost of the cheap setting.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 27,027-52,632 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

iterations-20 (set_control) πŸ“

And at its most expensive. The ratio against the step above is the pressure solve's share of the frame, which is what an author trades against grid size.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 9,901-14,706 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

iterations-5 (set_control) πŸ“

Back to the default, confirming the knob moves in both directions rather than one way.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 27,027-34,483 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

grid-64 (set_control) πŸ“

Widen to 64 under a running solver, height still 32: one dimension doubled, so twice the cells, and every one of the six grids is reallocated in prepare(). A solver that carried its old grid over would be addressing cells that no longer exist.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 13,514-15,625 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

grid-64-h (set_control) πŸ“

And the height, making it 64x64: four times the cells of the 32x32 the pair started from. Reallocating on each axis separately is the shape a UI resize actually takes.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 6,897-7,576 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

all-jets (set_control) πŸ“

Every jet pouring at once. The jets are forcing rather than per-pixel work, so this should barely move the frame time: the solver dominates, which is the point.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 6,897-7,576 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

long-persistence (set_control) πŸ“

Dye held at its longest half-life, the setting the 16-bit plane exists for: a value multiplied by slightly less than one many times a second has nowhere to go at 8 bits.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 6,803-7,634 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

cube-20 (set_control) πŸ“

A 20x20x20 cube, the largest volumetric fixture in practice: twenty independent slices, each its own medium, with the jets drifting through them in z. The cost is twenty small solves, close to one 90x90 panel, and it is what decides whether the effect belongs on a cube at all.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 25,000-27,778 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

cube-20-h (set_control) πŸ“

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 83,333-90,909 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

cube-20-d (set_control) πŸ“

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 4,016-4,545 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

back-to-panel (set_control) πŸ“

Depth back to 1: the slices are freed and the panel path is exactly the 2D one again.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 83,333-90,909 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

grid-16 (set_control) πŸ“

Shrink it again, the direction that frees rather than allocates, and the one where a solver holding a stale pointer shows up immediately.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 34,483-111,111 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

grid-16-h (set_control) πŸ“

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 125,000-142,857 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

GridBlacksLayout

scenario_GridBlacks_blackpixel

test/scenarios/light/scenario_GridBlacks_blackpixel.json β€” Toggle a mid-strand dark-gap (black column) run on a GridBlacks layout live and verify the full Layouts->Layer->Drivers pipeline keeps rendering. A gap is a physical wire slot held black: it forces the layer off the dense identity fast path onto the folded LUT (which drops the gap slots), while the SpiralEffect fill keeps the lit cells non-zero. Steps: build a 16x16 grid with a moving fill (dense, identity path), then set blackCount live (gap engaged, folded LUT, still renders non-zero because lit cells remain), then clear blackCount back (returns to the identity path). A crash, a wedge, or an all-black buffer on the gapped step would fail the measure. The exact 'gap cells carry no color' correctness is pinned by unit_GridBlacksLayout / unit_Layer_sparse_mapping; this is the live wired-pipeline gate.

Mode: mutate Β· Also touches: Layer, SpiralEffect

dense-no-gap (set_control) πŸ“

Baseline: a plain 16x16 moving fill, no gap. Dense identity fast path (no LUT). Buffer is fully non-zero.

Setup (preceding non-measured steps): - canvas-clear-layers (clear_children) β€” Self-canvas: clear+rebuild the pipeline this scenario assumes, so it runs from any device state. - canvas-clear-layouts (clear_children) - canvas-clear-drivers (clear_children) - canvas-grid (add_module) - canvas-layer (add_module) - canvas-effect (add_module) β€” A moving fill: every lit cell is a known non-zero color, so an all-black buffer would mean the gap wrongly blanked the whole grid.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 83,333-500,000 β€” / β€” β€” / β€”
desktop-windows β€” / 142,857 β€” / β€” β€” / β€”
esp32 β€” / 4,115 β€” / 120KB β€” / 92KB
esp32s3-n16r8 β€” / 5,682 β€” / 8326KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

gap-engaged (set_control) πŸ“

Set a dark column run live (blackStart 6, blackCount 4 -> columns 6..9 dark) as the FINAL state. Forces the folded LUT (drops the 4x16 gap slots). The pipeline must keep rendering: the lit cells of the moving fill stay non-zero (buffer non-zero holds), the physical light count is unchanged at 256 (gaps are real wire slots the driver still clocks), no crash. Ending on the gapped state means the buffer-non-zero check asserts the gap did NOT blank the grid.

Setup (preceding non-measured steps): - gap-start (set_control) β€” Position the dark run at column 6 (before engaging it). Still no gap until blackCount is set. - gap-cleared-again (set_control) β€” Clear the run back to 0 (from the earlier gap-start position set): confirms the toggle is reversible with no reboot before the gap is engaged for the final measured state. Returns to the dense identity fast path.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 62,500-500,000 β€” / β€” β€” / β€”
desktop-windows β€” / 125,000 β€” / β€” β€” / β€”
esp32 β€” / 3,717 β€” / 119KB β€” / 92KB
esp32s3-n16r8 β€” / 3,745 β€” / 8320KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

GridLayout

scenario_GridLayout_resize

test/scenarios/light/scenario_GridLayout_resize.json β€” Resize the grid while the pipeline is running and verify it reallocates cleanly under memory pressure. Lowers to 128x64 (release memory), increases to 128x128 (heaviest config: mirror + LUT). Each measured step captures tick/FPS/heap so the runner reports the degrade behaviour.

Mode: mutate Β· Also touches: MultiplyModifier, Layer

size-128x128 (set_control) πŸ“

Set grid height to 128 (alongside default width 128). Measures the heaviest config as the baseline for the next two steps.

Setup (preceding non-measured steps): - canvas-clear-layers (clear_children) β€” Self-canvas: clear+rebuild the pipeline this scenario assumes, so it runs from any device state (the perf scenarios' pattern). Pre-wired apparatus (Preview/Board) survives clear_children. On the in-process runner the fixture already built the tree; clearing then rebuilding is harmless there and makes the live run order-independent. - canvas-clear-layouts (clear_children) - canvas-clear-drivers (clear_children) - canvas-grid (add_module) - canvas-layer (add_module) - canvas-noise (add_module) - canvas-mirror (add_module) - canvas-artnet (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 8,333 / 3,215-7,874 unlimited / β€” β€” / β€”
desktop-windows β€” / 4,566 β€” / β€” β€” / β€”
esp32 β€” / 740 β€” / 83KB β€” / 48KB
esp32-eth β€” / 10.8 β€” / 132KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 12.4 β‰₯ 103KB / 93KB β€” / 48KB
esp32p4rev1-eth β€” / 880 β€” / 33206KB β€” / 376KB
esp32s3-n16r8 β€” / 989 β€” / 8293KB β€” / 76KB
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-07-24
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-22
  • esp32s3-n16r8: observed 2026-07-24

shrink-to-128x64 (set_control) πŸ“

Shrink to 128x64. Measured: tick/heap captured so the runner reports the realloc behaviour against each target's contract. (The old relative-to-baseline FPS bound was removed β€” it compared against the runner's idle pre-scenario baseline, not the prior render step, so it false-failed on fast boards like the P4 where idle FPS dwarfs render FPS.)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 16,667 / 6,897-15,152 unlimited / β€” β€” / β€”
desktop-windows β€” / 10,638 β€” / β€” β€” / β€”
esp32 β€” / 1,745 β€” / 63KB β€” / 17KB
esp32-eth β€” / 26.5 β€” / 114KB β€” / 48KB
esp32-eth-wifi β‰₯ 22.2 / 31.8 β‰₯ 83KB / 75KB β€” / 24KB
esp32p4rev1-eth β€” / 1,739 β€” / 33214KB β€” / 376KB
esp32s3-n16r8 β€” / 2,092 β€” / 8310KB β€” / 84KB
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-07-24
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-22
  • esp32s3-n16r8: observed 2026-07-24

grow-to-128x128 (set_control) πŸ“

Grow back to 128x128. Measured: confirms the heap can return to the heavy baseline after a shrink.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 8,333 / 3,356-7,937 unlimited / β€” β€” / β€”
desktop-windows β€” / 4,608 β€” / β€” β€” / β€”
esp32 β€” / 839 β€” / 83KB β€” / 52KB
esp32-eth β€” / 10.4 β€” / 132KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 12.2 β‰₯ 103KB / 93KB β€” / 52KB
esp32p4rev1-eth β€” / 875 β€” / 33206KB β€” / 376KB
esp32s3-n16r8 β€” / 1,106 β€” / 8292KB β€” / 76KB
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-07-24
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-22
  • esp32s3-n16r8: observed 2026-07-24

Layer

scenario_Layer_base_pipeline

test/scenarios/light/scenario_Layer_base_pipeline.json — Core pipeline: build Layouts→Grid→Layer→RainbowEffect→Drivers→NetworkSendDriver from scratch and verify each module wires correctly. Drives the bounded FPS check at the end so a render-path regression is caught.

Mode: construct Β· Also touches: GridLayout, RainbowEffect, Drivers, NetworkSendDriver

add-artnet (add_module) πŸ“

Add NetworkSendDriver and run the bounded FPS measurement (expected to stay at >=80% of the rated FPS for the 128x128 grid this scenario builds; min_pct needs a live baseline, so it gates only on hardware and is skipped with a WARN in the desktop runner).

Setup (preceding non-measured steps): - add-layout-group (add_module) β€” Create the top-level Layouts container. - add-grid (add_module) β€” Add a 128x128 GridLayout child to Layouts. Set explicitly (the module default is 16x16x1) so the tick is above the host's microsecond clock resolution β€” a 16x16 grid renders in <1us on desktop, flooring tick to 0. - add-layer (add_module) β€” Add a top-level Layer wired to the Layouts container, RGB (3 channels per light). - add-rainbow (add_module) β€” Add RainbowEffect as the Layer's only effect. - add-driver-group (add_module) β€” Add a top-level Drivers container wired to the Layer's output buffer.

Bounds: - FPS β‰₯ 80% of baseline - FPS Γ— lights β‰₯ 294,912

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 20,000 / 5,587-14,085 unlimited / β€” β€” / β€”
desktop-windows β€” / 8,475 β€” / β€” β€” / β€”
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-07-08

scenario_Layer_memory_1to1

test/scenarios/light/scenario_Layer_memory_1to1.json β€” Verify that an unshuffled 1:1 mapping (no modifier) uses no LUT and no driver buffer. Catches a regression where Layer would allocate a passthrough LUT for the identity case.

Mode: construct Β· Also touches: MappingLUT, BlendMap

add-artnet (add_module) πŸ“

Add NetworkSendDriver and run the bounded FPS measurement on the no-LUT path.

Setup (preceding non-measured steps): - add-layout-group (add_module) β€” Create the top-level Layouts container. - add-grid (add_module) β€” Add a 16x16 GridLayout. - add-layer (add_module) β€” Add a Layer wired to Layouts (RGB). - add-rainbow (add_module) β€” Add RainbowEffect as the Layer's effect. - add-driver-group (add_module) β€” Add a Drivers container wired to the Layer.

Bounds: - FPS β‰₯ 80% of baseline

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 20,000 / 25,000-200,000 unlimited / β€” β€” / β€”
desktop-windows β€” / 1,000,000 β€” / β€” β€” / β€”
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-08-22

scenario_modifier_chain

test/scenarios/light/scenario_modifier_chain.json β€” Stack TWO modifiers on one Layer (Region then Multiply) and verify the chain composes live end-to-end β€” the capability the old single-modifier engine couldn't do. Prepares its own canvas: Layout(Grid 32x32) + Layer + NoiseEffect + Region(0..50) + Multiply(2x), measures the composite, then adds a third (Checkerboard mask) and measures again, then removes the middle modifier and measures β€” exercising add/remove on a multi-modifier chain. A broken fold (null buffer, wrong light count, crash on a disabled/removed stage) shows up as a failed measure. The fold composition + order semantics are pinned by unit_Layer_modifier_chain; this is the live end-to-end gate.

Mode: mutate Β· Also touches: RegionModifier, MultiplyModifier, CheckerboardModifier, RotateModifier, NoiseEffect, Layouts, GridLayout, Drivers, NetworkSendDriver

add-mask (add_module) πŸ“

Add a third modifier (Checkerboard mask) on top of the chain β€” a 3-deep fold. Measure that the deeper chain still renders.

Setup (preceding non-measured steps): - region-then-multiply (measure) β€” Two stacked modifiers: Region(top-left quarter) then Multiply(2x mirror) compose into one mapping. Measure the live composite.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 27,778-111,111 β€” / β€” β€” / β€”
desktop-windows β€” / 83,333 β€” / β€” β€” / β€”
esp32 β€” / 64.7-82.2 β€” / 46KB-47KB β€” / 26KB-38KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-09-01

remove-middle (remove_module) πŸ“

Remove the middle modifier (Multiply) β€” the chain re-folds with Region then Checkerboard, no stale state. Measure.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 13,889-38,462 β€” / β€” β€” / β€”
desktop-windows β€” / 23,810 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22

add-live-rotate (add_module) πŸ“

Add a DYNAMIC Rotate on top of the static chain β€” its modifyLive runs the per-frame remap pass over the composed buffer. Verifies a static chain + a live modifier coexist (the buffer is remapped each frame on top of the baked Region/Checkerboard mapping) without a crash or null buffer.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 8,130-21,277 β€” / β€” β€” / β€”
desktop-windows β€” / 14,493 β€” / β€” β€” / β€”
esp32 β€” / 69.8-75.0 β€” / 43KB-46KB β€” / 26KB-38KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01

scenario_modifier_swap

test/scenarios/light/scenario_modifier_swap.json β€” Swap the Layer's modifier between Multiply and Checkerboard and verify the pipeline stays live across each replace. Prepares its own canvas (clear + rebuild) so it runs from any device state: one Layout(Grid 32x32) + one Layer + one effect + one modifier, then replace_module cycles the modifier MOD slot Multiply -> Checkerboard -> Multiply, measuring after each so a broken swap (null buffer / wrong light count) shows up. Exercises the modifier-replace path the UI's drag-replace uses.

Mode: mutate Β· Also touches: MultiplyModifier, CheckerboardModifier, NoiseEffect, Layouts, GridLayout, Drivers, NetworkSendDriver, PreviewDriver

multiply-1 (measure) πŸ“

Multiply modifier active β€” pipeline live, LUT folds the grid.

Setup (preceding non-measured steps): - shrink-w (set_control) - shrink-h (set_control) - clear-layers (clear_children) - clear-layouts (clear_children) - build-grid (add_module) - build-layer (add_module) - build-fx (add_module) - build-mod (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 14,085-111,111 β€” / β€” β€” / β€”
desktop-windows β€” / 71,429 β€” / β€” β€” / β€”
esp32 β€” / 71.3-81.6 β€” / 35KB-93KB β€” / 26KB-68KB
esp32-eth β€” / 7,752 β€” / 172KB β€” / 76KB
esp32p4rev1-eth β€” / 6,061 β€” / 33243KB β€” / 376KB
esp32s3-n16r8 β€” / 13,158 β€” / 7533KB β€” / 76KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32-eth: observed 2026-06-08
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

checkerboard (measure) πŸ“

Checkerboard modifier active β€” masks half the lights; pipeline stays live (driver buffer non-null).

Setup (preceding non-measured steps): - swap-to-checker (replace_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 11,494-41,667 β€” / β€” β€” / β€”
desktop-windows β€” / 24,390 β€” / β€” β€” / β€”
esp32 β€” / 81.5-89.2 β€” / 34KB-93KB β€” / 26KB-68KB
esp32-eth β€” / 990 β€” / 170KB β€” / 76KB
esp32p4rev1-eth β€” / 2,762 β€” / 33242KB β€” / 376KB
esp32s3-n16r8 β€” / 2,825 β€” / 7533KB β€” / 72KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32-eth: observed 2026-06-08
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

multiply-2 (measure) πŸ“

Back to Multiply β€” replace round-trips cleanly, pipeline live again.

Setup (preceding non-measured steps): - swap-to-multiply (replace_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 20,408-100,000 β€” / β€” β€” / β€”
desktop-windows β€” / 76,923 β€” / β€” β€” / β€”
esp32 β€” / 72.9-85.6 β€” / 37KB-95KB β€” / 26KB-68KB
esp32-eth β€” / 2,278 β€” / 169KB β€” / 76KB
esp32p4rev1-eth β€” / 6,410 β€” / 33243KB β€” / 376KB
esp32s3-n16r8 β€” / 6,803 β€” / 7534KB β€” / 76KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32-eth: observed 2026-06-08
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

scenario_perf_full

test/scenarios/light/scenario_perf_full.json β€” Comprehensive incremental performance check (the SLOW, on-device companion to scenario_perf_light). Mutate mode + canvas-preparing: clear_children whatever the device already had (pre-wired apparatus like PreviewDriver/Board survives β€” clear_children only drops user-editable children), rebuild a known minimal tree, then add one subsystem at a time β€” audio, device discovery, a modifier, then EVERY output driver this board has (each optional + capped to 64 output LEDs so its per-frame cost is comparable, not its transmit-all-16K time), then a network driver β€” measuring the tick/heap delta after each so each subsystem's cost is isolated. Then sweep the grid 16Β²β†’32Β²β†’64Β²β†’128Β² (16K) for both a LIGHT effect (Spiral) and a HEAVY one (Noise) to bracket the compute range across sizes. LED drivers are platform-gated (RMT on classic/S3, LCD on S3, Parlio on P4; none on desktop) so each driver step is optional:true and skipped where absent β€” the all-drivers comparison is assembled across boards (S3 gives RMT vs LCD, P4 gives RMT vs Parlio). Subsumes the old scenario_Layer_buildup (incremental module cost), scenario_GridLayout_grid_sizes (grid sweep), and scenario_AllEffects_grid_sizes (per-effect size sweep, here reduced to a light/heavy bracket). Runs minutes on a device; not a per-commit gate.

Mode: mutate Β· Also touches: Layouts, GridLayout, Drivers, PreviewDriver, NetworkSendDriver, RmtLedDriver, ParallelLedDriver, MultiplyModifier, SpiralEffect, NoiseEffect

measure-minimal (measure) πŸ“

Bare minimum at 16Β²: Grid + Layer + Spiral, no output driver, audio/discovery still on as the device ships. The floor for the subsystem-cost diffs below.

Setup (preceding non-measured steps): - clear-layers (clear_children) β€” Start clean: drop whatever effects/modifiers/layouts/drivers the device had (pre-wired Preview survives). - clear-layouts (clear_children) - clear-drivers (clear_children) - build-grid (add_module) - build-layer (add_module) - build-fx (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 111,111-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 758-1,098 β€” / 75KB-108KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 17,544 β€” / 33226KB β€” / 376KB
esp32s3-n16r8 β€” / 9,009 β€” / 8323KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-no-audio (measure) πŸ“

Setup (preceding non-measured steps): - disable-audio (set_control) β€” Disable the mic (stops I2S sampling in tick()). Diff vs measure-minimal = the audio subsystem's per-tick cost (device only; optional). Targets it by its runtime name 'Audio' (displayNameFor strips the 'Service' suffix).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 142,857-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 1,142-1,513 β€” / 82KB-118KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 18,868 β€” / 33228KB β€” / 376KB
esp32s3-n16r8 β€” / 9,901 β€” / 8330KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-quiet (measure) πŸ“

Quiet baseline: render-only, audio + discovery off. The cleanest render floor; the per-driver costs below diff against this.

Setup (preceding non-measured steps): - disable-devices (set_control) β€” Disable the Devices module (stops the blocking HTTP discovery sweep in tick1s()). Diff = the discovery cost (device only; optional).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 125,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 1,117-1,403 β€” / 84KB-118KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 18,519 β€” / 33226KB β€” / 376KB
esp32s3-n16r8 β€” / 9,901 β€” / 8331KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-modifier (measure) πŸ“

Setup (preceding non-measured steps): - add-modifier (add_module) β€” +MultiplyModifier: allocates the mapping LUT. Diff = modifier + LUT cost.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 250,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 2,336-3,367 β€” / 83KB-119KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 10,638 β€” / 33224KB β€” / 376KB
esp32s3-n16r8 β€” / 8,264 β€” / 8331KB β€” / 84KB
  • desktop-macos: observed 2026-09-03
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-preview (measure) πŸ“

Setup (preceding non-measured steps): - remove-modifier (remove_module) β€” Drop the modifier so the driver-cost measurements below are on the plain 1:1 pipeline (drivers, not the LUT, are what we compare here). - add-preview (add_module) β€” +PreviewDriver. Optional: on a device the pre-wired Preview survives clear_children so it's already present (the add is skipped); on the in-process desktop runner there's no apparatus, so this adds it. Either way the next measure includes Preview.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 125,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 1,080-1,458 β€” / 83KB-119KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 18,182 β€” / 33228KB β€” / 376KB
esp32s3-n16r8 β€” / 9,434 β€” / 8330KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-network (measure) πŸ“

Setup (preceding non-measured steps): - add-network-driver (add_module) β€” +NetworkSendDriver (ArtNet/DDP β€” works on every platform). Diff = the network output cost (capped by the 16Β² grid here).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 111,111-500,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 1,021-1,372 β€” / 80KB-117KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 17,544 β€” / 33226KB β€” / 376KB
esp32s3-n16r8 β€” / 8,065 β€” / 8331KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-rmt (measure) πŸ“

Setup (preceding non-measured steps): - remove-network-driver (remove_module) - add-rmt-driver (add_module) β€” +RmtLedDriver capped to 64 output LEDs (one pin, ledsPerPin=64). Optional β€” classic + S3. Diff = the RMT per-frame cost at a fixed 64-LED output.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 125,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 1,072-1,406 β€” / 57KB-91KB β€” / 38KB-68KB
esp32p4rev1-eth β€” / 17,857 β€” / 33200KB β€” / 376KB
esp32s3-n16r8 β€” / 9,346 β€” / 8307KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-i80 (measure) πŸ“

The i80 per-frame cost. Only meaningful where add-i80-driver's pins are valid (S3; desktop is inert but exercises the code path) β€” the classic/P4 blocks were removed: they recorded a driver that had failed to init, so the tiny tick was the bail-out path, not an encode.

Setup (preceding non-measured steps): - remove-rmt-driver (remove_module) - add-i80-driver (add_module) β€” +ParallelLedDriver (peripheral=i80) capped to 64 LEDs on lane 0 (i80 needs all 8 data pins; unused lanes get 0 LEDs). S3 ONLY β€” the pins below are the S3's clean set. The i80 peripheral also runs on the classic ESP32 (I2S backend) and the P4 (LCD_CAM), but NO 8-pin set is safe on all three (only GPIO 21 is common: the classic's 6-11 are the SPI flash bus, the P4 exposes a different range), so one shared step cannot measure them. On a non-S3 target the driver fails to init and the measure below is meaningless β€” the observed blocks are therefore S3/desktop only. Diff = the i80 per-frame cost.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 125,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 969-1,208 β€” / 80KB-93KB β€” / 38KB-46KB
esp32s3-n16r8 β€” / 9,259 β€” / 8330KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-parlio (measure) πŸ“

Setup (preceding non-measured steps): - remove-i80-driver (remove_module) - add-parlio-driver (add_module) β€” +ParallelLedDriver (peripheral=Parlio) capped to 64 LEDs on lane 0. Optional β€” P4 only. Diff = the Parlio per-frame cost.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 83,333-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 1,115-1,372 β€” / 80KB-92KB β€” / 38KB-46KB
esp32p4rev1-eth β€” / 17,857 β€” / 33225KB β€” / 376KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25

measure-light-16 (measure) πŸ“

Setup (preceding non-measured steps): - remove-parlio-driver (remove_module) - add-preview-for-sweep (add_module) β€” Re-add PreviewDriver as the output for the grid sweep (the per-driver adds above each removed their driver; Preview is the cheap, every-board output for a pure-render size curve). - light-16-w (set_control) β€” Grid sweep, LIGHT effect (Spiral is already FX). - light-16-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 111,111-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 1,218-1,513 β€” / 83KB-115KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 18,868 β€” / 33226KB β€” / 376KB
esp32s3-n16r8 β€” / 9,901 β€” / 8333KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-light-32 (measure) πŸ“

Setup (preceding non-measured steps): - light-32-w (set_control) - light-32-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 28,571-166,667 β€” / β€” β€” / β€”
desktop-windows β€” / 83,333 β€” / β€” β€” / β€”
esp32 β€” / 309-396 β€” / 78KB-114KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 7,576 β€” / 33225KB β€” / 376KB
esp32s3-n16r8 β€” / 3,597 β€” / 8323KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-light-64 (measure) πŸ“

Setup (preceding non-measured steps): - light-64-w (set_control) - light-64-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 5,747-43,478 β€” / β€” β€” / β€”
desktop-windows β€” / 20,408 β€” / β€” β€” / β€”
esp32 β€” / 67.2-96.2 β€” / 61KB-97KB β€” / 38KB-64KB
esp32p4rev1-eth β€” / 2,232 β€” / 33218KB β€” / 376KB
esp32s3-n16r8 β€” / 1,011 β€” / 8296KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-light-128 (measure) πŸ“

Setup (preceding non-measured steps): - light-128-w (set_control) - light-128-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 978-8,850 β€” / β€” β€” / β€”
desktop-windows β€” / 5,102 β€” / β€” β€” / β€”
esp32 β€” / 91.8-385 β€” / 84KB-90KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 573 β€” / 33182KB β€” / 376KB
esp32s3-n16r8 β€” / 134 β€” / 8283KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-heavy-16 (measure) πŸ“

Setup (preceding non-measured steps): - swap-heavy (replace_module) β€” Swap to the HEAVY effect (Noise) and repeat the sweep β€” the upper bracket of per-pixel compute. - heavy-16-w (set_control) - heavy-16-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 66,667-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 2,342-2,924 β€” / 83KB-120KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 3,367 β€” / 33229KB β€” / 376KB
esp32s3-n16r8 β€” / 3,472 β€” / 8329KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-heavy-32 (measure) πŸ“

Setup (preceding non-measured steps): - heavy-32-w (set_control) - heavy-32-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 13,889-58,824 β€” / β€” β€” / β€”
desktop-windows β€” / 27,778 β€” / β€” β€” / β€”
esp32 β€” / 584-853 β€” / 79KB-113KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 898 β€” / 33227KB β€” / 376KB
esp32s3-n16r8 β€” / 973 β€” / 8331KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-heavy-64 (measure) πŸ“

Setup (preceding non-measured steps): - heavy-64-w (set_control) - heavy-64-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 2,778-14,493 β€” / β€” β€” / β€”
desktop-windows β€” / 6,897 β€” / β€” β€” / β€”
esp32 β€” / 189-234 β€” / 61KB-90KB β€” / 25KB-64KB
esp32p4rev1-eth β€” / 229 β€” / 33218KB β€” / 376KB
esp32s3-n16r8 β€” / 251 β€” / 8320KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-heavy-128 (measure) πŸ“

Setup (preceding non-measured steps): - heavy-128-w (set_control) - heavy-128-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 898-3,584 β€” / β€” β€” / β€”
desktop-windows β€” / 1,689 β€” / β€” β€” / β€”
esp32 β€” / 178-238 β€” / 74KB-84KB β€” / 38KB
esp32p4rev1-eth β€” / 57.4 β€” / 33182KB β€” / 376KB
esp32s3-n16r8 β€” / 59.1 β€” / 8273KB β€” / 76KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-mod-16 (measure) πŸ“

Setup (preceding non-measured steps): - add-modifier-for-sweep (add_module) β€” Re-add the MultiplyModifier on the HEAVY effect and sweep grid sizes β€” the diff vs the matching measure-heavy-N step (no modifier) is the modifier's PER-FRAME cost at each grid, answering whether the ~180Β΅s seen at 16Β² scales with pixel count or is fixed overhead. - mod-16-w (set_control) - mod-16-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 125,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 500,000 β€” / β€” β€” / β€”
esp32 β€” / 2,874-4,673 β€” / 82KB-116KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 6,494 β€” / 33224KB β€” / 376KB
esp32s3-n16r8 β€” / 6,803 β€” / 8328KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-mod-32 (measure) πŸ“

Setup (preceding non-measured steps): - mod-32-w (set_control) - mod-32-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 52,632-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 1,227-1,558 β€” / 77KB-101KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 1,876 β€” / 33218KB β€” / 376KB
esp32s3-n16r8 β€” / 2,179 β€” / 8325KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-mod-64 (measure) πŸ“

Setup (preceding non-measured steps): - mod-64-w (set_control) - mod-64-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 13,514-55,556 β€” / β€” β€” / β€”
desktop-windows β€” / 27,778 β€” / β€” β€” / β€”
esp32 β€” / 332-457 β€” / 58KB-95KB β€” / 25KB-64KB
esp32p4rev1-eth β€” / 486 β€” / 33194KB β€” / 376KB
esp32s3-n16r8 β€” / 404 β€” / 8297KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-mod-128 (measure) πŸ“

Setup (preceding non-measured steps): - mod-128-w (set_control) - mod-128-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 2,703-14,286 β€” / β€” β€” / β€”
desktop-windows β€” / 6,897 β€” / β€” β€” / β€”
esp32 β€” / 187-238 β€” / 78KB-80KB β€” / 34KB
esp32p4rev1-eth β€” / 102 β€” / 33089KB β€” / 376KB
esp32s3-n16r8 β€” / 67.3 β€” / 8180KB β€” / 76KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

scenario_perf_light

test/scenarios/light/scenario_perf_light.json β€” Fast incremental performance check: start from the bare minimum render pipeline and add one thing at a time, measuring the tick/heap delta each step, so a regression shows up as a per-step jump. The LIGHT companion to scenario_perf_full β€” it stays small (≀64Β²) and driver-free so it runs in seconds. Mutate mode + canvas-preparing: the steps clear_children whatever Layouts/Effects/Drivers the device already had (the pre-wired apparatus like PreviewDriver/Board survives β€” clear_children only drops user-editable children) and rebuild a known tree, so it runs from any starting state and always measures the same minimal pipeline. Order: (1) minimal = Grid(16Β²)+Layer+a LIGHT effect (Spiral, a light effect), no modifier/driver/audio/discovery; (2) +MultiplyModifier (adds the mapping LUT β€” the heavy memory path); (3) +PreviewDriver; (4) swap to a HEAVY effect (Noise) to bracket the compute range; (5) grid 16Β²β†’32Β²β†’64Β² to show the size scaling. Full 128Β²/16K sweep, real LED/network drivers, audio+discovery cost: see scenario_perf_full.

Mode: mutate Β· Also touches: Layouts, GridLayout, Drivers, PreviewDriver, SpiralEffect, NoiseEffect, MultiplyModifier

measure-minimal (measure) πŸ“

Bare minimum: Grid(16Β²) + Layer + Spiral (light effect). No modifier, no driver. The render floor everything else is measured against.

Setup (preceding non-measured steps): - disable-audio (set_control) β€” Quiet I2S sampling so it can't pollute the tick (optional β€” device only). Targets the mic by its runtime name 'Audio' (displayNameFor strips the 'Service' suffix; the catalog adds it with id 'Audio'). - disable-devices (set_control) β€” Stop the blocking HTTP discovery sweep (optional β€” device only). - clear-layers (clear_children) β€” Drop whatever effects/modifiers the device had β€” start clean. - clear-layouts (clear_children) - clear-drivers (clear_children) β€” Drop any output driver; the pre-wired PreviewDriver is non-deletable and survives. - build-grid (add_module) - build-layer (add_module) - build-fx (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 111,111-500,000 β€” / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 816-971 β€” / 82KB-119KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 18,519 β€” / 33228KB β€” / 376KB
esp32s3-n16r8 β€” / 8,850 β€” / 8316KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-with-modifier (measure) πŸ“

Cost of the modifier + LUT over the minimal pipeline. Heap delta vs measure-minimal is the LUT allocation.

Setup (preceding non-measured steps): - add-modifier (add_module) β€” +MultiplyModifier: allocates the mapping LUT (the heavy memory path vs the 1:1 no-LUT case).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 250,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 2,188-3,378 β€” / 79KB-115KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 10,309 β€” / 33226KB β€” / 376KB
esp32s3-n16r8 β€” / 8,333 β€” / 8330KB β€” / 84KB
  • desktop-macos: observed 2026-09-03
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-with-preview (measure) πŸ“

PreviewDriver is the pre-wired apparatus β€” it survives clear_children and is already attached, so the measures above already include it (no add step needed; adding a second Preview is rejected). This is a stable repeat of the effect+modifier config for run-to-run variance.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 500,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 2,809-3,509 β€” / 82KB-117KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 10,753 β€” / 33226KB β€” / 376KB
esp32s3-n16r8 β€” / 8,621 β€” / 8328KB β€” / 84KB
  • desktop-macos: observed 2026-09-05
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-heavy-16 (measure) πŸ“

Setup (preceding non-measured steps): - swap-heavy-fx (replace_module) β€” Swap the light effect for a HEAVY one (Noise β€” simplex per pixel) to bracket the compute range at the same 16Β².

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 250,000-1,000,000 β€” / β€” β€” / β€”
desktop-windows β€” / 500,000 β€” / β€” β€” / β€”
esp32 β€” / 3,759-4,484 β€” / 83KB-117KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 6,494 β€” / 33224KB β€” / 376KB
esp32s3-n16r8 β€” / 6,803 β€” / 8332KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-heavy-32 (measure) πŸ“

Setup (preceding non-measured steps): - grid-32-w (set_control) - grid-32-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 71,429-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 1,266-1,553 β€” / 78KB-112KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 1,880 β€” / 33221KB β€” / 376KB
esp32s3-n16r8 β€” / 2,183 β€” / 8319KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

measure-heavy-64 (measure) πŸ“

Setup (preceding non-measured steps): - grid-64-w (set_control) - grid-64-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 16,393-58,824 β€” / β€” β€” / β€”
desktop-windows β€” / 28,571 β€” / β€” β€” / β€”
esp32 β€” / 272-458 β€” / 57KB-94KB β€” / 38KB-72KB
esp32p4rev1-eth β€” / 491 β€” / 33195KB β€” / 376KB
esp32s3-n16r8 β€” / 402 β€” / 8277KB β€” / 64KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-25
  • esp32s3-n16r8: observed 2026-07-24

Layouts

scenario_Layouts_mutation

test/scenarios/light/scenario_Layouts_mutation.json β€” Tree mutation on the Layouts container while the pipeline runs: add a second layout (multiple layouts under one Layouts), replace a layout with a different type, and remove a layout. The check is that each mutation leaves the pipeline RENDERING β€” Layer + Drivers re-wire via prepareTree and the buffer stays non-null and non-zero. Mirrors the HTTP add/replace/delete handlers; exercises the runner's add_module / replace_module / remove_module ops. NOTE: the Layer renders a dense bounding-box buffer sized by the layouts' coordinate EXTENT, not the summed light count β€” layouts that overlap in coordinate space share voxels (two 64x64 grids both occupy x,y in 0..63). There are no per-layout coordinate offsets, so multiple layouts share the same coordinate box; these steps assert liveness, not buffer-size arithmetic. Grids are 64x64 so the tick stays above the host's microsecond clock at every step.

Mode: mutate Β· Also touches: GridLayout, SphereLayout, Layer, RainbowEffect, Drivers, NetworkSendDriver

measure-one-layout (measure) πŸ“

Baseline: a single 64x64 grid layout drives the pipeline.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 20,000-58,824 β€” / β€” β€” / β€”
desktop-windows β€” / 37,037 β€” / β€” β€” / β€”
esp32 β€” / 76.7-89.8 β€” / 42KB-95KB β€” / 26KB-92KB
esp32-eth β€” / 41,667 β€” / 224KB β€” / 108KB
esp32s3-n16r8 β€” / 29,412 β€” / 7390KB β€” / 72KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32-eth: observed 2026-06-08
  • esp32s3-n16r8: observed 2026-07-24

measure-two-layouts (measure) πŸ“

Pipeline still renders with two layouts wired (buffer non-null, fps measurable).

Setup (preceding non-measured steps): - add-second-layout (add_module) β€” Add a SECOND layout (a 64x64 grid) under Layouts β€” two layouts now live under one container. prepareTree re-runs; the pipeline must still render. (Both grids share the 0..63 coordinate box, so the Layer buffer stays 64x64 β€” see the scenario NOTE.)

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 7,937-20,408 β€” / β€” β€” / β€”
desktop-windows β€” / 23,810 β€” / β€” β€” / β€”
esp32 β€” / 61.3-73.0 β€” / 33KB-77KB β€” / 26KB-60KB
esp32-eth β€” / 37,037 β€” / 223KB β€” / 108KB
esp32s3-n16r8 β€” / 22,222 β€” / 7453KB β€” / 72KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32-eth: observed 2026-06-08
  • esp32s3-n16r8: observed 2026-07-24

measure-after-replace (measure) πŸ“

Pipeline still renders after replacing a grid with a sphere (different layout type, same slot) β€” buffer re-wires without crashing.

Setup (preceding non-measured steps): - replace-second-layout (replace_module) β€” Replace the second grid with a SphereLayout (different type, same slot). The first grid is untouched; the pipeline re-wires to the new layout's light count.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 4,032-10,417 β€” / β€” β€” / β€”
desktop-windows β€” / 9,009 β€” / β€” β€” / β€”
esp32 β€” / 71.1-73.9 β€” / 29KB-95KB β€” / 16KB-80KB
esp32-eth β€” / 38,462 β€” / 223KB β€” / 108KB
esp32s3-n16r8 β€” / 27,027 β€” / 7108KB β€” / 72KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32-eth: observed 2026-06-08
  • esp32s3-n16r8: observed 2026-07-24

measure-after-remove (measure) πŸ“

Pipeline renders with the single remaining grid, same as the baseline.

Setup (preceding non-measured steps): - remove-second-layout (remove_module) β€” Remove the sphere β€” back to a single grid layout. Layer/Drivers shrink their buffers via prepareTree.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 21,277-50,000 β€” / β€” β€” / β€”
desktop-windows β€” / 38,462 β€” / β€” β€” / β€”
esp32 β€” / 72.2-84.2 β€” / 36KB-96KB β€” / 26KB-92KB
esp32-eth β€” / 41,667 β€” / 224KB β€” / 108KB
esp32s3-n16r8 β€” / 29,412 β€” / 7401KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32-eth: observed 2026-06-08
  • esp32s3-n16r8: observed 2026-07-24

MoonLiveEffect

scenario_MoonLiveEffect_controls

test/scenarios/light/scenario_MoonLiveEffect_controls.json β€” Exercise MoonLive Stage-1 CONTROLS end-to-end as a wired module. A script declares a member and surfaces it (addControl("speed", speed, 0, 15) in defineControls) and uses it (setRGB(speed, ...)); the engine surfaces the control, the binding creates a real MoonModule control of the widget its member's TYPE calls for, bound to the member's 4-byte slot in the live control-values arena. The scenario: add the effect with a control script (the control appears, renders), change the CONTROL value live (a slider move β€” must NOT recompile; the arena byte updates and the next tick reads it), edit the SOURCE to add a second control (recompile re-derives the set, existing slider value preserved by the stable-address grow-only arena), edit the source to remove a control (the orphaned value drops), push a broken script (compile fails, renders dark, status shows the diagnostic, no crash), recover, and remove + re-add (resource teardown + re-acquire). A crash in the LoadCtrl codegen, a dangling arena pointer across a recompile, or a value change that wrongly triggers a recompile all show up as a failed measure or a tick spike. The codegen + live-read contract is pinned by unit_moonlive_ir / unit_moonlive_compiler; this is the wired-module gate.

Mode: mutate Β· Also touches: Layouts, GridLayout, Effects, Layer, Drivers, NetworkSendDriver

add-control-script (add_module) πŸ“

Add a MoonLiveEffect whose source declares a speed control and uses it. The control appears bound to the arena slot (seeded to its default 7); the wired effect renders one pixel.

Setup (preceding non-measured steps): - stage-initial-script (write_file) β€” Stage a script before the card points at it.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 90,909 β€” / β€” β€” / β€”
esp32 β€” / 58.7-85.3 β€” / 34KB-95KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 216 β€” / 7376KB β€” / 60KB
  • desktop-macos: observed 2026-08-28
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

set-source-with-control (set_control) πŸ“

Edit the source to the control script. A source edit recompiles (affectsPrepare gates on script); the engine derives the speed control and the binding surfaces it.

Setup (preceding non-measured steps): - select-initial-script (set_control) β€” Point the card at its script file, so the controls it declares appear. - set-source-with-control-save (write_file) β€” Save the script file: editing a script in place is what re-derives its controls.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 74.4-83.0 β€” / 34KB-95KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 262 β€” / 7453KB β€” / 72KB
  • desktop-macos: observed 2026-08-31
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

change-control-live (set_control) πŸ“

Change the speed control value (a slider move). This must NOT recompile β€” affectsPrepare returns false for a scripted control; the arena byte updates and the next render tick reads it. Tick stays cheap (a recompile would spike it).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 879 β€” / 95KB β€” / 92KB
esp32s3-n16r8 β€” / 262 β€” / 7376KB β€” / 60KB
  • desktop-macos: observed 2026-08-31
  • esp32: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

edit-source-two-controls (set_control) πŸ“

Edit the source to add a second control. The recompile re-derives the control set; the stable-address grow-only arena keeps speed's live value while seeding the new slot.

Setup (preceding non-measured steps): - edit-source-two-controls-save (write_file) β€” Save the script file: editing a script in place is what re-derives its controls.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 100,000-1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 66.8-85.6 β€” / 34KB-95KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 230 β€” / 7399KB β€” / 84KB
  • desktop-macos: observed 2026-08-31
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

edit-source-shrink-to-one-control (set_control) πŸ“

Edit the source back to a single control. The control set shrinks 2 -> 1: speed stays bound (its live value kept), the removed hue's value is dropped, and the value change path is exercised without an unexpected recompile crash.

Setup (preceding non-measured steps): - edit-source-shrink-to-one-control-save (write_file) β€” Save the script file: editing a script in place is what re-derives its controls.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 333,333-1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 73.8-78.2 β€” / 34KB-95KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 262 β€” / 7382KB β€” / 60KB
  • desktop-macos: observed 2026-08-31
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

edit-source-broken (set_control) πŸ“

Push a broken script. Compile fails, the previous code is freed, the effect renders dark and the parse error surfaces in status β€” no crash.

Setup (preceding non-measured steps): - edit-source-broken-save (write_file) β€” Save the script file: editing a script in place is what re-derives its controls.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 500,000-1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 66.7-86.5 β€” / 34KB-95KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 262 β€” / 7400KB β€” / 72KB
  • desktop-macos: observed 2026-08-29
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

edit-source-recover (set_control) πŸ“

Recover with a valid control script β€” the effect compiles and renders again.

Setup (preceding non-measured steps): - edit-source-recover-save (write_file) β€” Save the script file: editing a script in place is what re-derives its controls.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 100,000-1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 74.2-77.5 β€” / 34KB-95KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 234 β€” / 7386KB β€” / 64KB
  • desktop-macos: observed 2026-09-03
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

re-add-control-effect (add_module) πŸ“

Re-add a fresh effect after the remove β€” exec memory + control arena re-acquired clean (it renders its default fill on add). Control re-acquisition itself is proven by the add-control-script step at the top: a freshly-added effect compiling a control source surfaces + seeds its control; construct-mode set_control can't apply a dynamically-added scripted control as the final asserted render, so the gate here is the bare re-add's liveness.

Setup (preceding non-measured steps): - remove-control-effect (remove_module) β€” Remove the effect β€” the engine releases its exec block AND its control arena (release).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 100,000-1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 66.2-86.1 β€” / 34KB-95KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 262 β€” / 7440KB β€” / 60KB
  • desktop-macos: observed 2026-08-30
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

re-select-script (set_control) πŸ“

Point the re-added card at the same script file, then TICK. Without a render after the select the buffer still holds what the removed card left, so this is what proves the re-added one draws.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 142,857-1,000,000 β€” / β€” β€” / β€”
esp32 β€” / 73.6-77.9 β€” / 34KB-43KB β€” / 26KB-38KB
  • desktop-macos: observed 2026-09-03
  • esp32: observed 2026-09-01

scenario_MoonLiveEffect_livescript

test/scenarios/light/scenario_MoonLiveEffect_livescript.json β€” Exercise a scripted MoonLiveEffect as a wired MoonModule end-to-end β€” the integration layer the unit tests can't reach. The effect compiles its source text to native code on-device and renders it into the Layer buffer each tick. Prepares its own canvas: Layout(Grid 16x16) + Layer + MoonLiveEffect, measures the default compile, then edits source live (a new fill color recompiles and keeps rendering), pushes a BROKEN script (compile fails, the previous code is freed, the effect renders dark and the parse error surfaces in status, no crash), recovers with a valid script, and finally removes + re-adds the effect (add/remove robustness in any order). A crash in the JIT/emit path, a failed recompile that wedges the tick, or a buffer overrun on an odd grid all show up as a failed measure. The compiler + emit golden bytes are pinned by unit_moonlive_compiler / unit_moonlive_fill; this is the live wired-module gate.

Mode: mutate Β· Also touches: Layouts, GridLayout, Effects, Layer, Drivers, NetworkSendDriver

add-moonlive (add_module) πŸ“

Add a MoonLiveEffect to the Layer. Its default source (random pixels) compiles on-device to native code; measure that the wired effect renders.

Setup (preceding non-measured steps): - stage-initial-script (write_file) β€” Stage the script the effect starts on, before it is added.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 45,455-166,667 β€” / β€” β€” / β€”
esp32 β€” / 71.5-75.1 β€” / 34KB-95KB β€” / 26KB-92KB
esp32p4rev1-eth β€” / 88.6 β€” / 33211KB β€” / 376KB
esp32s3-n16r8 β€” / 249 β€” / 7448KB β€” / 62KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-27
  • esp32s3-n16r8: observed 2026-07-24

edit-source-red (set_control) πŸ“

Live-edit the script source to a new color. A script edit triggers a recompile (affectsPrepare gates on script); the new native code swaps in and keeps rendering.

Setup (preceding non-measured steps): - select-initial-script (set_control) β€” Point the card at its script file. - edit-source-red-save (write_file) β€” Save the script file, the way the card editor writes it.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 41,667-200,000 β€” / β€” β€” / β€”
esp32 β€” / 73.2-86.1 β€” / 34KB-95KB β€” / 26KB-92KB
esp32p4rev1-eth β€” / 98.4 β€” / 33213KB β€” / 376KB
esp32s3-n16r8 β€” / 225 β€” / 7465KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-27
  • esp32s3-n16r8: observed 2026-07-24

edit-source-broken (set_control) πŸ“

Push a script that fails to parse. The compile fails, the engine reports the diagnostic in the module status and renders dark, but the device keeps running (robust, no reboot) β€” the script-editor failure path. The measure passes because the pipeline still ticks.

Setup (preceding non-measured steps): - edit-source-broken-save (write_file) β€” Save the script file, the way the card editor writes it.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 52,632-200,000 β€” / β€” β€” / β€”
esp32 β€” / 65.5-85.9 β€” / 34KB-95KB β€” / 26KB-92KB
esp32p4rev1-eth β€” / 94.6 β€” / 33209KB β€” / 376KB
esp32s3-n16r8 β€” / 229 β€” / 7467KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-27
  • esp32s3-n16r8: observed 2026-07-24

edit-source-recover (set_control) πŸ“

Push a valid script again. The engine recompiles cleanly and rendering resumes β€” a broken edit is fully recoverable.

Setup (preceding non-measured steps): - edit-source-recover-save (write_file) β€” Save the script file, the way the card editor writes it.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 47,619-200,000 β€” / β€” β€” / β€”
esp32 β€” / 73.5-77.1 β€” / 34KB-95KB β€” / 26KB-92KB
esp32p4rev1-eth β€” / 93.4 β€” / 33212KB β€” / 376KB
esp32s3-n16r8 β€” / 248 β€” / 7465KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-27
  • esp32s3-n16r8: observed 2026-07-24

shrink-grid-1x1 (set_control) πŸ“

Resize the canvas to 1x1 while the scripted effect renders β€” the smallest non-empty grid. The native fill loops over a single light; the run guards (non-null buffer, cpl>=3) keep it in-bounds. Pins the 'runs at every grid size' hard rule for the JIT'd routine.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 50,000-200,000 β€” / β€” β€” / β€”
esp32 β€” / 405-707 β€” / 35KB-113KB β€” / 26KB-92KB
esp32p4rev1-eth β€” / 862 β€” / 33215KB β€” / 376KB
esp32s3-n16r8 β€” / 868 β€” / 7535KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-27
  • esp32s3-n16r8: observed 2026-07-24

grow-grid-back (set_control) πŸ“

Resize back to a wider grid; the effect keeps rendering across the live dimension change (the no-reboot reconfiguration contract applied to scripted code).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 41,667-200,000 β€” / β€” β€” / β€”
esp32 β€” / 74.3-85.1 β€” / 34KB-113KB β€” / 26KB-92KB
esp32p4rev1-eth β€” / 97.0 β€” / 33209KB β€” / 376KB
esp32s3-n16r8 β€” / 136 β€” / 7515KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-27
  • esp32s3-n16r8: observed 2026-07-24

transpose-grid-h (set_control) πŸ“

Make it 16x8, then 8x16 below: the SAME sample count in a different shape. resize() keeps the buffer and its contents, which are laid out for the old geometry, so a scripted trail must clear both of its planes or the previous picture smears across the new one. The compiled effects each carry this guard; the binding does it on the script's behalf.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 125,000-200,000 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

transpose-grid-w (set_control) πŸ“

And the transpose: 8 wide by 16 high, the same count again. The pair proves the clear fires on a reshape rather than only on a size change, in both directions.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 166,667-200,000 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

transpose-restore (set_control) πŸ“

Back to the square the rest of the scenario assumes.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 166,667-200,000 β€” / β€” β€” / β€”
  • desktop-macos: observed 2026-09-06

remove-moonlive (remove_module) πŸ“

Remove the scripted effect. release frees the exec block; the Layer keeps rendering (now empty). Measures add/remove robustness.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 47,619-200,000 β€” / β€” β€” / β€”
esp32 β€” / 73.6-75.6 β€” / 36KB-114KB β€” / 26KB-92KB
esp32p4rev1-eth β€” / 88.2 β€” / 33209KB β€” / 376KB
esp32s3-n16r8 β€” / 135 β€” / 7516KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-27
  • esp32s3-n16r8: observed 2026-07-24

re-add-moonlive (add_module) πŸ“

Re-add a MoonLiveEffect after removal β€” the exec memory is re-acquired fresh, no leak, no stale pointer. The scripted effect renders again.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 34,483-200,000 β€” / β€” β€” / β€”
esp32 β€” / 62.0-85.7 β€” / 34KB-113KB β€” / 26KB-92KB
esp32p4rev1-eth β€” / 90.9 β€” / 33209KB β€” / 376KB
esp32s3-n16r8 β€” / 121 β€” / 7521KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-06-27
  • esp32s3-n16r8: observed 2026-07-24

re-select-script (set_control) πŸ“

Point the re-added card at the same script file, then TICK. Without a render after the select the buffer still holds what the removed card left, so this is what proves the re-added one draws.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 32,258-200,000 β€” / β€” β€” / β€”
esp32 β€” / 74.2-74.4 β€” / 34KB-47KB β€” / 26KB-38KB
  • desktop-macos: observed 2026-09-06
  • esp32: observed 2026-09-01

MoonLiveLayout

scenario_MoonLive_pipeline

test/scenarios/light/scenario_MoonLive_pipeline.json β€” Drive a WHOLE pipeline whose layout, effect and modifier are all live scripts, and verify it keeps rendering across every edit. This is the end-to-end proof the unit tests cannot give: they exercise each binding against a stub, while here a scripted layout decides where the lights are, a scripted effect colours them, and a scripted modifier folds the mapping, composed in one Layer feeding a real driver. The steps walk the live-authoring loop a user performs: start on the default 16x16 script, add a scripted effect and see it colour the fixture, then replace the layout script with a 24-light row and watch the buffer resize under a running effect; add a scripted modifier and edit it live; break the layout script and confirm the device degrades to zero lights rather than dying; recover it and confirm the fixture comes back. A scripted layout is the demanding case because the pipeline asks it for a light COUNT before asking where any light is, and both answers come from running the same script, so a mismatch shows up here as a wrong buffer size or a wedged tick rather than a compile error. The compile + emit path is pinned by unit_moonlive_compiler / unit_MoonLiveLayout; this is the wired-pipeline gate.

Mode: mutate Β· Also touches: MoonLiveEffect, MoonLiveModifier, MoonLive, Layouts, Layer

Trailing setup (no measurement after)

  • add-scripted-layout (add_module) β€” Add the scripted layout. A step, not fixture: on a live device only the containers are guaranteed to exist, so the instance has to be created here.
  • select-default-layout (set_control) β€” Point the scripted layout at the SHIPPED grid.mll β€” the fixture a user starts from. A shipped file, not a staged one: the live runner skips the fixture, so anything written there is absent on a real device.
  • baseline-default-script (measure)
  • add-scripted-effect (add_module)
  • effect-gradient-write (write_file) β€” Write the script file the card points at, the way the editor saves it.
  • effect-gradient (set_control)
  • measure-scripted-effect (measure)
  • layout-a-row-write (write_file) β€” Write the script file the card points at, the way the editor saves it.
  • layout-a-row (set_control)
  • measure-scripted-row (measure)
  • add-scripted-modifier (add_module)
  • modifier-mirror-write (write_file) β€” Write the script file the card points at, the way the editor saves it.
  • modifier-mirror (set_control)
  • measure-all-three-scripted (measure)
  • modifier-transpose-write (write_file) β€” Write the script file the card points at, the way the editor saves it.
  • modifier-transpose-resync (set_control) β€” Re-assert the same filename: editing a file in place must recompile it.
  • measure-modifier-edited (measure)
  • break-the-layout-write (write_file) β€” Write the script file the card points at, the way the editor saves it.
  • break-the-layout-resync (set_control) β€” Re-assert the same filename: editing a file in place must recompile it.
  • measure-broken-layout (measure)
  • recover-the-layout-write (write_file) β€” Write the script file the card points at, the way the editor saves it.
  • recover-the-layout-resync (set_control) β€” Re-assert the same filename: editing a file in place must recompile it.
  • measure-recovered (measure)
  • remove-modifier (remove_module)
  • measure-after-removal (measure) β€” The scripted effect still colours the scripted layout after the modifier goes.

MoonModule

scenario_MoonModule_control_change

test/scenarios/core/scenario_MoonModule_control_change.json β€” Measure the cost of control changes on a running pipeline. Toggles MultiplyModifier's mirrorX/Y at different points and verifies each change is applied without freezing the render loop. Companion to the MoonModule control-change gate unit tests (unit_MoonModule_control_change_gate.cpp) β€” this is the live equivalent.

Mode: mutate Β· Also touches: MultiplyModifier, NoiseEffect

baseline (set_control) πŸ“

Set NoiseEffect.scale=4 and measure baseline FPS (mirror on). Effect controls don't rebuild the pipeline β€” slider stutter check.

Setup (preceding non-measured steps): - canvas-clear-layers (clear_children) β€” Self-canvas: clear+rebuild the pipeline this scenario assumes, so it runs from any device state (order-independent in a chained live run). Pre-wired apparatus survives clear_children; replaces the old fixture+reset model. - canvas-clear-layouts (clear_children) - canvas-clear-drivers (clear_children) - canvas-grid (add_module) - canvas-layer (add_module) - canvas-noise (add_module) - canvas-mirror (add_module) - canvas-artnet (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 8,333 / 4,032-6,944 unlimited / β€” β€” / β€”
desktop-windows β€” / 4,405 β€” / β€” β€” / β€”
esp32 β€” / 4,717 β€” / 88KB β€” / 48KB
esp32-eth β€” / 10.6 β€” / 133KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 12.2 β‰₯ 103KB / 94KB β€” / 48KB
esp32p4rev1-eth β€” / 6,250 β€” / 33238KB β€” / 376KB
esp32s3-n16r8 β€” / 6,024 β€” / 8318KB β€” / 84KB
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-07-24
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-17
  • esp32s3-n16r8: observed 2026-07-24

disable-mirrorX (set_control) πŸ“

Disable mirrorX. Modifier control triggers a pipeline rebuild β€” measures the rebuilt path.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 5,000 / 4,049-7,463 unlimited / β€” β€” / β€”
desktop-windows β€” / 3,817 β€” / β€” β€” / β€”
esp32 β€” / 4,717 β€” / 88KB β€” / 48KB
esp32-eth β€” / 10.4 β€” / 132KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 12.0 β‰₯ 103KB / 94KB β€” / 48KB
esp32p4rev1-eth β€” / 6,135 β€” / 33238KB β€” / 376KB
esp32s3-n16r8 β€” / 6,536 β€” / 8321KB β€” / 84KB
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-07-24
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-17
  • esp32s3-n16r8: observed 2026-07-24

disable-mirrorY (set_control) πŸ“

Disable mirrorY. Mirror is now fully off β€” should land on the no-LUT path.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 2,500 / 4,184-7,937 unlimited / β€” β€” / β€”
desktop-windows β€” / 3,861 β€” / β€” β€” / β€”
esp32 β€” / 5,464 β€” / 88KB β€” / 48KB
esp32-eth β€” / 9.0 β€” / 132KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 11.1 β‰₯ 103KB / 94KB β€” / 48KB
esp32p4rev1-eth β€” / 6,061 β€” / 33238KB β€” / 376KB
esp32s3-n16r8 β€” / 6,494 β€” / 8317KB β€” / 80KB
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-08-21
  • esp32: observed 2026-07-24
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-17
  • esp32s3-n16r8: observed 2026-07-24

re-enable-mirrorY (set_control) πŸ“

Re-enable mirrorY and measure β€” the heavy LUT path must recover (FPS within 50% of baseline) without staying degraded.

Setup (preceding non-measured steps): - re-enable-mirrors (set_control) β€” Re-enable mirrorX (rebuild back to LUT path).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 8,333 / 4,049-7,874 unlimited / β€” β€” / β€”
desktop-windows β€” / 4,854 β€” / β€” β€” / β€”
esp32 β€” / 5,376 β€” / 88KB β€” / 48KB
esp32-eth β€” / 10.6 β€” / 132KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 12.1 β‰₯ 103KB / 94KB β€” / 48KB
esp32p4rev1-eth β€” / 6,098 β€” / 33238KB β€” / 376KB
esp32s3-n16r8 β€” / 6,803 β€” / 8322KB β€” / 84KB
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-07-24
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-17
  • esp32s3-n16r8: observed 2026-07-24

MqttModule

scenario_MqttModule_haDiscovery_toggle

test/scenarios/core/scenario_MqttModule_haDiscovery_toggle.json β€” Toggle the Home-Assistant MQTT-discovery announce on and off and confirm the render loop survives every change. Pins the robustness + No-reboot-to-apply guarantees for the new haDiscovery control: turning it on publishes a retained homeassistant/light//config (and subscribes ha/set); turning it off retracts the config (empty retained payload) β€” both live over the already-connected socket on tick1s(), never a reconnect and never on the render task. No toggle, in any order, may crash, wedge the tick, or leak heap. Runs live only β€” discovery needs a real broker to publish to and MqttModule only exists on a board whose catalog entry includes it (the Shelly model does), so the in-process desktop runner SKIPs it; on a device it flips the control over HTTP. The bound checks FPS + heap stay within range across the on/off/on cycle (a discovery publish is a transient cost, not a permanent degradation), proving the toggle never leaves the network task or render loop degraded. (Background: the announce/retract are on the 1 Hz tick, off the hot path β€” see the MqttModule /// Β§ Home Assistant MQTT Discovery.)

Mode: mutate Β· live-only (skipped in-process)

baseline-discovery-on (set_control) πŸ“

haDiscovery on (default) β€” captures the baseline FPS/heap for the next two steps.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 29,412 β€” / 117KB β€” / 108KB
esp32-eth-wifi β‰₯ 10.0 / β€” β‰₯ 88KB / β€” β€” / β€”
esp32s3-n16r8 β€” / 29,412 β€” / 7468KB β€” / 84KB
  • desktop-macos: observed 2026-08-08
  • esp32: observed 2026-07-24
  • esp32-eth-wifi: contract set 2026-07-06 "initial contract β€” pending first hardware capture"
  • esp32s3-n16r8: observed 2026-07-24

discovery-off (set_control) πŸ“

haDiscovery off β€” the retract publish (empty retained config) goes out; FPS/heap must match or exceed the baseline.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 27,778 β€” / 117KB β€” / 108KB
esp32-eth-wifi β‰₯ 10.0 / β€” β‰₯ 88KB / β€” β€” / β€”
esp32s3-n16r8 β€” / 27,778 β€” / 7401KB β€” / 84KB
  • desktop-macos: observed 2026-08-08
  • esp32: observed 2026-07-24
  • esp32-eth-wifi: contract set 2026-07-06 "initial contract β€” pending first hardware capture"
  • esp32s3-n16r8: observed 2026-07-24

discovery-on-again (set_control) πŸ“

haDiscovery on again β€” re-announce + re-subscribe live. Bound: FPS must stay within 20% of the baseline (proves toggling doesn't leave the network task in a degraded state).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 333,333 β€” / β€” β€” / β€”
esp32 β€” / 28,571 β€” / 118KB β€” / 108KB
esp32-eth-wifi β‰₯ 10.0 / β€” β‰₯ 88KB / β€” β€” / β€”
esp32s3-n16r8 β€” / 28,571 β€” / 7467KB β€” / 84KB
  • desktop-macos: observed 2026-08-08
  • esp32: observed 2026-07-24
  • esp32-eth-wifi: contract set 2026-07-06 "initial contract β€” pending first hardware capture"
  • esp32s3-n16r8: observed 2026-07-24

MultiplyModifier

scenario_MultiplyModifier_memory_lut

test/scenarios/light/scenario_MultiplyModifier_memory_lut.json β€” Verify that adding a MultiplyModifier allocates both the mapping LUT and the driver buffer (the heavy memory path). Companion to scenario_Layer_memory_1to1, which verifies the no-LUT path.

Mode: construct Β· Also touches: Layer, MappingLUT, BlendMap

add-artnet (add_module) πŸ“

Add NetworkSendDriver and run the bounded FPS measurement on the LUT path.

Setup (preceding non-measured steps): - add-layout-group (add_module) β€” Create the top-level Layouts container. - add-grid (add_module) β€” Add a 16x16 GridLayout. - add-layer (add_module) β€” Add a Layer wired to Layouts (RGB). - add-noise (add_module) β€” Add NoiseEffect as the Layer's effect. - add-mirror (add_module) β€” Add MultiplyModifier β€” triggers LUT and driver-buffer allocation. - add-driver-group (add_module) β€” Add a Drivers container wired to the Layer.

Bounds: - FPS β‰₯ 80% of baseline

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 8,333 / 47,619-333,333 unlimited / β€” β€” / β€”
desktop-windows β€” / 333,333 β€” / β€” β€” / β€”
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-08-23

scenario_MultiplyModifier_pipeline

test/scenarios/light/scenario_MultiplyModifier_pipeline.json β€” Pipeline with a mirror modifier: NoiseEffect renders one quadrant, MultiplyModifier reflects across X and Y to produce a kaleidoscope. Used to verify the MultiplyModifier wires into Layer cleanly and that the full pipeline still meets its FPS bound.

Mode: construct Β· Also touches: Layer, NoiseEffect, NetworkSendDriver

add-artnet (add_module) πŸ“

Add NetworkSendDriver and run the bounded FPS measurement (mirror + LUT path must stay at >=80% of the rated FPS).

Setup (preceding non-measured steps): - add-layout-group (add_module) β€” Create the top-level Layouts container. - add-grid (add_module) β€” Add a 128x128 GridLayout child to Layouts. Set explicitly (the module default is 16x16x1) so the tick is measurable above the host's microsecond clock. - add-layer (add_module) β€” Add a Layer wired to Layouts (RGB). - add-noise (add_module) β€” Add NoiseEffect as the Layer's effect. - add-mirror (add_module) β€” Add MultiplyModifier so logical pixels reflect across X and Y in the physical grid. - add-driver-group (add_module) β€” Add a Drivers container wired to the Layer's output buffer.

Bounds: - FPS β‰₯ 80% of baseline

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 8,333 / 3,534-7,937 unlimited / β€” β€” / β€”
desktop-windows β€” / 4,444 β€” / β€” β€” / β€”
  • desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-06
  • desktop-windows: observed 2026-07-08

NetworkModule

scenario_NetworkModule_eth_reconfigure

test/scenarios/core/scenario_NetworkModule_eth_reconfigure.json β€” Cycle the Ethernet PHY type (ethType: None/LAN8720/IP101/W5500) live on a running device and confirm the render loop survives every transition. Pins the robustness principle for the runtime Ethernet config: changing ethType reshapes the platform eth config (NetworkModule.syncEthLive β†’ setEthConfig, with a live ethStop+ethInit on W5500), and no value or transition order may crash or wedge the tick. Runs live only β€” the eth controls exist only on hasEthernet ESP32 builds, so the in-process desktop runner SKIPs it; on a device it drives the controls over HTTP. On RMII boards (Olimex) the change saves + asks for restart (no hot re-init), so the live HTTP connection is undisturbed; the W5500 path hot-reinits but the SPI bus teardown keeps the netif alive enough to keep serving.

Mode: mutate Β· live-only (skipped in-process)

ethType-lan8720 (set_control) πŸ“

LAN8720 (RMII) β€” the classic-ESP32 default. Baseline: device renders with Ethernet RMII selected.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
esp32 β€” / 1,022 β€” / 100KB β€” / 48KB
esp32s3-n16r8 β€” / 11.4 β€” / 7464KB β€” / 80KB
  • esp32: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

ethType-none (set_control) πŸ“

Switch to None (no Ethernet) live. The eth pin rows hide; the device must keep rendering and stay reachable.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
esp32 β€” / 1,021 β€” / 100KB β€” / 48KB
esp32s3-n16r8 β€” / 10.5 β€” / 7467KB β€” / 84KB
  • esp32: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

ethType-w5500 (set_control) πŸ“

Switch to W5500 (SPI) live. On an S3 this hot-reinits eth (ethStop + ethInit); on RMII boards it saves + flags restart. Either way the render loop must survive.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
esp32 β€” / 1,020 β€” / 100KB β€” / 48KB
esp32s3-n16r8 β€” / 10.2 β€” / 7467KB β€” / 84KB
  • esp32: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

ethType-ip101 (set_control) πŸ“

Switch to IP101 (RMII) live β€” the P4 PHY. Exercises the last dropdown value; render loop must survive.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
esp32 β€” / 1,007 β€” / 100KB β€” / 48KB
esp32s3-n16r8 β€” / 10.8 β€” / 7442KB β€” / 60KB
  • esp32: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

ethType-back-to-lan8720 (set_control) πŸ“

Return to LAN8720 β€” confirms the cycle is reversible and leaves the device in a sane state.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
esp32 β€” / 855 β€” / 100KB β€” / 48KB
esp32s3-n16r8 β€” / 11.1 β€” / 7465KB β€” / 84KB
  • esp32: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

scenario_NetworkModule_mdns_toggle

test/scenarios/core/scenario_NetworkModule_mdns_toggle.json β€” Toggle the mDNS responder on and off and measure render-FPS impact. Validates that mDNS announcement traffic doesn't degrade the render loop more than 20% on the busiest tick.

Mode: mutate Β· live-only (skipped in-process)

baseline-mdns-on (set_control) πŸ“

mDNS on (default) β€” captures the baseline FPS for the next two steps.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 76,923 β€” / β€” β€” / β€”
esp32 β€” / 28,571 β€” / 88KB β€” / 48KB
esp32-eth β€” / 10.6 β€” / 132KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 12.2 β‰₯ 103KB / 93KB β€” / 48KB
esp32p4rev1-eth β€” / 47,619 β€” / 33245KB β€” / 376KB
esp32s3-n16r8 β€” / 27,778 β€” / 7400KB β€” / 80KB
  • desktop-macos: observed 2026-08-08
  • esp32: observed 2026-07-22
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-17
  • esp32s3-n16r8: observed 2026-07-24

mdns-off (set_control) πŸ“

mDNS off β€” measured. Expected to match or exceed the baseline.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 76,923 β€” / β€” β€” / β€”
esp32 β€” / 27,778 β€” / 88KB β€” / 48KB
esp32-eth β€” / 10.5 β€” / 137KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 12.0 β‰₯ 93KB / 98KB β€” / 48KB
esp32p4rev1-eth β€” / 47,619 β€” / 33245KB β€” / 376KB
esp32s3-n16r8 β€” / 27,778 β€” / 7469KB β€” / 84KB
  • desktop-macos: observed 2026-08-08
  • esp32: observed 2026-07-22
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "shared heap budget; cumulative sweep state reduces standalone-mDNS-off heap by ~15KB" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-17
  • esp32s3-n16r8: observed 2026-07-24

mdns-on-again (set_control) πŸ“

mDNS on again β€” measured with a bound: FPS must stay within 20% of the baseline (proves toggling doesn't leave the network task in a degraded state).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 76,923 β€” / β€” β€” / β€”
esp32 β€” / 28,571 β€” / 83KB β€” / 48KB
esp32-eth β€” / 9.1 β€” / 132KB β€” / 48KB
esp32-eth-wifi β‰₯ 10.0 / 10.6 β‰₯ 103KB / 93KB β€” / 48KB
esp32p4rev1-eth β€” / 50,000 β€” / 33245KB β€” / 376KB
esp32s3-n16r8 β€” / 28,571 β€” / 7441KB β€” / 60KB
  • desktop-macos: observed 2026-08-08
  • esp32: observed 2026-07-22
  • esp32-eth: observed 2026-06-02
  • esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02
  • esp32p4rev1-eth: observed 2026-06-17
  • esp32s3-n16r8: observed 2026-07-24

NetworkSendDriver

scenario_Driver_mutation

test/scenarios/light/scenario_Driver_mutation.json β€” Add / remove output drivers under the Drivers container while the render pipeline runs, proving the robustness rule for driver deletion (the LED-driver delete path the product owner asked to cover; on the host RmtLed/I80Led/Parlio are inert via platform constants, so the portable NetworkSendDriver + PreviewDriver exercise the SAME generic add/remove lifecycle through the Scheduler that the hardware drivers use). Drivers are consumers of the Layer's output buffer, added/removed at runtime in any order. The checks assert the pipeline keeps RENDERING (buffer non-null, fps measurable) through each mutation: adding a second driver, removing it, and crucially removing a driver while the pipeline is still live (a driver teardown must release its resources without stranding the buffer or the other drivers β€” the same end-to-end Scheduler path the Audio producer/consumer scenario proves for peripherals, here for the output stage). Grid is 64x64 so the tick stays above the host microsecond clock at every step.

Mode: mutate Β· Also touches: GridLayout, Layer, RainbowEffect, Drivers, PreviewDriver

measure-one-driver (measure) πŸ“

Baseline: the pipeline renders with one driver (Preview) wired.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 10,101-50,000 β€” / β€” β€” / β€”
desktop-windows β€” / 27,778 β€” / β€” β€” / β€”
esp32 β€” / 77.3-89.8 β€” / 40KB-97KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 29,412 β€” / 7468KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-two-drivers (measure) πŸ“

Pipeline renders with both drivers wired.

Setup (preceding non-measured steps): - add-second-driver (add_module) β€” Add a NetworkSendDriver beside the Preview driver β€” two consumers of the same Layer output buffer. The add must not disturb the running pipeline.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 14,706-50,000 β€” / β€” β€” / β€”
desktop-windows β€” / 28,571 β€” / β€” β€” / β€”
esp32 β€” / 69.9-81.5 β€” / 38KB-83KB β€” / 26KB-80KB
esp32s3-n16r8 β€” / 26,316 β€” / 7453KB β€” / 60KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-three-drivers (measure) πŸ“

Pipeline renders with three drivers wired.

Setup (preceding non-measured steps): - add-third-driver (add_module) β€” Add a second NetworkSendDriver β€” three drivers now share the Layer output buffer (Preview + two ArtNet). Stacking editable drivers before tearing them down in sequence.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 15,625-47,619 β€” / β€” β€” / β€”
desktop-windows β€” / 23,810 β€” / β€” β€” / β€”
esp32 β€” / 70.0-81.7 β€” / 37KB-69KB β€” / 26KB-64KB
esp32s3-n16r8 β€” / 25,641 β€” / 7442KB β€” / 72KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-after-first-remove (measure) πŸ“

One ArtNet gone, Preview + ArtNet2 remain: pipeline keeps rendering (buffer non-null, fps measurable). No crash from the mid-list teardown.

Setup (preceding non-measured steps): - remove-first-added-driver (remove_module) β€” Remove the FIRST-added NetworkSendDriver while the others (Preview + ArtNet2) are still live β€” delete a middle consumer, not the last. Its teardown must release its resources without stranding the buffer or the surviving drivers.

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 12,048-50,000 β€” / β€” β€” / β€”
desktop-windows β€” / 23,810 β€” / β€” β€” / β€”
esp32 β€” / 59.6-78.1 β€” / 39KB-83KB β€” / 26KB-68KB
esp32s3-n16r8 β€” / 27,778 β€” / 7455KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-07-08
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

measure-back-to-one-driver (measure) πŸ“

Both added drivers gone, back to the single Preview baseline, still rendering β€” the add/remove cycle leaves the pipeline coherent.

Setup (preceding non-measured steps): - remove-second-added-driver (remove_module) β€” Remove the remaining editable driver (ArtNet2) too β€” back to just the boot-wired Preview. Repeated driver teardown leaves no residue; the pipeline still renders. (PreviewDriver is userEditable=false, so it stays β€” drivers always have at least the wired output consumer; this mirrors the live API, which forbids deleting code-wired modules.)

Bounds: - FPS β‰₯ 1 (absolute)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 15,152-50,000 β€” / β€” β€” / β€”
desktop-windows β€” / 27,778 β€” / β€” β€” / β€”
esp32 β€” / 70.1-83.5 β€” / 41KB-96KB β€” / 26KB-92KB
esp32s3-n16r8 β€” / 27,778 β€” / 7469KB β€” / 84KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32s3-n16r8: observed 2026-07-24

ParallelLedDriver

scenario_peripheral_grid_sweep

test/scenarios/light/scenario_peripheral_grid_sweep.json β€” Cross-product benchmark: for EACH output peripheral (RMT, i80, MoonI80, Parlio), sweep the grid 16x16 -> 32x32 -> 64x64 -> 128x128 and measure the tick/heap at each size, driving the FULL grid out that peripheral (16 lanes so 128x128=16384 lights distribute ~1024/lane). This is the apples-to-apples per-board number set: run it identically on any board and the per-peripheral, per-size ticks compare directly (an S31 vs a P4, say). Each peripheral block is optional:true, so a board that lacks one (a classic ESP32 has no MoonI80/Parlio; a P4 has all three) skips it without failing, and the observed blocks record only where the peripheral is real. A NoiseEffect fills every cell so the render cost is non-trivial at every size. Pins are the S3/P4/S31 clean 16-lane set; a board whose pins differ records a driver that failed to init (a small bailout tick, not an encode) for that peripheral. NOTE the driver drives the full grid, so at 128x128 (16384 lights) a whole-frame path may exceed its DMA budget and idle with a status (a clean degrade, captured as a fast bail tick) while the streaming ring / PSRAM path keeps driving; that contrast IS the measurement.

Mode: mutate Β· Also touches: MultiPinLedDriver, MoonLedDriver, ParlioLedDriver, RmtLedDriver, Layouts, GridLayout, Effects, Layer, NoiseEffect, Drivers, PreviewDriver

measure-rmt-16 (measure) πŸ“

Setup (preceding non-measured steps): - clear-layers (clear_children) - clear-drivers (clear_children) - build-grid (add_module) β€” One GridLayout, resized live per sweep step below. - build-layer (add_module) - build-fx (add_module) β€” A NoiseEffect fills every cell, so render is non-trivial at every grid size. - add-rmt (add_module) β€” RMT output (every ESP32). One pin; RMT drives per-channel, so this is the single-strand output baseline. - rmt-16-w (set_control) - rmt-16-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 71,429-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 2,273-2,309 β€” / 56KB β€” / 38KB
esp32p4rev1-eth β€” / 9,709 β€” / 33143KB β€” / 344KB
esp32s31 β€” / 5,376 β€” / 16573KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-rmt-32 (measure) πŸ“

Setup (preceding non-measured steps): - rmt-32-w (set_control) - rmt-32-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 11,905-55,556 β€” / β€” β€” / β€”
desktop-windows β€” / 27,027 β€” / β€” β€” / β€”
esp32 β€” / 670-692 β€” / 74KB-76KB β€” / 38KB
esp32p4rev1-eth β€” / 2,037 β€” / 33067KB β€” / 344KB
esp32s31 β€” / 1,473 β€” / 16500KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-rmt-64 (measure) πŸ“

Setup (preceding non-measured steps): - rmt-64-w (set_control) - rmt-64-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 4,149-14,085 β€” / β€” β€” / β€”
desktop-windows β€” / 6,250 β€” / β€” β€” / β€”
esp32 β€” / 179 β€” / 59KB β€” / 26KB-38KB
esp32p4rev1-eth β€” / 453 β€” / 32761KB β€” / 344KB
esp32s31 β€” / 346 β€” / 16194KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-rmt-128 (measure) πŸ“

Setup (preceding non-measured steps): - rmt-128-w (set_control) - rmt-128-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 1,066-3,484 β€” / β€” β€” / β€”
desktop-windows β€” / 1,541 β€” / β€” β€” / β€”
esp32 β€” / 719-858 β€” / 88KB β€” / 38KB
esp32p4rev1-eth β€” / 87.0 β€” / 31537KB β€” / 344KB
esp32s31 β€” / 162 β€” / 14966KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-i80-16 (measure) πŸ“

Setup (preceding non-measured steps): - remove-rmt (remove_module) - add-i80 (add_module) β€” i80 (esp_lcd): LCD_CAM on S3/P4/S31, I2S on classic. 16 lanes so 128x128=16384 distributes ~1024/lane. - i80-16-w (set_control) - i80-16-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 62,500-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 2,347 β€” / 79KB-80KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 4,808 β€” / 33166KB β€” / 344KB
esp32s31 β€” / 2,469 β€” / 16599KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-i80-32 (measure) πŸ“

Setup (preceding non-measured steps): - i80-32-w (set_control) - i80-32-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 16,393-55,556 β€” / β€” β€” / β€”
desktop-windows β€” / 27,027 β€” / β€” β€” / β€”
esp32 β€” / 659-691 β€” / 75KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 2,242 β€” / 33161KB β€” / 344KB
esp32s31 β€” / 1,835 β€” / 16594KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-i80-64 (measure) πŸ“

Setup (preceding non-measured steps): - i80-64-w (set_control) - i80-64-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 4,386-14,085 β€” / β€” β€” / β€”
desktop-windows β€” / 6,579 β€” / β€” β€” / β€”
esp32 β€” / 136-143 β€” / 57KB-58KB β€” / 22KB-38KB
esp32p4rev1-eth β€” / 536 β€” / 33143KB β€” / 344KB
esp32s31 β€” / 439 β€” / 16576KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-i80-128 (measure) πŸ“

Setup (preceding non-measured steps): - i80-128-w (set_control) - i80-128-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 1,135-3,247 β€” / β€” β€” / β€”
desktop-windows β€” / 1,553 β€” / β€” β€” / β€”
esp32 β€” / 180-189 β€” / 77KB-89KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 111 β€” / 33071KB β€” / 344KB
esp32s31 β€” / 151 β€” / 16504KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-moon-16 (measure) πŸ“

Setup (preceding non-measured steps): - remove-i80 (remove_module) - add-moonI80 (add_module) β€” MoonI80 (own-GDMA below esp_lcd, LCD_CAM only): the streaming ring, so length is not a memory question. Same 16-lane pin set. - moon-16-w (set_control) - moon-16-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 71,429-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 2,364-2,381 β€” / 79KB-80KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 4,167 β€” / 33166KB β€” / 344KB
esp32s31 β€” / 4,167 β€” / 16599KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-moon-32 (measure) πŸ“

Setup (preceding non-measured steps): - moon-32-w (set_control) - moon-32-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 18,868-58,824 β€” / β€” β€” / β€”
desktop-windows β€” / 27,027 β€” / β€” β€” / β€”
esp32 β€” / 645-655 β€” / 75KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 1,992 β€” / 33161KB β€” / 344KB
esp32s31 β€” / 1,477 β€” / 16595KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-moon-64 (measure) πŸ“

Setup (preceding non-measured steps): - moon-64-w (set_control) - moon-64-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 3,401-14,085 β€” / β€” β€” / β€”
desktop-windows β€” / 6,452 β€” / β€” β€” / β€”
esp32 β€” / 148-160 β€” / 57KB-58KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 434 β€” / 33143KB β€” / 344KB
esp32s31 β€” / 653 β€” / 16577KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-moon-128 (measure) πŸ“

Setup (preceding non-measured steps): - moon-128-w (set_control) - moon-128-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 1,089-3,497 β€” / β€” β€” / β€”
desktop-windows β€” / 1,560 β€” / β€” β€” / β€”
esp32 β€” / 180-859 β€” / 86KB-87KB β€” / 38KB
esp32p4rev1-eth β€” / 150 β€” / 33071KB β€” / 344KB
esp32s31 β€” / 165 β€” / 16499KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-parlio-16 (measure) πŸ“

Setup (preceding non-measured steps): - remove-moonI80 (remove_module) - add-parlio (add_module) β€” Parlio (P4 / S31): the Parallel-IO peripheral, 16 lanes. - parlio-16-w (set_control) - parlio-16-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 76,923-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 2,342-2,817 β€” / 79KB-80KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 2,584 β€” / 33166KB β€” / 344KB
esp32s31 β€” / 2,703 β€” / 16595KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-parlio-32 (measure) πŸ“

Setup (preceding non-measured steps): - parlio-32-w (set_control) - parlio-32-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 19,231-55,556 β€” / β€” β€” / β€”
desktop-windows β€” / 27,027 β€” / β€” β€” / β€”
esp32 β€” / 540-552 β€” / 73KB-76KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 1,961 β€” / 33161KB β€” / 344KB
esp32s31 β€” / 1,745 β€” / 16595KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-parlio-64 (measure) πŸ“

Setup (preceding non-measured steps): - parlio-64-w (set_control) - parlio-64-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 4,808-14,493 β€” / β€” β€” / β€”
desktop-windows β€” / 6,289 β€” / β€” β€” / β€”
esp32 β€” / 167-171 β€” / 54KB-58KB β€” / 22KB-38KB
esp32p4rev1-eth β€” / 432 β€” / 33143KB β€” / 344KB
esp32s31 β€” / 393 β€” / 16577KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

measure-parlio-128 (measure) πŸ“

Setup (preceding non-measured steps): - parlio-128-w (set_control) - parlio-128-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 1,144-3,559 β€” / β€” β€” / β€”
desktop-windows β€” / 1,560 β€” / β€” β€” / β€”
esp32 β€” / 201-230 β€” / 77KB-89KB β€” / 34KB-38KB
esp32p4rev1-eth β€” / 216 β€” / 33071KB β€” / 344KB
esp32s31 β€” / 81.5 β€” / 16504KB β€” / 164KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-01
  • esp32p4rev1-eth: observed 2026-07-25
  • esp32s31: observed 2026-07-25

Trailing setup (no measurement after)

  • remove-parlio (remove_module)
  • cleanup-fx (remove_module)
  • cleanup-layer (remove_module)
  • cleanup-grid (remove_module)

scenario_peripheral_switch

test/scenarios/light/scenario_peripheral_switch.json β€” Switch a live ParallelLedDriver between its DMA peripherals (i80 / MoonI80 / Parlio) and verify the device keeps running clean across every swap, and that the peripheral-dependent controls (doubleBuffer, pinExpander) behave per backend. Builds one Layout(Grid 16x16) + one Layer + one NoiseEffect + one ParallelLedDriver on lane-0 (64 LEDs), then set_control cycles peripheral and toggles doubleBuffer/pinExpander, measuring after each. The peripheral steps are optional so a board that lacks a given peripheral (a classic ESP32 offers only i80/I2S; a P4 adds Parlio; an S3 offers i80+MoonI80) skips them without failing. Guards the runtime-peripheral-strategy consolidation (ADR-0016): a swap that crashes, wedges, or leaves the driver in a bad state shows up here. Specifically pins the MoonI80 double-buffer fix: with doubleBuffer ON, MoonI80 must run single-buffer and NOT freeze (its own-GDMA whole-frame two-buffer handshake races); the measure after switching to MoonI80 with doubleBuffer ON is the regression guard (a freeze shows as a ~200 ms tick).

Mode: mutate Β· Also touches: MultiPinLedDriver, MoonLedDriver, ParlioLedDriver, Layouts, GridLayout, Effects, Layer, NoiseEffect, Drivers, PreviewDriver

measure-i80-single (measure) πŸ“

i80 single-buffer baseline. Only meaningful where the pins are valid (S3/desktop path); a classic/P4 records its own i80 backend.

Setup (preceding non-measured steps): - shrink-w (set_control) - shrink-h (set_control) - clear-layers (clear_children) β€” Start clean: drop whatever effects/modifiers the device had. - clear-drivers (clear_children) β€” Drop existing drivers (the pre-wired Preview survives on a device). - rebuild-layer (add_module) - rebuild-fx (add_module) - add-parallel-driver (add_module) β€” One ParallelLedDriver on lane 0, 64 LEDs (i80 rounds the bus up to 8 pins and parks the spare lanes; the 8-pin set is the S3's clean range). The peripheral is set per switch step below. - switch-i80 (set_control) β€” Select the i80 peripheral (esp_lcd: LCD_CAM on S3/P4/S31, I2S on classic).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 62,500-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 90.7-2,331 β€” / 75KB-118KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 4,808 β€” / 33166KB β€” / 344KB
esp32s3-n16r8 β€” / 23,810 β€” / 8292KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-02
  • esp32p4rev1-eth: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

measure-i80-double (measure) πŸ“

i80 with double-buffer: the encode overlaps the wire, so the tick should be at or below the single-buffer baseline. No freeze.

Setup (preceding non-measured steps): - i80-doublebuffer-on (set_control) β€” i80 supports the async double-buffer (esp_lcd has a real transaction queue) β€” turn it on.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 62,500-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 90.4-2,288 β€” / 77KB-117KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 4,651 β€” / 33166KB β€” / 344KB
esp32s3-n16r8 β€” / 21,739 β€” / 8216KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-02
  • esp32p4rev1-eth: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

measure-moonI80-nofreeze (measure) πŸ“

MoonI80 whole-frame, doubleBuffer requested but forced single. A ~200 ms tick here is the double-buffer freeze regressing; a normal tick is the fix holding.

Setup (preceding non-measured steps): - switch-moonI80-dbstill-on (set_control) β€” Switch to MoonI80 with doubleBuffer STILL ON. This is the regression guard: MoonI80 must ignore the async request and run single-buffer (its whole-frame own-GDMA two-buffer handshake races and wedges). The measure below must be a normal tick, NOT ~200 ms.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 71,429-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 90.5-2,353 β€” / 80KB-119KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 4,630 β€” / 33174KB β€” / 352KB
esp32s3-n16r8 β€” / 23,810 β€” / 8299KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-02
  • esp32p4rev1-eth: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

measure-moonI80-single (measure) πŸ“

Setup (preceding non-measured steps): - moonI80-doublebuffer-off (set_control) β€” Explicitly turn doubleBuffer off on MoonI80 (a no-op behaviorally β€” it was already forced single β€” but exercises the control write on the hidden control).

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 71,429-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 90.3-2,342 β€” / 78KB-119KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 4,785 β€” / 33166KB β€” / 344KB
esp32s3-n16r8 β€” / 22,727 β€” / 8292KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-02
  • esp32p4rev1-eth: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

measure-moonI80-ring (measure) πŸ“

MoonI80 ring path live. A crash/wedge on the expander engage shows here.

Setup (preceding non-measured steps): - moonI80-expander-on (set_control) β€” Turn on the 74HCT595 pin expander: MoonI80 switches to the streaming RING path (wantsRing()). At 64 LOGICAL lights the ring still initializes; this exercises the ring engage on a live switch without needing a 48x256 rig.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 66,667-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 90.1-2,299 β€” / 74KB-119KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 4,608 β€” / 33166KB β€” / 344KB
esp32s3-n16r8 β€” / 22,727 β€” / 8290KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-23
  • esp32: observed 2026-09-02
  • esp32p4rev1-eth: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

measure-back-i80 (measure) πŸ“

Setup (preceding non-measured steps): - moonI80-expander-off (set_control) - switch-parlio (set_control) — Select Parlio (P4 only). On a board that doesn't offer it (S3/classic) the Select value is out of range → the device returns 400 and this optional step is SKIPPED, leaving the peripheral where it was. No standalone measure follows it (that would record the wrong peripheral on a skip); the round-trip measure below covers the post-switch state. On a P4 the switch succeeds and the round-trip back to i80 exercises the Parlio→i80 swap. - switch-back-i80 (set_control) — Round-trip back to i80 — the driver must return to a clean, running state after the whole cycle.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β€” / 62,500-250,000 β€” / β€” β€” / β€”
desktop-windows β€” / 111,111 β€” / β€” β€” / β€”
esp32 β€” / 90.5-2,326 β€” / 76KB-119KB β€” / 38KB-92KB
esp32p4rev1-eth β€” / 4,854 β€” / 33174KB β€” / 352KB
esp32s3-n16r8 β€” / 23,810 β€” / 8279KB β€” / 80KB
  • desktop-macos: observed 2026-09-06
  • desktop-windows: observed 2026-08-22
  • esp32: observed 2026-09-02
  • esp32p4rev1-eth: observed 2026-07-24
  • esp32s3-n16r8: observed 2026-07-24

Trailing setup (no measurement after)

  • cleanup-driver (remove_module)
  • cleanup-fx (remove_module)
  • cleanup-layer (remove_module)

PolarLut

scenario_Fields_polar_lut

test/scenarios/light/scenario_Fields_polar_lut.json β€” The polar address table through a running pipeline: it is built for the grid a radial effect draws on, rebuilt when that grid resizes, dropped when the effect switches it off, and doubled when full precision is asked for. What the steps pin is that every one of those transitions happens live, under a rendering pipeline, without a reboot and without a stale table addressing pixels that no longer exist. The frame times are recorded per target and read as a TREND rather than as a pass mark: on a desktop the table and the computed address measure the same, because an out-of-order core with a large cache computes atan16 and dist16 about as fast as it loads two arrays. The 34% the table is worth was measured on an ESP32-S3, and it is an in-order-core property, so the contracts that matter here are the device ones. Aurora is measured last because it samples the field several times per pixel, which is what sets the fps a composition of this shape can promise.

Mode: mutate Β· Also touches: PolarNoiseEffect, AuroraEffect, GridLayout, Layer

add-polarnoise (add_module) πŸ“

A radial effect on the layer. It builds the table in prepare(), so the allocation happens here rather than on the first rendered frame.

Setup (preceding non-measured steps): - canvas-clear-layers (clear_children) β€” Self-canvas: clear and rebuild the pipeline this scenario assumes, so it runs from any device state. - canvas-clear-layouts (clear_children) - canvas-grid (add_module) β€” A 64x64 panel: large enough that the address table is real memory (8 KB narrow) and small enough to run on every target. - canvas-layer (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 250 / 2,604-3,448 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: PolarNoise at 64x64 through the address table" Β· observed 2026-09-06

table-off (set_control) πŸ“

Switch the table off under a running effect: the effect computes each pixel's angle and radius instead, which is the path a device short of memory takes. The picture is unchanged, which unit_PolarLut_equivalence pins bit-for-bit at full precision; what this step proves is that the switch takes effect live.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 167 / 3,125-3,509 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: the computed-address fallback, slower than the table above" Β· observed 2026-09-06

table-on (set_control) πŸ“

Back to the table, which prepare() rebuilds. Confirms the switch is live in both directions rather than one-way.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 250 / 3,155-3,521 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: back to the table cost" Β· observed 2026-09-06

table-wide (set_control) πŸ“

Full 16-bit precision: 4 bytes per pixel instead of 2, so the table doubles at this grid. Frame time barely moves on any target, which is what makes precision a memory decision rather than a speed one.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 238 / 2,950-3,521 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: the wide table, near the narrow one in time" Β· observed 2026-09-06

resize-down (set_control) πŸ“

Shrink the grid under a running effect. The table is rebuilt for the new geometry in prepare(), not carried over: a stale table would address pixels that no longer exist.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 417 / 6,452-7,092 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: half the pixels, half the table" Β· observed 2026-09-06

resize-up (set_control) πŸ“

Grow it again, the direction that has to allocate rather than free. On a device without PSRAM this is where prepare() weighs the table against free heap minus the reserve that protects stacks, WiFi and HTTP, and declines it rather than taking the last of the heap; the effect keeps rendering through the computed address.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 143 / 1,988-2,370 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: 64x96 with the wide table" Β· observed 2026-09-06

swap-to-aurora (add_module) πŸ“

Aurora alone on the layer: three layers of warped noise over the same address, each on its own oscillator. Several field samples per pixel, so this is the frame time that decides what the fps contract can promise.

Setup (preceding non-measured steps): - remove-polarnoise (remove_module) β€” Take PolarNoise off the layer before Aurora goes on it. Adding a second effect to a layer does not replace the first: both render, so the step below would have measured the pair and reported it as Aurora's cost.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 25.0 / 713-810 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: Aurora at 64x96, 3 layers, 2 octaves" Β· observed 2026-09-06

aurora-one-layer (set_control) πŸ“

Drop to a single layer, the cost knob at its cheapest. The ratio against the step above is what an effect author reads to choose a layer count for their fixture.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 62.5 / 1,695-2,083 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "initial contract: roughly a third of the three-layer cost" Β· observed 2026-09-06

TrailsEffect

scenario_Trails_ladder

test/scenarios/light/scenario_Trails_ladder.json β€” What a flow costs as a fixture grows, and what it costs to become a volume. Trails runs three passes over its own plane every frame (advect it along the flow, decay it by a half-life, write the emitters), so its cost is per light and the ladder is what proves that: 16x16, 32x32, 64x64 and then a 20x20x20 cube, which has roughly twice the lights of the 64x64 panel and must cost roughly twice as much rather than something worse. The last two steps hold the geometry still and move the controls instead, because a control that changes the cost would be the thing an author has to know about: persistence must not (the decay is a multiply per sample whatever the half-life) and dots must barely (an emitter is one write). The plane is 16-bit and there are two of them, so the memory each step reports is 6 bytes per light, which is the honest price of a tail that fades smoothly.

Mode: mutate Β· Also touches: draw, OscillatorBank, GridLayout, Layer

trails-16 (add_module) πŸ“

Trails as it ships, on 16x16. The tick is one advect plus one decay over the whole plane, plus a noise sample per light for the flow, so the per-light cost here is what every step below multiplies.

Setup (preceding non-measured steps): - canvas-clear-layers (clear_children) β€” Self-canvas: clear and rebuild the pipeline this scenario assumes, so it runs from any device state. - canvas-clear-layouts (clear_children) - canvas-grid (add_module) β€” A 16x16 panel to start the ladder: the smallest size a trail reads on, and the one every larger step is compared against per light. - canvas-layer (add_module)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 25,000 / 76,923-90,909 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "measured 2026-09-04, budget ~3x p50: Trails at 16x16, 3 dots, curl off" Β· observed 2026-09-06

grid-32-h (set_control) πŸ“

Setup (preceding non-measured steps): - grid-32 (set_control) β€” Four times the lights. Transport and decay are both per light, so this step and the next are what show the cost is linear rather than hiding a per-frame constant.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 5,556 / 17,544-22,222 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "measured 2026-09-04, budget ~3x p50: 32x32, four times 16x16" Β· observed 2026-09-06

grid-64-h (set_control) πŸ“

64x64: the size a panel user actually runs, and the one the catalog card's fps claim is read from.

Setup (preceding non-measured steps): - grid-64 (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 1,538 / 4,808-5,556 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "measured 2026-09-04, budget ~3x p50: 64x64, the panel size the card quotes" Β· observed 2026-09-06

cube-20-d (set_control) πŸ“

Setup (preceding non-measured steps): - cube-20 (set_control) β€” The volumetric case, and the bench fixture: a 20x20x20 cube is 8000 lights against the panel's 4096, and every slice is carried by the same flow. What this pins is that 3D is the same cost per light, not a different algorithm. - cube-20-h (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 1,000 / 2,525-2,865 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "measured 2026-09-04, budget ~3x p50: the 20-cube, 8000 lights" Β· observed 2026-09-06

long-tail (set_control) πŸ“

Persistence at its top. The decay is a multiply per sample whatever the half-life is, so a long tail must cost the same as a short one: a step that moved here would mean the decay had become conditional on its own result.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 1,000 / 1,645-2,817 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "measured 2026-09-04, budget ~3x p50: a long tail costs what a short one costs" Β· observed 2026-09-06

more-dots (set_control) πŸ“

Eight emitters rather than three. A dot is one write, so this is the cheapest control on the effect and the step proves it: the emitters are not what a large fixture pays for.

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 1,000 / 2,217-2,849 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "measured 2026-09-04, budget ~3x p50: emitters are nearly free next to the per-light passes" Β· observed 2026-09-06

back-to-panel-h (set_control) πŸ“

Setup (preceding non-measured steps): - back-to-panel (set_control) β€” Back to a flat 64x64, which frees the cube's planes. Returning near the earlier 64x64 number is what shows the effect carries no state that accumulates across a resize: the planes are rebuilt for the new geometry rather than reused. - back-to-panel-w (set_control)

Performance (contract / observed) β€” tick stored, FPS shown:

Board FPS heap block
desktop-macos β‰₯ 1,923 / 4,000-5,556 unlimited / β€” β€” / β€”
  • desktop-macos: contract set 2026-09-04 "measured 2026-09-04, budget ~3x p50: back to the 64x64 number above" Β· observed 2026-09-06